feat: validate podcast cost estimation accuracy, document per-token costs, and fix subscription/plan enforcement
Issue #543 — Validate Estimated Cost Accuracy (UI vs Backend) Backend: - cost_estimator.py uses pricing catalog (APIProviderPricing) as single source of truth - All 7 cost components: analysis, research (search+LLM), script, TTS, voice clone, avatar, video - initialize_default_pricing() runs on every app startup for auto-sync Frontend cost estimation fixes: - Added missing analysisCost, scriptCost, voiceCloneCost to PodcastEstimate type - toPodcastEstimate() now extracts all 7 backend fields (was dropping 3) - headerCostEst maps analysisCost->Analyze, scriptCost->Write, voiceCloneCost->Produce - EstimateCard shows 5 chips: Analysis, Research, Script, Voice(TTS+clone), Visuals(avatar+video) - Chip sum now equals backend total for all configurations Subscription & plan fixes: - Removed Stripe re-verification from checkSubscription() (downgrade regression fix #539) - Added verifyCheckoutRef pattern for reliable mount-time checkout polling - One-time Stripe sync effect with pending_subscription_change flag for Customer Portal returns - Free plan limits: stability_calls 3->10, audio_calls 5->10 (supports 2 podcasts) - Image enforcement uses actual provider (GPT_PROVIDER), not hardcoded Stability - Billing/pricing pages bypass onboarding check in ProtectedRoute - Gradient buttons + loading spinner on plan chip in UserBadge - Added metadata-based Stripe lookup fallback (Issue #538) Documentation: - TESTING_GUIDE.md: comprehensive testing instructions for non-technical testers - Free plan limits, usage tracking, cost estimation formulas - 10 test cases for UI verification - Troubleshooting guide - Quick-reference cost formulas with all default rates Cleanup: removed legacy ToBeMigrated directory (70+ files, ~22K LOC) GSC Brainstorm: service, hook, modal, and UI components for blog topic brainstorming
This commit is contained in:
@@ -241,7 +241,8 @@ def validate_exa_research_operations(
|
||||
def validate_image_generation_operations(
|
||||
pricing_service: PricingService,
|
||||
user_id: str,
|
||||
num_images: int = 1
|
||||
num_images: int = 1,
|
||||
provider_name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Validate image generation operation(s) before making API calls.
|
||||
@@ -250,25 +251,36 @@ def validate_image_generation_operations(
|
||||
pricing_service: PricingService instance
|
||||
user_id: User ID for subscription checking
|
||||
num_images: Number of images to generate (for multiple variations)
|
||||
provider_name: Actual image provider (e.g., 'stability', 'gemini', 'huggingface', 'wavespeed')
|
||||
|
||||
Returns:
|
||||
None
|
||||
If validation fails, raises HTTPException with 429 status
|
||||
"""
|
||||
try:
|
||||
# Map actual provider name to the APIProvider used for limit enforcement
|
||||
provider_map = {
|
||||
'stability': APIProvider.STABILITY,
|
||||
'gemini': APIProvider.GEMINI,
|
||||
'huggingface': APIProvider.MISTRAL, # HF images track to total_calls, enforce via MISTRAL
|
||||
'wavespeed': APIProvider.WAVESPEED,
|
||||
}
|
||||
api_provider = provider_map.get(provider_name or '', APIProvider.STABILITY)
|
||||
display_name = provider_name or 'stability'
|
||||
|
||||
# Create validation operations for each image
|
||||
operations_to_validate = [
|
||||
{
|
||||
'provider': APIProvider.STABILITY,
|
||||
'provider': api_provider,
|
||||
'tokens_requested': 0,
|
||||
'actual_provider_name': 'stability',
|
||||
'actual_provider_name': display_name,
|
||||
'operation_type': 'image_generation'
|
||||
}
|
||||
for _ in range(num_images)
|
||||
]
|
||||
|
||||
logger.info(f"[Pre-flight Validator] 🚀 Validating {num_images} image generation(s) for user {user_id}")
|
||||
|
||||
logger.info(f"[Pre-flight Validator] 🚀 Validating {num_images} image generation(s) for user {user_id}, provider={display_name}")
|
||||
|
||||
can_proceed, message, error_details = pricing_service.check_comprehensive_limits(
|
||||
user_id=user_id,
|
||||
operations=operations_to_validate
|
||||
@@ -278,7 +290,7 @@ def validate_image_generation_operations(
|
||||
logger.error(f"[Pre-flight Validator] Image generation blocked for user {user_id}: {message}")
|
||||
|
||||
usage_info = error_details.get('usage_info', {}) if error_details else {}
|
||||
provider = usage_info.get('provider', 'stability') if usage_info else 'stability'
|
||||
provider = usage_info.get('provider', display_name) if usage_info else display_name
|
||||
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
|
||||
@@ -564,11 +564,11 @@ class PricingService:
|
||||
"serper_calls_limit": 10,
|
||||
"metaphor_calls_limit": 0, # DISABLED: Metaphor not in Free tier
|
||||
"firecrawl_calls_limit": 0, # DISABLED: Firecrawl not in Free tier
|
||||
"stability_calls_limit": 3, # 3 images - enough to try the product
|
||||
"stability_calls_limit": 10, # 10 images - enough for 2 podcasts (5 images each)
|
||||
"exa_calls_limit": 10, # 10 research queries - enough to try the product
|
||||
"video_calls_limit": 2, # 2 video renders - try podcast video on Free
|
||||
"image_edit_calls_limit": 5, # 5 image edits - enough to try the product
|
||||
"audio_calls_limit": 5, # 5 audio clips - enough to try the product
|
||||
"audio_calls_limit": 10, # 10 audio clips - enough for 2 podcasts (5 clips each)
|
||||
"wavespeed_calls_limit": 0, # 0 = unlimited for Free; video controlled via video_calls_limit
|
||||
"gemini_tokens_limit": 50000,
|
||||
"openai_tokens_limit": 0, # DISABLED
|
||||
|
||||
Reference in New Issue
Block a user