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:
ajaysi
2026-05-27 08:46:38 +05:30
parent 96fa469fe8
commit aaf94049da
100 changed files with 2953 additions and 22118 deletions

View File

@@ -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,

View File

@@ -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