story writer backend migration complete, Blog writer SEO and story writer backend migration complete, Blog writer SEO and story writer frontend migration complete
This commit is contained in:
231
backend/services/subscription/log_wrapping_service.py
Normal file
231
backend/services/subscription/log_wrapping_service.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Log Wrapping Service
|
||||
Intelligently wraps API usage logs when they exceed 5000 records.
|
||||
Aggregates old logs into cumulative records while preserving historical data.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc
|
||||
from loguru import logger
|
||||
|
||||
from models.subscription_models import APIUsageLog, APIProvider
|
||||
|
||||
|
||||
class LogWrappingService:
|
||||
"""Service for wrapping and aggregating API usage logs."""
|
||||
|
||||
MAX_LOGS_PER_USER = 5000
|
||||
AGGREGATION_THRESHOLD_DAYS = 30 # Aggregate logs older than 30 days
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def check_and_wrap_logs(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if user has exceeded log limit and wrap if necessary.
|
||||
|
||||
Returns:
|
||||
Dict with wrapping status and statistics
|
||||
"""
|
||||
try:
|
||||
# Count total logs for user
|
||||
total_count = self.db.query(func.count(APIUsageLog.id)).filter(
|
||||
APIUsageLog.user_id == user_id
|
||||
).scalar() or 0
|
||||
|
||||
if total_count <= self.MAX_LOGS_PER_USER:
|
||||
return {
|
||||
'wrapped': False,
|
||||
'total_logs': total_count,
|
||||
'max_logs': self.MAX_LOGS_PER_USER,
|
||||
'message': f'Log count ({total_count}) is within limit ({self.MAX_LOGS_PER_USER})'
|
||||
}
|
||||
|
||||
# Need to wrap logs - aggregate old logs
|
||||
logger.info(f"[LogWrapping] User {user_id} has {total_count} logs, exceeding limit of {self.MAX_LOGS_PER_USER}. Starting wrap...")
|
||||
|
||||
wrap_result = self._wrap_old_logs(user_id, total_count)
|
||||
|
||||
return {
|
||||
'wrapped': True,
|
||||
'total_logs_before': total_count,
|
||||
'total_logs_after': wrap_result['logs_remaining'],
|
||||
'aggregated_logs': wrap_result['aggregated_count'],
|
||||
'aggregated_periods': wrap_result['periods'],
|
||||
'message': f'Wrapped {wrap_result["aggregated_count"]} logs into {len(wrap_result["periods"])} aggregated records'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[LogWrapping] Error checking/wrapping logs for user {user_id}: {e}", exc_info=True)
|
||||
return {
|
||||
'wrapped': False,
|
||||
'error': str(e),
|
||||
'message': f'Error wrapping logs: {str(e)}'
|
||||
}
|
||||
|
||||
def _wrap_old_logs(self, user_id: str, total_count: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Aggregate old logs into cumulative records.
|
||||
|
||||
Strategy:
|
||||
1. Keep most recent 4000 logs (detailed)
|
||||
2. Aggregate logs older than 30 days or oldest logs beyond 4000
|
||||
3. Create aggregated records grouped by provider and billing period
|
||||
4. Delete individual logs that were aggregated
|
||||
"""
|
||||
try:
|
||||
# Calculate how many logs to keep (4000 detailed, rest aggregated)
|
||||
logs_to_keep = 4000
|
||||
logs_to_aggregate = total_count - logs_to_keep
|
||||
|
||||
# Get cutoff date (30 days ago)
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=self.AGGREGATION_THRESHOLD_DAYS)
|
||||
|
||||
# Get logs to aggregate: oldest logs beyond the keep limit
|
||||
# Order by timestamp ascending to get oldest first
|
||||
# We'll keep the most recent logs_to_keep logs, aggregate the rest
|
||||
logs_to_process = self.db.query(APIUsageLog).filter(
|
||||
APIUsageLog.user_id == user_id
|
||||
).order_by(APIUsageLog.timestamp.asc()).limit(logs_to_aggregate).all()
|
||||
|
||||
if not logs_to_process:
|
||||
return {
|
||||
'aggregated_count': 0,
|
||||
'logs_remaining': total_count,
|
||||
'periods': []
|
||||
}
|
||||
|
||||
# Group logs by provider and billing period for aggregation
|
||||
aggregated_data: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
for log in logs_to_process:
|
||||
# Use provider value as key (e.g., "mistral" for huggingface)
|
||||
provider_key = log.provider.value
|
||||
# Special handling: if provider is MISTRAL but we want to show as huggingface
|
||||
if provider_key == "mistral":
|
||||
# Check if this is actually huggingface by looking at model or endpoint
|
||||
# For now, we'll use "mistral" as the key but store actual provider name
|
||||
provider_display = "huggingface" if "huggingface" in (log.model_used or "").lower() else "mistral"
|
||||
else:
|
||||
provider_display = provider_key
|
||||
|
||||
period_key = f"{provider_display}_{log.billing_period}"
|
||||
|
||||
if period_key not in aggregated_data:
|
||||
aggregated_data[period_key] = {
|
||||
'provider': log.provider,
|
||||
'billing_period': log.billing_period,
|
||||
'count': 0,
|
||||
'total_tokens_input': 0,
|
||||
'total_tokens_output': 0,
|
||||
'total_tokens': 0,
|
||||
'total_cost_input': 0.0,
|
||||
'total_cost_output': 0.0,
|
||||
'total_cost': 0.0,
|
||||
'total_response_time': 0.0,
|
||||
'success_count': 0,
|
||||
'failed_count': 0,
|
||||
'oldest_timestamp': log.timestamp,
|
||||
'newest_timestamp': log.timestamp,
|
||||
'log_ids': []
|
||||
}
|
||||
|
||||
agg = aggregated_data[period_key]
|
||||
agg['count'] += 1
|
||||
agg['total_tokens_input'] += log.tokens_input or 0
|
||||
agg['total_tokens_output'] += log.tokens_output or 0
|
||||
agg['total_tokens'] += log.tokens_total or 0
|
||||
agg['total_cost_input'] += float(log.cost_input or 0.0)
|
||||
agg['total_cost_output'] += float(log.cost_output or 0.0)
|
||||
agg['total_cost'] += float(log.cost_total or 0.0)
|
||||
agg['total_response_time'] += float(log.response_time or 0.0)
|
||||
|
||||
if 200 <= log.status_code < 300:
|
||||
agg['success_count'] += 1
|
||||
else:
|
||||
agg['failed_count'] += 1
|
||||
|
||||
if log.timestamp:
|
||||
if log.timestamp < agg['oldest_timestamp']:
|
||||
agg['oldest_timestamp'] = log.timestamp
|
||||
if log.timestamp > agg['newest_timestamp']:
|
||||
agg['newest_timestamp'] = log.timestamp
|
||||
|
||||
agg['log_ids'].append(log.id)
|
||||
|
||||
# Create aggregated log entries
|
||||
aggregated_count = 0
|
||||
periods_created = []
|
||||
|
||||
for period_key, agg_data in aggregated_data.items():
|
||||
# Calculate averages
|
||||
count = agg_data['count']
|
||||
avg_response_time = agg_data['total_response_time'] / count if count > 0 else 0.0
|
||||
|
||||
# Create aggregated log entry
|
||||
aggregated_log = APIUsageLog(
|
||||
user_id=user_id,
|
||||
provider=agg_data['provider'],
|
||||
endpoint='[AGGREGATED]',
|
||||
method='AGGREGATED',
|
||||
model_used=f"[{count} calls aggregated]",
|
||||
tokens_input=agg_data['total_tokens_input'],
|
||||
tokens_output=agg_data['total_tokens_output'],
|
||||
tokens_total=agg_data['total_tokens'],
|
||||
cost_input=agg_data['total_cost_input'],
|
||||
cost_output=agg_data['total_cost_output'],
|
||||
cost_total=agg_data['total_cost'],
|
||||
response_time=avg_response_time,
|
||||
status_code=200 if agg_data['success_count'] > agg_data['failed_count'] else 500,
|
||||
error_message=f"Aggregated {count} calls: {agg_data['success_count']} success, {agg_data['failed_count']} failed",
|
||||
retry_count=0,
|
||||
timestamp=agg_data['oldest_timestamp'], # Use oldest timestamp
|
||||
billing_period=agg_data['billing_period']
|
||||
)
|
||||
|
||||
self.db.add(aggregated_log)
|
||||
periods_created.append({
|
||||
'provider': agg_data['provider'].value,
|
||||
'billing_period': agg_data['billing_period'],
|
||||
'count': count,
|
||||
'period_start': agg_data['oldest_timestamp'].isoformat() if agg_data['oldest_timestamp'] else None,
|
||||
'period_end': agg_data['newest_timestamp'].isoformat() if agg_data['newest_timestamp'] else None
|
||||
})
|
||||
|
||||
aggregated_count += count
|
||||
|
||||
# Delete individual logs that were aggregated
|
||||
log_ids_to_delete = []
|
||||
for agg_data in aggregated_data.values():
|
||||
log_ids_to_delete.extend(agg_data['log_ids'])
|
||||
|
||||
if log_ids_to_delete:
|
||||
self.db.query(APIUsageLog).filter(
|
||||
APIUsageLog.id.in_(log_ids_to_delete)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# Get remaining log count
|
||||
remaining_count = self.db.query(func.count(APIUsageLog.id)).filter(
|
||||
APIUsageLog.user_id == user_id
|
||||
).scalar() or 0
|
||||
|
||||
logger.info(
|
||||
f"[LogWrapping] Wrapped {aggregated_count} logs into {len(periods_created)} aggregated records. "
|
||||
f"Remaining logs: {remaining_count}"
|
||||
)
|
||||
|
||||
return {
|
||||
'aggregated_count': aggregated_count,
|
||||
'logs_remaining': remaining_count,
|
||||
'periods': periods_created
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"[LogWrapping] Error wrapping logs: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from loguru import logger
|
||||
import os
|
||||
|
||||
from models.subscription_models import (
|
||||
APIProviderPricing, SubscriptionPlan, UserSubscription,
|
||||
@@ -227,6 +228,36 @@ class PricingService:
|
||||
}
|
||||
]
|
||||
|
||||
# HuggingFace/Mistral Pricing (for GPT-OSS-120B via Groq)
|
||||
# Default pricing from environment variables or fallback to estimated values
|
||||
# Based on Groq pricing: ~$1 per 1M input tokens, ~$3 per 1M output tokens
|
||||
hf_input_cost = float(os.getenv('HUGGINGFACE_INPUT_TOKEN_COST', '0.000001')) # $1 per 1M tokens default
|
||||
hf_output_cost = float(os.getenv('HUGGINGFACE_OUTPUT_TOKEN_COST', '0.000003')) # $3 per 1M tokens default
|
||||
|
||||
mistral_pricing = [
|
||||
{
|
||||
"provider": APIProvider.MISTRAL,
|
||||
"model_name": "openai/gpt-oss-120b:groq",
|
||||
"cost_per_input_token": hf_input_cost,
|
||||
"cost_per_output_token": hf_output_cost,
|
||||
"description": f"GPT-OSS-120B via HuggingFace/Groq (configurable via HUGGINGFACE_INPUT_TOKEN_COST and HUGGINGFACE_OUTPUT_TOKEN_COST env vars)"
|
||||
},
|
||||
{
|
||||
"provider": APIProvider.MISTRAL,
|
||||
"model_name": "gpt-oss-120b",
|
||||
"cost_per_input_token": hf_input_cost,
|
||||
"cost_per_output_token": hf_output_cost,
|
||||
"description": f"GPT-OSS-120B via HuggingFace/Groq (configurable via HUGGINGFACE_INPUT_TOKEN_COST and HUGGINGFACE_OUTPUT_TOKEN_COST env vars)"
|
||||
},
|
||||
{
|
||||
"provider": APIProvider.MISTRAL,
|
||||
"model_name": "default",
|
||||
"cost_per_input_token": hf_input_cost,
|
||||
"cost_per_output_token": hf_output_cost,
|
||||
"description": f"HuggingFace default model pricing (configurable via HUGGINGFACE_INPUT_TOKEN_COST and HUGGINGFACE_OUTPUT_TOKEN_COST env vars)"
|
||||
}
|
||||
]
|
||||
|
||||
# Search API Pricing (estimated)
|
||||
search_pricing = [
|
||||
{
|
||||
@@ -268,21 +299,31 @@ class PricingService:
|
||||
]
|
||||
|
||||
# Combine all pricing data
|
||||
all_pricing = gemini_pricing + openai_pricing + anthropic_pricing + search_pricing
|
||||
all_pricing = gemini_pricing + openai_pricing + anthropic_pricing + mistral_pricing + search_pricing
|
||||
|
||||
# Insert pricing data
|
||||
# Insert or update pricing data
|
||||
for pricing_data in all_pricing:
|
||||
existing = self.db.query(APIProviderPricing).filter(
|
||||
APIProviderPricing.provider == pricing_data["provider"],
|
||||
APIProviderPricing.model_name == pricing_data["model_name"]
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
if existing:
|
||||
# Update existing pricing (especially for HuggingFace if env vars changed)
|
||||
if pricing_data["provider"] == APIProvider.MISTRAL:
|
||||
# Update HuggingFace pricing from env vars
|
||||
existing.cost_per_input_token = pricing_data["cost_per_input_token"]
|
||||
existing.cost_per_output_token = pricing_data["cost_per_output_token"]
|
||||
existing.description = pricing_data["description"]
|
||||
existing.updated_at = datetime.utcnow()
|
||||
logger.debug(f"Updated pricing for {pricing_data['provider'].value}:{pricing_data['model_name']}")
|
||||
else:
|
||||
pricing = APIProviderPricing(**pricing_data)
|
||||
self.db.add(pricing)
|
||||
logger.debug(f"Added new pricing for {pricing_data['provider'].value}:{pricing_data['model_name']}")
|
||||
|
||||
self.db.commit()
|
||||
logger.debug("Default API pricing initialized")
|
||||
logger.info("Default API pricing initialized/updated. HuggingFace pricing loaded from env vars if available.")
|
||||
|
||||
def initialize_default_plans(self):
|
||||
"""Initialize default subscription plans."""
|
||||
@@ -395,31 +436,82 @@ class PricingService:
|
||||
def calculate_api_cost(self, provider: APIProvider, model_name: str,
|
||||
tokens_input: int = 0, tokens_output: int = 0,
|
||||
request_count: int = 1, **kwargs) -> Dict[str, float]:
|
||||
"""Calculate cost for an API call."""
|
||||
"""Calculate cost for an API call.
|
||||
|
||||
Args:
|
||||
provider: APIProvider enum (e.g., APIProvider.MISTRAL for HuggingFace)
|
||||
model_name: Model name (e.g., "openai/gpt-oss-120b:groq")
|
||||
tokens_input: Number of input tokens
|
||||
tokens_output: Number of output tokens
|
||||
request_count: Number of requests (default: 1)
|
||||
**kwargs: Additional parameters (search_count, image_count, page_count, etc.)
|
||||
|
||||
Returns:
|
||||
Dict with cost_input, cost_output, and cost_total
|
||||
"""
|
||||
|
||||
# Get pricing for the provider and model
|
||||
# Try exact match first
|
||||
pricing = self.db.query(APIProviderPricing).filter(
|
||||
APIProviderPricing.provider == provider,
|
||||
APIProviderPricing.model_name == model_name,
|
||||
APIProviderPricing.is_active == True
|
||||
).first()
|
||||
|
||||
# If not found, try "default" model name for the provider
|
||||
if not pricing:
|
||||
logger.warning(f"No pricing found for {provider.value}:{model_name}, using default estimates")
|
||||
# Use default estimates
|
||||
cost_input = tokens_input * 0.000001 # $1 per 1M tokens default
|
||||
cost_output = tokens_output * 0.000001
|
||||
cost_total = (cost_input + cost_output) * request_count
|
||||
pricing = self.db.query(APIProviderPricing).filter(
|
||||
APIProviderPricing.provider == provider,
|
||||
APIProviderPricing.model_name == "default",
|
||||
APIProviderPricing.is_active == True
|
||||
).first()
|
||||
|
||||
# If still not found, check for HuggingFace models (provider is MISTRAL)
|
||||
# Try alternative model name variations
|
||||
if not pricing and provider == APIProvider.MISTRAL:
|
||||
# Try with "gpt-oss-120b" (without full path) if model contains it
|
||||
if "gpt-oss-120b" in model_name.lower():
|
||||
pricing = self.db.query(APIProviderPricing).filter(
|
||||
APIProviderPricing.provider == provider,
|
||||
APIProviderPricing.model_name == "gpt-oss-120b",
|
||||
APIProviderPricing.is_active == True
|
||||
).first()
|
||||
|
||||
# Also try with full model path
|
||||
if not pricing:
|
||||
pricing = self.db.query(APIProviderPricing).filter(
|
||||
APIProviderPricing.provider == provider,
|
||||
APIProviderPricing.model_name == "openai/gpt-oss-120b:groq",
|
||||
APIProviderPricing.is_active == True
|
||||
).first()
|
||||
|
||||
if not pricing:
|
||||
# Check if we should use env vars for HuggingFace/Mistral
|
||||
if provider == APIProvider.MISTRAL:
|
||||
# Use environment variables for HuggingFace pricing if available
|
||||
hf_input_cost = float(os.getenv('HUGGINGFACE_INPUT_TOKEN_COST', '0.000001'))
|
||||
hf_output_cost = float(os.getenv('HUGGINGFACE_OUTPUT_TOKEN_COST', '0.000003'))
|
||||
logger.info(f"Using HuggingFace pricing from env vars: input={hf_input_cost}, output={hf_output_cost} for model {model_name}")
|
||||
cost_input = tokens_input * hf_input_cost
|
||||
cost_output = tokens_output * hf_output_cost
|
||||
cost_total = cost_input + cost_output
|
||||
else:
|
||||
logger.warning(f"No pricing found for {provider.value}:{model_name}, using default estimates")
|
||||
# Use default estimates
|
||||
cost_input = tokens_input * 0.000001 # $1 per 1M tokens default
|
||||
cost_output = tokens_output * 0.000001
|
||||
cost_total = cost_input + cost_output
|
||||
else:
|
||||
# Calculate based on actual pricing
|
||||
cost_input = tokens_input * pricing.cost_per_input_token
|
||||
cost_output = tokens_output * pricing.cost_per_output_token
|
||||
cost_request = request_count * pricing.cost_per_request
|
||||
# Calculate based on actual pricing from database
|
||||
logger.debug(f"Using pricing from DB for {provider.value}:{model_name} - input: {pricing.cost_per_input_token}, output: {pricing.cost_per_output_token}")
|
||||
cost_input = tokens_input * (pricing.cost_per_input_token or 0.0)
|
||||
cost_output = tokens_output * (pricing.cost_per_output_token or 0.0)
|
||||
cost_request = request_count * (pricing.cost_per_request or 0.0)
|
||||
|
||||
# Handle special cases for non-LLM APIs
|
||||
cost_search = kwargs.get('search_count', 0) * pricing.cost_per_search
|
||||
cost_image = kwargs.get('image_count', 0) * pricing.cost_per_image
|
||||
cost_page = kwargs.get('page_count', 0) * pricing.cost_per_page
|
||||
cost_search = kwargs.get('search_count', 0) * (pricing.cost_per_search or 0.0)
|
||||
cost_image = kwargs.get('image_count', 0) * (pricing.cost_per_image or 0.0)
|
||||
cost_page = kwargs.get('page_count', 0) * (pricing.cost_per_page or 0.0)
|
||||
|
||||
cost_total = cost_input + cost_output + cost_request + cost_search + cost_image + cost_page
|
||||
|
||||
|
||||
@@ -42,10 +42,19 @@ class UsageTrackingService:
|
||||
default_models = {
|
||||
"gemini": "gemini-2.5-flash", # Use Flash as default (cost-effective)
|
||||
"openai": "gpt-4o-mini", # Use Mini as default (cost-effective)
|
||||
"anthropic": "claude-3.5-sonnet" # Use Sonnet as default
|
||||
"anthropic": "claude-3.5-sonnet", # Use Sonnet as default
|
||||
"mistral": "openai/gpt-oss-120b:groq" # HuggingFace default model
|
||||
}
|
||||
|
||||
model_name = model_used or default_models.get(provider.value, f"{provider.value}-default")
|
||||
# For HuggingFace (stored as MISTRAL), use the actual model name or default
|
||||
if provider == APIProvider.MISTRAL:
|
||||
# HuggingFace models - try to match the actual model name from model_used
|
||||
if model_used:
|
||||
model_name = model_used
|
||||
else:
|
||||
model_name = default_models.get("mistral", "openai/gpt-oss-120b:groq")
|
||||
else:
|
||||
model_name = model_used or default_models.get(provider.value, f"{provider.value}-default")
|
||||
|
||||
cost_data = self.pricing_service.calculate_api_cost(
|
||||
provider=provider,
|
||||
@@ -344,46 +353,106 @@ class UsageTrackingService:
|
||||
'limits': limits,
|
||||
'provider_breakdown': provider_breakdown,
|
||||
'alerts': [],
|
||||
'usage_percentages': usage_percentages
|
||||
'usage_percentages': {}
|
||||
}
|
||||
|
||||
# Calculate usage percentages
|
||||
# Provider breakdown - calculate costs first, then use for percentages
|
||||
# Only include Gemini and HuggingFace (HuggingFace is stored under MISTRAL enum)
|
||||
provider_breakdown = {}
|
||||
|
||||
# Gemini
|
||||
gemini_calls = getattr(summary, "gemini_calls", 0) or 0
|
||||
gemini_tokens = getattr(summary, "gemini_tokens", 0) or 0
|
||||
gemini_cost = getattr(summary, "gemini_cost", 0.0) or 0.0
|
||||
|
||||
# If gemini cost is 0 but there are calls, calculate from usage logs
|
||||
if gemini_calls > 0 and gemini_cost == 0.0:
|
||||
gemini_logs = self.db.query(APIUsageLog).filter(
|
||||
APIUsageLog.user_id == user_id,
|
||||
APIUsageLog.provider == APIProvider.GEMINI,
|
||||
APIUsageLog.billing_period == billing_period
|
||||
).all()
|
||||
if gemini_logs:
|
||||
gemini_cost = sum(float(log.cost_total or 0.0) for log in gemini_logs)
|
||||
logger.info(f"[UsageStats] Calculated gemini cost from {len(gemini_logs)} logs: ${gemini_cost:.6f}")
|
||||
|
||||
provider_breakdown['gemini'] = {
|
||||
'calls': gemini_calls,
|
||||
'tokens': gemini_tokens,
|
||||
'cost': gemini_cost
|
||||
}
|
||||
|
||||
# HuggingFace (stored as MISTRAL in database)
|
||||
mistral_calls = getattr(summary, "mistral_calls", 0) or 0
|
||||
mistral_tokens = getattr(summary, "mistral_tokens", 0) or 0
|
||||
mistral_cost = getattr(summary, "mistral_cost", 0.0) or 0.0
|
||||
|
||||
# If mistral (HuggingFace) cost is 0 but there are calls, calculate from usage logs
|
||||
if mistral_calls > 0 and mistral_cost == 0.0:
|
||||
mistral_logs = self.db.query(APIUsageLog).filter(
|
||||
APIUsageLog.user_id == user_id,
|
||||
APIUsageLog.provider == APIProvider.MISTRAL,
|
||||
APIUsageLog.billing_period == billing_period
|
||||
).all()
|
||||
if mistral_logs:
|
||||
mistral_cost = sum(float(log.cost_total or 0.0) for log in mistral_logs)
|
||||
logger.info(f"[UsageStats] Calculated mistral (HuggingFace) cost from {len(mistral_logs)} logs: ${mistral_cost:.6f}")
|
||||
|
||||
provider_breakdown['huggingface'] = {
|
||||
'calls': mistral_calls,
|
||||
'tokens': mistral_tokens,
|
||||
'cost': mistral_cost
|
||||
}
|
||||
|
||||
# Calculate total cost from provider breakdown if summary total_cost is 0
|
||||
calculated_total_cost = gemini_cost + mistral_cost
|
||||
summary_total_cost = summary.total_cost or 0.0
|
||||
# Use calculated cost if summary cost is 0, otherwise use summary cost (it's more accurate)
|
||||
final_total_cost = summary_total_cost if summary_total_cost > 0 else calculated_total_cost
|
||||
|
||||
# If we calculated costs from logs, update the summary for future requests
|
||||
if calculated_total_cost > 0 and summary_total_cost == 0.0:
|
||||
logger.info(f"[UsageStats] Updating summary costs: total_cost={final_total_cost:.6f}, gemini_cost={gemini_cost:.6f}, mistral_cost={mistral_cost:.6f}")
|
||||
summary.total_cost = final_total_cost
|
||||
summary.gemini_cost = gemini_cost
|
||||
summary.mistral_cost = mistral_cost
|
||||
try:
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"[UsageStats] Error updating summary costs: {e}")
|
||||
self.db.rollback()
|
||||
|
||||
# Calculate usage percentages - only for Gemini and HuggingFace
|
||||
# Use the calculated costs for accurate percentages
|
||||
usage_percentages = {}
|
||||
if limits:
|
||||
for provider in APIProvider:
|
||||
provider_name = provider.value
|
||||
current_calls = getattr(summary, f"{provider_name}_calls", 0) or 0
|
||||
call_limit = limits['limits'].get(f"{provider_name}_calls", 0) or 0
|
||||
|
||||
if call_limit > 0:
|
||||
usage_percentages[f"{provider_name}_calls"] = (current_calls / call_limit) * 100
|
||||
else:
|
||||
usage_percentages[f"{provider_name}_calls"] = 0
|
||||
# Gemini
|
||||
gemini_call_limit = limits['limits'].get("gemini_calls", 0) or 0
|
||||
if gemini_call_limit > 0:
|
||||
usage_percentages['gemini_calls'] = (gemini_calls / gemini_call_limit) * 100
|
||||
else:
|
||||
usage_percentages['gemini_calls'] = 0
|
||||
|
||||
# Cost usage percentage
|
||||
# HuggingFace (stored as mistral in database)
|
||||
mistral_call_limit = limits['limits'].get("mistral_calls", 0) or 0
|
||||
if mistral_call_limit > 0:
|
||||
usage_percentages['mistral_calls'] = (mistral_calls / mistral_call_limit) * 100
|
||||
else:
|
||||
usage_percentages['mistral_calls'] = 0
|
||||
|
||||
# Cost usage percentage - use final_total_cost (calculated from logs if needed)
|
||||
cost_limit = limits['limits'].get('monthly_cost', 0) or 0
|
||||
total_cost = summary.total_cost or 0
|
||||
if cost_limit > 0:
|
||||
usage_percentages['cost'] = (total_cost / cost_limit) * 100
|
||||
usage_percentages['cost'] = (final_total_cost / cost_limit) * 100
|
||||
else:
|
||||
usage_percentages['cost'] = 0
|
||||
|
||||
# Provider breakdown
|
||||
provider_breakdown = {}
|
||||
for provider in APIProvider:
|
||||
provider_name = provider.value
|
||||
provider_breakdown[provider_name] = {
|
||||
'calls': getattr(summary, f"{provider_name}_calls", 0) or 0,
|
||||
'tokens': getattr(summary, f"{provider_name}_tokens", 0) or 0,
|
||||
'cost': getattr(summary, f"{provider_name}_cost", 0.0) or 0.0
|
||||
}
|
||||
|
||||
return {
|
||||
'billing_period': billing_period,
|
||||
'usage_status': summary.usage_status.value if hasattr(summary.usage_status, 'value') else str(summary.usage_status),
|
||||
'total_calls': summary.total_calls or 0,
|
||||
'total_tokens': summary.total_tokens or 0,
|
||||
'total_cost': summary.total_cost or 0.0,
|
||||
'total_cost': final_total_cost,
|
||||
'avg_response_time': summary.avg_response_time or 0.0,
|
||||
'error_rate': summary.error_rate or 0.0,
|
||||
'limits': limits,
|
||||
|
||||
Reference in New Issue
Block a user