Fix voice clone NotSupportedError and improve subscription services
This commit is contained in:
@@ -43,16 +43,9 @@ def _resolve_asset_path(user_id: str, category: str, filename: str) -> Path:
|
||||
safe_user_id = sanitize_user_id(user_id)
|
||||
repo_root = get_repo_root()
|
||||
|
||||
logger.warning(f"[Assets] repo_root: {repo_root}")
|
||||
logger.warning(f"[Assets] user_id: {user_id}, safe_user_id: {safe_user_id}")
|
||||
|
||||
file_path = (repo_root / "workspace" / f"workspace_{safe_user_id}" / "assets" / category / filename).resolve()
|
||||
|
||||
workspace_dir = (repo_root / "workspace" / f"workspace_{safe_user_id}").resolve()
|
||||
logger.warning(f"[Assets] resolved path: {file_path}")
|
||||
logger.warning(f"[Assets] workspace_dir: {workspace_dir}")
|
||||
logger.warning(f"[Assets] path exists: {file_path.exists()}")
|
||||
|
||||
if not str(file_path).startswith(str(workspace_dir)):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
|
||||
@@ -539,6 +539,7 @@ async def create_voice_clone(
|
||||
# 3. Save Preview Audio (if generated)
|
||||
preview_url = None
|
||||
preview_mime_type = "audio/wav"
|
||||
actual_filename = None # Default if preview save fails
|
||||
if preview_audio_bytes:
|
||||
from utils.media_utils import detect_audio_format, ensure_audio_extension
|
||||
|
||||
@@ -579,14 +580,16 @@ async def create_voice_clone(
|
||||
logger.warning(f"[VoiceClone] Failed to save preview audio: {error}")
|
||||
|
||||
# 4. Save to Asset Library
|
||||
# Use the preview file (with corrected .wav extension) as the main asset file
|
||||
stored_filename = actual_filename if preview_audio_bytes and saved_preview_path else filename
|
||||
asset_id = save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
file_path=file_path,
|
||||
asset_type="audio",
|
||||
source_module="voice_cloner",
|
||||
filename=filename,
|
||||
file_url=f"/api/assets/{user_id}/voice_samples/{filename}",
|
||||
filename=stored_filename,
|
||||
file_url=f"/api/assets/{user_id}/voice_samples/{stored_filename}",
|
||||
asset_metadata={
|
||||
"voice_name": voice_name,
|
||||
"engine": engine,
|
||||
@@ -599,12 +602,12 @@ async def create_voice_clone(
|
||||
)
|
||||
|
||||
logger.warning(f"[VoiceClone] Response preview_url: {preview_url}")
|
||||
logger.warning(f"[VoiceClone] Response filename: {filename}")
|
||||
logger.warning(f"[VoiceClone] Response stored_filename: {stored_filename}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"custom_voice_id": custom_voice_id,
|
||||
"preview_audio_url": preview_url or f"/api/assets/{user_id}/voice_samples/{filename}",
|
||||
"preview_audio_url": preview_url or f"/api/assets/{user_id}/voice_samples/{stored_filename}",
|
||||
"asset_id": asset_id,
|
||||
"message": "Voice clone created successfully"
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class SubscriptionPlan(Base):
|
||||
video_calls_limit = Column(Integer, default=0) # AI video generation
|
||||
image_edit_calls_limit = Column(Integer, default=0) # AI image editing
|
||||
audio_calls_limit = Column(Integer, default=0) # AI audio generation (text-to-speech)
|
||||
wavespeed_calls_limit = Column(Integer, default=0) # WaveSpeed API calls (LLM + TTS + video + image)
|
||||
|
||||
# Token Limits (for LLM providers)
|
||||
gemini_tokens_limit = Column(Integer, default=0)
|
||||
|
||||
@@ -107,6 +107,20 @@ class LimitValidator:
|
||||
}
|
||||
return result
|
||||
|
||||
# Helper: Check if a limit should be enforced based on tier
|
||||
def should_enforce_limit(limit_value: int, tier: str) -> bool:
|
||||
"""
|
||||
Determine if a limit should be enforced.
|
||||
- Free tier: 0 means DISABLED (not unlimited)
|
||||
- Basic/Pro/Enterprise: 0 means UNLIMITED
|
||||
"""
|
||||
if tier == 'free':
|
||||
# Free tier: 0 means disabled
|
||||
return limit_value > 0
|
||||
else:
|
||||
# Basic/Pro/Enterprise: 0 means unlimited
|
||||
return limit_value > 0
|
||||
|
||||
# Get user limits with error handling (STRICT: fail on errors)
|
||||
# CRITICAL: Expire SQLAlchemy objects to ensure we get fresh plan data after renewal
|
||||
try:
|
||||
@@ -144,6 +158,9 @@ class LimitValidator:
|
||||
logger.warning(f"[Subscription Check] No subscription or free tier found for user {user_id}, denying access")
|
||||
return False, "No subscription plan found. Please subscribe to a plan.", {}
|
||||
|
||||
# Extract tier for limit enforcement logic
|
||||
user_tier = limits.get('tier', 'free') if limits else 'free'
|
||||
|
||||
# Get current usage for this billing period with error handling
|
||||
# Use targeted expiry instead of expire_all() to avoid nuking the entire session cache
|
||||
try:
|
||||
@@ -245,8 +262,8 @@ class LimitValidator:
|
||||
(usage.mistral_calls or 0)
|
||||
)
|
||||
|
||||
# Only enforce limit if limit > 0 (0 means unlimited for Enterprise)
|
||||
if ai_text_gen_limit > 0 and current_total_llm_calls >= ai_text_gen_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(ai_text_gen_limit, user_tier) and current_total_llm_calls >= ai_text_gen_limit:
|
||||
logger.error(f"[Subscription Check] AI text generation call limit exceeded for user {user_id}: {current_total_llm_calls}/{ai_text_gen_limit} (provider: {display_provider_name})")
|
||||
result = (False, f"AI text generation call limit reached. Used {current_total_llm_calls} of {ai_text_gen_limit} total AI text generation calls this billing period.", {
|
||||
'current_calls': current_total_llm_calls,
|
||||
@@ -278,8 +295,8 @@ class LimitValidator:
|
||||
current_calls = getattr(usage, f"{provider_name}_calls", 0) or 0
|
||||
call_limit = limits['limits'].get(f"{provider_name}_calls", 0) or 0
|
||||
|
||||
# Only enforce limit if limit > 0 (0 means unlimited for Enterprise)
|
||||
if call_limit > 0 and current_calls >= call_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(call_limit, user_tier) and current_calls >= call_limit:
|
||||
logger.error(f"[Subscription Check] Call limit exceeded for user {user_id}, provider {display_provider_name}: {current_calls}/{call_limit}")
|
||||
result = (False, f"API call limit reached for {display_provider_name}. Used {current_calls} of {call_limit} calls this billing period.", {
|
||||
'current_calls': current_calls,
|
||||
@@ -296,7 +313,13 @@ class LimitValidator:
|
||||
logger.debug(f"[Subscription Check] Call limit check passed for user {user_id}, provider {display_provider_name}: {current_calls}/{call_limit if call_limit > 0 else 'unlimited'}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking call limits: {e}")
|
||||
# Continue to next check
|
||||
# Fail closed - deny if we can't verify the limit
|
||||
result = (False, f"Unable to verify call limit: {str(e)}", {})
|
||||
self.pricing_service._limits_cache[cache_key] = {
|
||||
'result': result,
|
||||
'expires_at': now + timedelta(seconds=30)
|
||||
}
|
||||
return result
|
||||
|
||||
# Check token limits for LLM providers with error handling
|
||||
# NOTE: token_limit = 0 means UNLIMITED (Enterprise plans)
|
||||
@@ -305,8 +328,8 @@ class LimitValidator:
|
||||
current_tokens = getattr(usage, f"{provider_name}_tokens", 0) or 0
|
||||
token_limit = limits['limits'].get(f"{provider_name}_tokens", 0) or 0
|
||||
|
||||
# Only enforce limit if limit > 0 (0 means unlimited for Enterprise)
|
||||
if token_limit > 0 and (current_tokens + tokens_requested) > token_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(token_limit, user_tier) and (current_tokens + tokens_requested) > token_limit:
|
||||
result = (False, f"Token limit would be exceeded for {display_provider_name}. Current: {current_tokens}, Requested: {tokens_requested}, Limit: {token_limit}", {
|
||||
'current_tokens': current_tokens,
|
||||
'requested_tokens': tokens_requested,
|
||||
@@ -328,14 +351,19 @@ class LimitValidator:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking token limits: {e}")
|
||||
# Continue to next check
|
||||
# Fail closed - deny if we can't verify the limit
|
||||
result = (False, f"Unable to verify token limit: {str(e)}", {})
|
||||
self.pricing_service._limits_cache[cache_key] = {
|
||||
'result': result,
|
||||
'expires_at': now + timedelta(seconds=30)
|
||||
}
|
||||
return result
|
||||
|
||||
# Check cost limits with error handling
|
||||
# NOTE: cost_limit = 0 means UNLIMITED (Enterprise plans)
|
||||
try:
|
||||
cost_limit = limits['limits'].get('monthly_cost', 0) or 0
|
||||
# Only enforce limit if limit > 0 (0 means unlimited for Enterprise)
|
||||
if cost_limit > 0 and usage.total_cost >= cost_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(cost_limit, user_tier) and usage.total_cost >= cost_limit:
|
||||
result = (False, f"Monthly cost limit reached. Current cost: ${usage.total_cost:.2f}, Limit: ${cost_limit:.2f}", {
|
||||
'current_cost': usage.total_cost,
|
||||
'limit': cost_limit,
|
||||
@@ -348,7 +376,13 @@ class LimitValidator:
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking cost limits: {e}")
|
||||
# Continue to success case
|
||||
# Fail closed - deny if we can't verify the limit
|
||||
result = (False, f"Unable to verify cost limit: {str(e)}", {})
|
||||
self.pricing_service._limits_cache[cache_key] = {
|
||||
'result': result,
|
||||
'expires_at': now + timedelta(seconds=30)
|
||||
}
|
||||
return result
|
||||
|
||||
# Calculate usage percentages for warnings
|
||||
try:
|
||||
@@ -503,6 +537,7 @@ class LimitValidator:
|
||||
return False, "No subscription plan found. Please subscribe to a plan.", {}
|
||||
|
||||
limits = limits_dict.get('limits', {})
|
||||
tier = limits_dict.get('tier', 'free')
|
||||
|
||||
# Track cumulative usage across all operations
|
||||
total_llm_calls = (
|
||||
@@ -547,7 +582,8 @@ class LimitValidator:
|
||||
# Count this operation as an LLM call
|
||||
projected_total_llm_calls = total_llm_calls + 1
|
||||
|
||||
if ai_text_gen_limit > 0 and projected_total_llm_calls > ai_text_gen_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(ai_text_gen_limit, tier) and projected_total_llm_calls > ai_text_gen_limit:
|
||||
error_info = {
|
||||
'current_calls': total_llm_calls,
|
||||
'limit': ai_text_gen_limit,
|
||||
@@ -654,7 +690,8 @@ class LimitValidator:
|
||||
|
||||
token_limit = limits.get(provider_tokens_key, 0) or 0
|
||||
|
||||
if token_limit > 0 and tokens_requested > 0:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(token_limit, tier) and tokens_requested > 0:
|
||||
projected_tokens = current_provider_tokens + tokens_requested
|
||||
logger.info(f" └─ Token Check: {current_provider_tokens} (current) + {tokens_requested} (requested) = {projected_tokens} (total) / {token_limit} (limit)")
|
||||
|
||||
@@ -716,7 +753,8 @@ class LimitValidator:
|
||||
image_limit = limits.get('stability_calls', 0) or 0
|
||||
projected_images = total_images + 1
|
||||
|
||||
if image_limit > 0 and projected_images > image_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(image_limit, tier) and projected_images > image_limit:
|
||||
error_info = {
|
||||
'current_images': total_images,
|
||||
'limit': image_limit,
|
||||
@@ -737,7 +775,8 @@ class LimitValidator:
|
||||
total_video_calls = usage.video_calls or 0
|
||||
projected_video_calls = total_video_calls + 1
|
||||
|
||||
if video_limit > 0 and projected_video_calls > video_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(video_limit, tier) and projected_video_calls > video_limit:
|
||||
error_info = {
|
||||
'current_calls': total_video_calls,
|
||||
'limit': video_limit,
|
||||
@@ -756,7 +795,8 @@ class LimitValidator:
|
||||
total_image_edit_calls = getattr(usage, 'image_edit_calls', 0) or 0
|
||||
projected_image_edit_calls = total_image_edit_calls + 1
|
||||
|
||||
if image_edit_limit > 0 and projected_image_edit_calls > image_edit_limit:
|
||||
# Enforce limit based on tier (Free: 0=disabled, others: 0=unlimited)
|
||||
if should_enforce_limit(image_edit_limit, tier) and projected_image_edit_calls > image_edit_limit:
|
||||
error_info = {
|
||||
'current_calls': total_image_edit_calls,
|
||||
'limit': image_edit_limit,
|
||||
@@ -789,6 +829,25 @@ class LimitValidator:
|
||||
'error_type': 'call_limit',
|
||||
'usage_info': error_info
|
||||
}
|
||||
|
||||
# Check WaveSpeed combined limit if actual_provider is WaveSpeed
|
||||
if actual_provider_name == 'wavespeed':
|
||||
wavespeed_limit = limits.get('wavespeed_calls', 0) or 0
|
||||
if should_enforce_limit(wavespeed_limit, tier):
|
||||
wavespeed_usage = usage.wavespeed_calls or 0
|
||||
projected_wavespeed = wavespeed_usage + 1
|
||||
if projected_wavespeed > wavespeed_limit:
|
||||
error_info = {
|
||||
'current_calls': wavespeed_usage,
|
||||
'limit': wavespeed_limit,
|
||||
'provider': 'wavespeed',
|
||||
'operation_type': operation_type,
|
||||
'operation_index': op_idx
|
||||
}
|
||||
return False, f"WaveSpeed API limit would be exceeded. Would use {projected_wavespeed} of {wavespeed_limit} WaveSpeed calls this billing period.", {
|
||||
'error_type': 'wavespeed_limit',
|
||||
'usage_info': error_info
|
||||
}
|
||||
|
||||
# All checks passed
|
||||
logger.info(f"[Pre-flight Check] ✅ All {len(operations)} operation(s) validated successfully")
|
||||
|
||||
@@ -505,21 +505,26 @@ class PricingService:
|
||||
"tier": SubscriptionTier.FREE,
|
||||
"price_monthly": 0.0,
|
||||
"price_yearly": 0.0,
|
||||
"gemini_calls_limit": 100,
|
||||
"openai_calls_limit": 0,
|
||||
"anthropic_calls_limit": 0,
|
||||
"mistral_calls_limit": 50,
|
||||
"tavily_calls_limit": 20,
|
||||
"serper_calls_limit": 20,
|
||||
"metaphor_calls_limit": 10,
|
||||
"firecrawl_calls_limit": 10,
|
||||
"stability_calls_limit": 5,
|
||||
"exa_calls_limit": 100,
|
||||
"video_calls_limit": 0, # No video generation for free tier
|
||||
"image_edit_calls_limit": 10, # 10 AI image editing calls/month
|
||||
"audio_calls_limit": 20, # 20 AI audio generation calls/month
|
||||
"gemini_tokens_limit": 100000,
|
||||
"monthly_cost_limit": 0.0,
|
||||
"ai_text_generation_calls_limit": 50, # Explicit: Free gets 50 AI text calls (via Gemini fallback)
|
||||
"gemini_calls_limit": 50,
|
||||
"openai_calls_limit": 0, # DISABLED: OpenAI access not included in Free tier
|
||||
"anthropic_calls_limit": 0, # DISABLED: Anthropic access not included in Free tier
|
||||
"mistral_calls_limit": 0, # DISABLED: HuggingFace not in Free tier
|
||||
"tavily_calls_limit": 10,
|
||||
"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
|
||||
"exa_calls_limit": 10, # 10 research queries - enough to try the product
|
||||
"video_calls_limit": 0, # DISABLED: Video generation not in Free tier
|
||||
"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
|
||||
"wavespeed_calls_limit": 0, # DISABLED: WaveSpeed not included in Free tier
|
||||
"gemini_tokens_limit": 50000,
|
||||
"openai_tokens_limit": 0, # DISABLED
|
||||
"anthropic_tokens_limit": 0, # DISABLED
|
||||
"mistral_tokens_limit": 0, # DISABLED
|
||||
"monthly_cost_limit": 2.0, # $2 cap - prevents runaway costs on free tier
|
||||
"features": ["basic_content_generation", "limited_research"],
|
||||
"description": "Perfect for trying out ALwrity"
|
||||
},
|
||||
@@ -528,7 +533,7 @@ class PricingService:
|
||||
"tier": SubscriptionTier.BASIC,
|
||||
"price_monthly": 29.0,
|
||||
"price_yearly": 290.0,
|
||||
"ai_text_generation_calls_limit": 50, # INCREASED: Unified limit for all LLM providers (OSS-focused strategy)
|
||||
"ai_text_generation_calls_limit": 500, # Unified limit for all LLM providers
|
||||
"gemini_calls_limit": 1000, # Legacy, kept for backwards compatibility (not used for enforcement)
|
||||
"openai_calls_limit": 500,
|
||||
"anthropic_calls_limit": 200,
|
||||
@@ -537,16 +542,17 @@ class PricingService:
|
||||
"serper_calls_limit": 200,
|
||||
"metaphor_calls_limit": 100,
|
||||
"firecrawl_calls_limit": 100,
|
||||
"stability_calls_limit": 50, # INCREASED: Now includes WaveSpeed OSS models (Qwen Image $0.03)
|
||||
"exa_calls_limit": 500,
|
||||
"video_calls_limit": 30, # INCREASED: 30 videos/month (WAN 2.5 OSS $0.25)
|
||||
"image_edit_calls_limit": 50, # INCREASED: 50 AI image editing calls/month (Qwen Edit OSS $0.02)
|
||||
"stability_calls_limit": 25, # 25 images - good for podcast episode covers
|
||||
"exa_calls_limit": 100, # 100 research queries
|
||||
"video_calls_limit": 10, # 10 videos - enough for a few podcast episodes
|
||||
"image_edit_calls_limit": 25, # 25 AI image edits
|
||||
"audio_calls_limit": 100, # INCREASED: 100 AI audio generation calls/month (Minimax Speech OSS)
|
||||
"wavespeed_calls_limit": 200, # WaveSpeed combined limit: TTS + video + image + LLM (Minimax Speech $0.002/min, Qwen $0.03/img, Kling $0.25/5s)
|
||||
"gemini_tokens_limit": 100000, # INCREASED: 100K tokens per provider (OSS-focused strategy)
|
||||
"openai_tokens_limit": 100000, # INCREASED: 100K tokens per provider
|
||||
"anthropic_tokens_limit": 100000, # INCREASED: 100K tokens per provider
|
||||
"mistral_tokens_limit": 100000, # INCREASED: 100K tokens per provider
|
||||
"monthly_cost_limit": 45.0, # ADJUSTED: $45 cap (aligns with $40-50 hard limit target)
|
||||
"monthly_cost_limit": 25.0, # $25 cap - podcast-focused pricing
|
||||
"features": ["full_content_generation", "advanced_research", "basic_analytics", "all_tools_access", "oss_models_priority"],
|
||||
"description": "Perfect for individuals and small teams. Access all ALwrity features with generous limits powered by OSS AI models."
|
||||
},
|
||||
@@ -555,6 +561,7 @@ class PricingService:
|
||||
"tier": SubscriptionTier.PRO,
|
||||
"price_monthly": 79.0,
|
||||
"price_yearly": 790.0,
|
||||
"ai_text_generation_calls_limit": 3000, # Explicit: Pro gets 3000 AI text calls
|
||||
"gemini_calls_limit": 5000,
|
||||
"openai_calls_limit": 2500,
|
||||
"anthropic_calls_limit": 1000,
|
||||
@@ -563,16 +570,17 @@ class PricingService:
|
||||
"serper_calls_limit": 1000,
|
||||
"metaphor_calls_limit": 500,
|
||||
"firecrawl_calls_limit": 500,
|
||||
"stability_calls_limit": 200,
|
||||
"exa_calls_limit": 2000,
|
||||
"video_calls_limit": 50, # 50 videos/month for pro plan
|
||||
"image_edit_calls_limit": 100, # 100 AI image editing calls/month
|
||||
"audio_calls_limit": 200, # 200 AI audio generation calls/month
|
||||
"stability_calls_limit": 100, # 100 images - good for regular podcasts
|
||||
"exa_calls_limit": 500, # 500 research queries
|
||||
"video_calls_limit": 30, # 30 videos - enough for daily episodes
|
||||
"image_edit_calls_limit": 100, # 100 AI image edits
|
||||
"audio_calls_limit": 100, # 100 audio clips - podcast-focused
|
||||
"wavespeed_calls_limit": 500, # WaveSpeed combined limit: TTS + video + image + LLM
|
||||
"gemini_tokens_limit": 5000000,
|
||||
"openai_tokens_limit": 2500000,
|
||||
"anthropic_tokens_limit": 1000000,
|
||||
"mistral_tokens_limit": 2500000,
|
||||
"monthly_cost_limit": 150.0,
|
||||
"monthly_cost_limit": 100.0, # $100 cap - podcast-focused
|
||||
"features": ["unlimited_content_generation", "premium_research", "advanced_analytics", "priority_support"],
|
||||
"description": "Perfect for growing businesses"
|
||||
},
|
||||
@@ -581,6 +589,7 @@ class PricingService:
|
||||
"tier": SubscriptionTier.ENTERPRISE,
|
||||
"price_monthly": 199.0,
|
||||
"price_yearly": 1990.0,
|
||||
"ai_text_generation_calls_limit": 0, # Unlimited
|
||||
"gemini_calls_limit": 0, # Unlimited
|
||||
"openai_calls_limit": 0,
|
||||
"anthropic_calls_limit": 0,
|
||||
@@ -594,6 +603,7 @@ class PricingService:
|
||||
"video_calls_limit": 0, # Unlimited for enterprise
|
||||
"image_edit_calls_limit": 0, # Unlimited image editing for enterprise
|
||||
"audio_calls_limit": 0, # Unlimited audio generation for enterprise
|
||||
"wavespeed_calls_limit": 0, # Unlimited for enterprise
|
||||
"gemini_tokens_limit": 0,
|
||||
"openai_tokens_limit": 0,
|
||||
"anthropic_tokens_limit": 0,
|
||||
@@ -815,6 +825,7 @@ class PricingService:
|
||||
'video_calls': getattr(plan, 'video_calls_limit', 0), # Support missing column
|
||||
'image_edit_calls': getattr(plan, 'image_edit_calls_limit', 0), # Support missing column
|
||||
'audio_calls': getattr(plan, 'audio_calls_limit', 0), # Support missing column
|
||||
'wavespeed_calls': getattr(plan, 'wavespeed_calls_limit', 0), # WaveSpeed API calls
|
||||
# Token limits
|
||||
'gemini_tokens': plan.gemini_tokens_limit,
|
||||
'openai_tokens': plan.openai_tokens_limit,
|
||||
|
||||
@@ -29,10 +29,12 @@ def ensure_subscription_plan_columns(db: Session) -> None:
|
||||
|
||||
# Columns we may reference in models but might be missing in older DBs
|
||||
required_columns = {
|
||||
"ai_text_generation_calls_limit": "INTEGER DEFAULT 0",
|
||||
"exa_calls_limit": "INTEGER DEFAULT 0",
|
||||
"video_calls_limit": "INTEGER DEFAULT 0",
|
||||
"image_edit_calls_limit": "INTEGER DEFAULT 0",
|
||||
"audio_calls_limit": "INTEGER DEFAULT 0",
|
||||
"wavespeed_calls_limit": "INTEGER DEFAULT 0",
|
||||
}
|
||||
|
||||
for col_name, ddl in required_columns.items():
|
||||
|
||||
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from loguru import logger
|
||||
import json
|
||||
from api.subscription.cache import clear_dashboard_cache
|
||||
|
||||
from models.subscription_models import (
|
||||
APIUsageLog, UsageSummary, APIProvider, UsageAlert,
|
||||
@@ -170,6 +171,12 @@ class UsageTrackingService:
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# Invalidate dashboard cache so header stats update immediately
|
||||
try:
|
||||
clear_dashboard_cache(user_id)
|
||||
except Exception as cache_err:
|
||||
logger.debug(f"Could not clear dashboard cache: {cache_err}")
|
||||
|
||||
logger.info(f"Tracked API usage: {user_id} -> {provider.value} -> ${cost_data['cost_total']:.6f}")
|
||||
|
||||
return {
|
||||
@@ -521,20 +528,26 @@ class UsageTrackingService:
|
||||
|
||||
async def reset_current_billing_period(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Reset usage status and counters for the current billing period (after plan renewal/change)."""
|
||||
period_keys = self._get_authoritative_billing_period_keys(user_id)
|
||||
billing_period = period_keys["billing_period"]
|
||||
summary = self.db.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period.in_(period_keys["lookup_periods"])
|
||||
).first()
|
||||
|
||||
if not summary:
|
||||
return {"reset": False, "reason": "no_summary"}
|
||||
|
||||
try:
|
||||
period_keys = self._get_authoritative_billing_period_keys(user_id)
|
||||
billing_period = period_keys["billing_period"]
|
||||
summary = self.db.query(UsageSummary).filter(
|
||||
UsageSummary.user_id == user_id,
|
||||
UsageSummary.billing_period.in_(period_keys["lookup_periods"])
|
||||
).first()
|
||||
|
||||
if not summary:
|
||||
return {"reset": False, "reason": "no_summary"}
|
||||
|
||||
reset_usage_summary_counters(summary)
|
||||
self.db.commit()
|
||||
|
||||
|
||||
# Invalidate dashboard cache so header stats update after reset
|
||||
try:
|
||||
clear_dashboard_cache(user_id)
|
||||
except Exception as cache_err:
|
||||
logger.debug(f"Could not clear dashboard cache: {cache_err}")
|
||||
|
||||
logger.info(f"Reset usage counters for user {user_id} in billing period {billing_period} after renewal")
|
||||
return {"reset": True, "counters_reset": True}
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user