Files
ajaysi 928c2f20aa fix: WYSIWYG editor, content generation, and writing assistant bug fixes
- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField
- Fix blog title not truncating: add min-w-0 for flex item overflow
- Fix outline generation 500: escape curly braces in f-string prompt template
- Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager
- Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient
- Fix hallucination detector 404: explicitly include router in main.py and app.py
- Fix missing error_data in task failure responses
- Hide CopilotKit web inspector button
- Remove hardcoded fallback suggestions from SmartTypingAssist
- Fix stale closure refs in SmartTypingAssist handleTypingChange
- Add two-column editor layout, stats bar, section hover menu
- Various subscription, billing, and research module improvements
2026-05-14 09:11:51 +05:30

139 lines
5.8 KiB
Python

"""
Shared utility functions for subscription API routes.
"""
from typing import Dict, Any, Optional
from sqlalchemy.orm import Session
from loguru import logger
import sqlite3
from models.subscription_models import SubscriptionPlan
def format_plan_limits(plan: SubscriptionPlan) -> Dict[str, Any]:
"""
Format subscription plan limits for API response.
Includes _zero_means metadata per field to disambiguate:
- 'disabled': 0 means the feature is not available (Free tier)
- 'unlimited': 0 means unlimited usage (Enterprise tier)
- 'limited': >0 means numerical limit applies
Args:
plan: SubscriptionPlan model instance
Returns:
Dictionary with formatted limits and _zero_means metadata
"""
tier = plan.tier.value if hasattr(plan.tier, 'value') else str(plan.tier)
is_enterprise = tier == 'enterprise'
limit_fields = {
"ai_text_generation_calls": getattr(plan, 'ai_text_generation_calls_limit', None) or 0,
"gemini_calls": plan.gemini_calls_limit,
"openai_calls": plan.openai_calls_limit,
"anthropic_calls": plan.anthropic_calls_limit,
"mistral_calls": plan.mistral_calls_limit,
"tavily_calls": plan.tavily_calls_limit,
"serper_calls": plan.serper_calls_limit,
"metaphor_calls": plan.metaphor_calls_limit,
"firecrawl_calls": plan.firecrawl_calls_limit,
"stability_calls": plan.stability_calls_limit,
"video_calls": getattr(plan, 'video_calls_limit', 0) or 0,
"image_edit_calls": getattr(plan, 'image_edit_calls_limit', 0) or 0,
"audio_calls": getattr(plan, 'audio_calls_limit', 0) or 0,
"exa_calls": getattr(plan, 'exa_calls_limit', 0) or 0,
"wavespeed_calls": getattr(plan, 'wavespeed_calls_limit', 0) or 0,
"gemini_tokens": plan.gemini_tokens_limit,
"openai_tokens": plan.openai_tokens_limit,
"anthropic_tokens": plan.anthropic_tokens_limit,
"mistral_tokens": plan.mistral_tokens_limit,
"monthly_cost": plan.monthly_cost_limit,
}
# Build _zero_means metadata: indicates whether 0 means 'disabled' or 'unlimited'
zero_means = {}
for field, value in limit_fields.items():
if field == "monthly_cost":
zero_means[field] = "disabled"
elif is_enterprise:
# Enterprise: 0 means unlimited for all call/token fields
zero_means[field] = "unlimited"
else:
# Free/Basic/Pro: determine per-field
# Fields that are 0=disabled on Free tier but 0=unlimited on Basic/Pro
call_and_token_fields = {
"gemini_calls", "openai_calls", "anthropic_calls", "mistral_calls",
"tavily_calls", "serper_calls", "metaphor_calls", "firecrawl_calls",
"stability_calls", "video_calls", "image_edit_calls", "audio_calls",
"exa_calls", "wavespeed_calls", "ai_text_generation_calls",
"gemini_tokens", "openai_tokens", "anthropic_tokens", "mistral_tokens",
}
if field in call_and_token_fields:
if value == 0:
zero_means[field] = "disabled" if tier == "free" else "unlimited"
else:
zero_means[field] = "limited"
else:
zero_means[field] = "limited" if value > 0 else "disabled"
return {
**limit_fields,
"_zero_means": zero_means,
}
def handle_schema_error(
error: Exception,
db: Session,
error_str: str,
retry_func: callable
) -> Any:
"""
Handle database schema errors by fixing schema and retrying.
Args:
error: The original exception
error_str: Lowercase string representation of error
db: Database session
retry_func: Function to retry after schema fix
Returns:
Result from retry_func
Raises:
HTTPException: If schema fix fails
"""
if 'no such column' in error_str:
logger.warning("Missing column detected, attempting schema fix...")
try:
import services.subscription.schema_utils as schema_utils
# Reset schema check flags based on error type
if 'exa_calls_limit' in error_str or 'video_calls_limit' in error_str or \
'image_edit_calls_limit' in error_str or 'audio_calls_limit' in error_str:
schema_utils._checked_subscription_plan_columns = False
from services.subscription.schema_utils import ensure_subscription_plan_columns
ensure_subscription_plan_columns(db)
elif 'exa_calls' in error_str or 'exa_cost' in error_str or \
'video_calls' in error_str or 'video_cost' in error_str or \
'image_edit_calls' in error_str or 'image_edit_cost' in error_str or \
'audio_calls' in error_str or 'audio_cost' in error_str:
schema_utils._checked_usage_summaries_columns = False
schema_utils._checked_subscription_plan_columns = False
from services.subscription.schema_utils import ensure_usage_summaries_columns, ensure_subscription_plan_columns
ensure_usage_summaries_columns(db)
ensure_subscription_plan_columns(db)
elif 'actual_provider_name' in error_str:
schema_utils._checked_api_usage_logs_columns = False
from services.subscription.schema_utils import ensure_api_usage_logs_columns
ensure_api_usage_logs_columns(db)
db.expire_all()
return retry_func()
except Exception as retry_err:
logger.error(f"Schema fix and retry failed: {retry_err}")
raise HTTPException(status_code=500, detail=f"Database schema error: {str(error)}")
raise error