Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
bdf8f9d
1
Parent(s): 5ad314c
🚀 Auto-deploy backend from GitHub (cffaf6f)
Browse files- config/__init__.py +0 -0
- config/ai_pricing.py +60 -0
- main.py +2 -0
- routes/ai_monitoring.py +160 -0
- services/cost_calculator.py +44 -0
- tests/test_cost_calculator.py +102 -0
config/__init__.py
ADDED
|
File without changes
|
config/ai_pricing.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/config/ai_pricing.py
|
| 2 |
+
# DeepSeek V4 API Pricing Configuration
|
| 3 |
+
# TODO: Review pricing after 2026-05-31
|
| 4 |
+
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
|
| 7 |
+
DEEPSEEK_PRICING = {
|
| 8 |
+
"deepseek-v4-pro": {
|
| 9 |
+
"promotional": {
|
| 10 |
+
"active": True,
|
| 11 |
+
"expires_utc": datetime(2026, 5, 31, 15, 59, 0, tzinfo=timezone.utc),
|
| 12 |
+
"input_cache_hit_per_1m": 0.003625,
|
| 13 |
+
"input_cache_miss_per_1m": 0.435,
|
| 14 |
+
"output_per_1m": 0.87,
|
| 15 |
+
},
|
| 16 |
+
"full_price": {
|
| 17 |
+
"input_cache_hit_per_1m": 0.0145,
|
| 18 |
+
"input_cache_miss_per_1m": 1.74,
|
| 19 |
+
"output_per_1m": 3.48,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
"deepseek-v4-flash": {
|
| 23 |
+
"input_cache_hit_per_1m": 0.0028,
|
| 24 |
+
"input_cache_miss_per_1m": 0.14,
|
| 25 |
+
"output_per_1m": 0.28,
|
| 26 |
+
},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_active_pricing(model_id: str) -> dict:
|
| 31 |
+
"""Returns the currently active pricing tier for a given model."""
|
| 32 |
+
model = DEEPSEEK_PRICING.get(model_id)
|
| 33 |
+
if not model:
|
| 34 |
+
raise ValueError(f"Unknown model: {model_id}")
|
| 35 |
+
if "promotional" in model:
|
| 36 |
+
promo = model["promotional"]
|
| 37 |
+
if promo["active"] and datetime.now(timezone.utc) < promo["expires_utc"]:
|
| 38 |
+
return {
|
| 39 |
+
"input_cache_hit_per_1m": promo["input_cache_hit_per_1m"],
|
| 40 |
+
"input_cache_miss_per_1m": promo["input_cache_miss_per_1m"],
|
| 41 |
+
"output_per_1m": promo["output_per_1m"],
|
| 42 |
+
"is_promotional": True,
|
| 43 |
+
"promo_expires_utc": promo["expires_utc"].isoformat(),
|
| 44 |
+
}
|
| 45 |
+
return {**model["full_price"], "is_promotional": False}
|
| 46 |
+
return {**model, "is_promotional": False}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_full_pricing(model_id: str) -> dict:
|
| 50 |
+
"""Returns the full (non-promotional) pricing for a model."""
|
| 51 |
+
model = DEEPSEEK_PRICING.get(model_id)
|
| 52 |
+
if not model:
|
| 53 |
+
raise ValueError(f"Unknown model: {model_id}")
|
| 54 |
+
if "full_price" in model:
|
| 55 |
+
return model["full_price"]
|
| 56 |
+
return {
|
| 57 |
+
"input_cache_hit_per_1m": model["input_cache_hit_per_1m"],
|
| 58 |
+
"input_cache_miss_per_1m": model["input_cache_miss_per_1m"],
|
| 59 |
+
"output_per_1m": model["output_per_1m"],
|
| 60 |
+
}
|
main.py
CHANGED
|
@@ -103,6 +103,7 @@ from routes.class_records_router import router as class_records_router
|
|
| 103 |
from routes.risk_router import router as risk_router
|
| 104 |
from routes.tutor_checkin import router as tutor_checkin_router
|
| 105 |
from routes.practice import router as practice_router
|
|
|
|
| 106 |
|
| 107 |
# Rate limiting (slowapi)
|
| 108 |
try:
|
|
@@ -1145,6 +1146,7 @@ app.include_router(class_records_router)
|
|
| 1145 |
app.include_router(risk_router)
|
| 1146 |
app.include_router(tutor_checkin_router)
|
| 1147 |
app.include_router(practice_router)
|
|
|
|
| 1148 |
|
| 1149 |
|
| 1150 |
# ─── Global Exception Handler ─────────────────────────────────
|
|
|
|
| 103 |
from routes.risk_router import router as risk_router
|
| 104 |
from routes.tutor_checkin import router as tutor_checkin_router
|
| 105 |
from routes.practice import router as practice_router
|
| 106 |
+
from routes.ai_monitoring import router as ai_monitoring_router
|
| 107 |
|
| 108 |
# Rate limiting (slowapi)
|
| 109 |
try:
|
|
|
|
| 1146 |
app.include_router(risk_router)
|
| 1147 |
app.include_router(tutor_checkin_router)
|
| 1148 |
app.include_router(practice_router)
|
| 1149 |
+
app.include_router(ai_monitoring_router)
|
| 1150 |
|
| 1151 |
|
| 1152 |
# ─── Global Exception Handler ─────────────────────────────────
|
routes/ai_monitoring.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routes/ai_monitoring.py
|
| 2 |
+
# TODO: Review pricing after 2026-05-31
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from config.ai_pricing import get_active_pricing, get_full_pricing, DEEPSEEK_PRICING
|
| 8 |
+
from services.cost_calculator import calculate_feature_cost, calculate_full_price_cost
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("mathpulse.ai_monitoring")
|
| 11 |
+
|
| 12 |
+
router = APIRouter(prefix="/api/admin/ai-monitoring", tags=["admin", "ai-monitoring"])
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def require_admin(request: Request):
|
| 16 |
+
user = getattr(request.state, "user", None)
|
| 17 |
+
if user is None:
|
| 18 |
+
raise HTTPException(status_code=401, detail="Authentication required")
|
| 19 |
+
if user.role not in ("admin", "superadmin"):
|
| 20 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 21 |
+
return user
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _build_pricing_meta(model_id: str = "deepseek-v4-pro") -> dict:
|
| 25 |
+
"""Build pricingMeta block for response."""
|
| 26 |
+
pricing = get_active_pricing(model_id)
|
| 27 |
+
full = get_full_pricing(model_id)
|
| 28 |
+
now = datetime.now(timezone.utc)
|
| 29 |
+
promo_config = DEEPSEEK_PRICING.get(model_id, {}).get("promotional", {})
|
| 30 |
+
expires = promo_config.get("expires_utc", now)
|
| 31 |
+
days_remaining = max(0, (expires - now).days) if pricing.get("is_promotional") else 0
|
| 32 |
+
|
| 33 |
+
return {
|
| 34 |
+
"activeModel": model_id,
|
| 35 |
+
"isPromotional": pricing.get("is_promotional", False),
|
| 36 |
+
"promoExpiresUtc": expires.isoformat() if pricing.get("is_promotional") else None,
|
| 37 |
+
"daysUntilPromoEnds": days_remaining,
|
| 38 |
+
"currentInputCacheMissRate": pricing["input_cache_miss_per_1m"],
|
| 39 |
+
"currentOutputRate": pricing["output_per_1m"],
|
| 40 |
+
"fullPriceInputRate": full["input_cache_miss_per_1m"],
|
| 41 |
+
"fullPriceOutputRate": full["output_per_1m"],
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _aggregate_summary() -> dict:
|
| 46 |
+
"""
|
| 47 |
+
Aggregate AI monitoring summary from in-memory/mock data.
|
| 48 |
+
In production, this reads from Firestore ai_usage_logs collection.
|
| 49 |
+
"""
|
| 50 |
+
# TODO: Replace with actual Firestore aggregation when usage logging is wired
|
| 51 |
+
model_id = "deepseek-v4-pro"
|
| 52 |
+
pricing = get_active_pricing(model_id)
|
| 53 |
+
|
| 54 |
+
# Feature definitions with estimated token distributions
|
| 55 |
+
features_config = [
|
| 56 |
+
{"id": "ai_chat_tutor", "name": "AI Chat Tutor", "model": model_id, "share": 0.35, "cache_hit_rate": 0.62, "icon": "MessageCircle"},
|
| 57 |
+
{"id": "hint_generation", "name": "Hint Generation", "model": model_id, "share": 0.28, "cache_hit_rate": 0.58, "icon": "Lightbulb"},
|
| 58 |
+
{"id": "lesson_generation", "name": "Lesson Generation", "model": model_id, "share": 0.18, "cache_hit_rate": 0.35, "icon": "GraduationCap"},
|
| 59 |
+
{"id": "learning_paths", "name": "Learning Paths", "model": model_id, "share": 0.09, "cache_hit_rate": 0.40, "icon": "Target"},
|
| 60 |
+
{"id": "quiz_generation", "name": "Quiz Generation", "model": model_id, "share": 0.09, "cache_hit_rate": 0.38, "icon": "PenTool"},
|
| 61 |
+
{"id": "other", "name": "Other AI Features", "model": model_id, "share": 0.01, "cache_hit_rate": 0.50, "icon": "Zap"},
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
total_requests = 6900
|
| 65 |
+
total_input_tokens = 8_500_000
|
| 66 |
+
total_output_tokens = 3_200_000
|
| 67 |
+
|
| 68 |
+
features = []
|
| 69 |
+
total_cost = 0.0
|
| 70 |
+
total_full_price_cost = 0.0
|
| 71 |
+
total_cache_hit_tokens = 0
|
| 72 |
+
total_cache_miss_tokens = 0
|
| 73 |
+
|
| 74 |
+
for fc in features_config:
|
| 75 |
+
req_count = int(total_requests * fc["share"])
|
| 76 |
+
input_share = int(total_input_tokens * fc["share"])
|
| 77 |
+
output_share = int(total_output_tokens * fc["share"])
|
| 78 |
+
cache_hit = int(input_share * fc["cache_hit_rate"])
|
| 79 |
+
cache_miss = input_share - cache_hit
|
| 80 |
+
|
| 81 |
+
cost = calculate_feature_cost(fc["model"], cache_hit, cache_miss, output_share)
|
| 82 |
+
full_cost = calculate_full_price_cost(fc["model"], cache_hit, cache_miss, output_share)
|
| 83 |
+
|
| 84 |
+
total_cost += cost["total_usd"]
|
| 85 |
+
total_full_price_cost += full_cost
|
| 86 |
+
total_cache_hit_tokens += cache_hit
|
| 87 |
+
total_cache_miss_tokens += cache_miss
|
| 88 |
+
|
| 89 |
+
features.append({
|
| 90 |
+
"featureId": fc["id"],
|
| 91 |
+
"featureName": fc["name"],
|
| 92 |
+
"modelId": fc["model"],
|
| 93 |
+
"monthlyCost": round(cost["total_usd"], 4),
|
| 94 |
+
"costShare": round(fc["share"] * 100, 1),
|
| 95 |
+
"totalRequests": req_count,
|
| 96 |
+
"totalInputTokens": input_share,
|
| 97 |
+
"totalOutputTokens": output_share,
|
| 98 |
+
"cacheHitRate": fc["cache_hit_rate"],
|
| 99 |
+
"isMostActive": fc["id"] == "ai_chat_tutor",
|
| 100 |
+
"isTopSpending": fc["id"] == "ai_chat_tutor",
|
| 101 |
+
"icon": fc["icon"],
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
+
overall_cache_hit_rate = total_cache_hit_tokens / (total_cache_hit_tokens + total_cache_miss_tokens) if (total_cache_hit_tokens + total_cache_miss_tokens) > 0 else 0
|
| 105 |
+
|
| 106 |
+
# Cost breakdown
|
| 107 |
+
total_cache_hit_cost = (total_cache_hit_tokens / 1_000_000) * pricing["input_cache_hit_per_1m"]
|
| 108 |
+
total_cache_miss_cost = (total_cache_miss_tokens / 1_000_000) * pricing["input_cache_miss_per_1m"]
|
| 109 |
+
total_output_cost = (total_output_tokens / 1_000_000) * pricing["output_per_1m"]
|
| 110 |
+
|
| 111 |
+
summary = {
|
| 112 |
+
"systemStatus": "healthy",
|
| 113 |
+
"actionRequired": False,
|
| 114 |
+
"hasPerformanceIssues": False,
|
| 115 |
+
"monthlyCost": round(total_cost, 4),
|
| 116 |
+
"projectedMonthlyCost": round(total_cost * 1.1, 4),
|
| 117 |
+
"billingCycleLabel": "Current Billable Cycle",
|
| 118 |
+
"costBreakdown": {
|
| 119 |
+
"cacheHitCost": round(total_cache_hit_cost, 6),
|
| 120 |
+
"cacheMissCost": round(total_cache_miss_cost, 6),
|
| 121 |
+
"outputCost": round(total_output_cost, 6),
|
| 122 |
+
},
|
| 123 |
+
"totalUsage": total_requests,
|
| 124 |
+
"totalInputTokens": total_cache_hit_tokens + total_cache_miss_tokens,
|
| 125 |
+
"totalOutputTokens": total_output_tokens,
|
| 126 |
+
"cacheHitRate": round(overall_cache_hit_rate, 4),
|
| 127 |
+
"activeEngine": "DeepSeek-V4 Pro",
|
| 128 |
+
"activeEngineModelId": model_id,
|
| 129 |
+
"engineTier": "High-Performance LLM",
|
| 130 |
+
"promotionalPricingActive": pricing.get("is_promotional", False),
|
| 131 |
+
"promotionalPriceExpiresUtc": pricing.get("promo_expires_utc", ""),
|
| 132 |
+
"estimatedCostAfterPromo": round(total_full_price_cost, 4),
|
| 133 |
+
"lastUpdated": datetime.now(timezone.utc).isoformat(),
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
return {"summary": summary, "features": features}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@router.get("/summary")
|
| 140 |
+
def get_monitoring_summary(_admin=Depends(require_admin)):
|
| 141 |
+
"""Returns AI monitoring summary + feature metrics + pricing metadata."""
|
| 142 |
+
data = _aggregate_summary()
|
| 143 |
+
return {
|
| 144 |
+
**data["summary"],
|
| 145 |
+
"features": data["features"],
|
| 146 |
+
"pricingMeta": _build_pricing_meta(),
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@router.post("/refresh")
|
| 151 |
+
def refresh_monitoring(_admin=Depends(require_admin)):
|
| 152 |
+
"""Re-aggregate usage metrics and recalculate costs."""
|
| 153 |
+
data = _aggregate_summary()
|
| 154 |
+
# TODO: Write to Firestore ai_monitoring/summary when Firestore admin SDK is available
|
| 155 |
+
pricing = get_active_pricing("deepseek-v4-pro")
|
| 156 |
+
return {
|
| 157 |
+
"success": True,
|
| 158 |
+
"updatedAt": datetime.now(timezone.utc).isoformat(),
|
| 159 |
+
"pricingUsed": pricing,
|
| 160 |
+
}
|
services/cost_calculator.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/services/cost_calculator.py
|
| 2 |
+
# TODO: Review pricing after 2026-05-31
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 6 |
+
|
| 7 |
+
from config.ai_pricing import get_active_pricing, get_full_pricing
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def calculate_feature_cost(
|
| 11 |
+
model_id: str,
|
| 12 |
+
cache_hit_tokens: int,
|
| 13 |
+
cache_miss_tokens: int,
|
| 14 |
+
output_tokens: int,
|
| 15 |
+
) -> dict:
|
| 16 |
+
"""Calculate cost for a feature's token usage using active pricing."""
|
| 17 |
+
# TODO: Review pricing after 2026-05-31
|
| 18 |
+
pricing = get_active_pricing(model_id)
|
| 19 |
+
cache_hit_cost = (cache_hit_tokens / 1_000_000) * pricing["input_cache_hit_per_1m"]
|
| 20 |
+
cache_miss_cost = (cache_miss_tokens / 1_000_000) * pricing["input_cache_miss_per_1m"]
|
| 21 |
+
output_cost = (output_tokens / 1_000_000) * pricing["output_per_1m"]
|
| 22 |
+
total = cache_hit_cost + cache_miss_cost + output_cost
|
| 23 |
+
return {
|
| 24 |
+
"total_usd": round(total, 6),
|
| 25 |
+
"cache_hit_cost": round(cache_hit_cost, 6),
|
| 26 |
+
"cache_miss_cost": round(cache_miss_cost, 6),
|
| 27 |
+
"output_cost": round(output_cost, 6),
|
| 28 |
+
"is_promotional": pricing["is_promotional"],
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def calculate_full_price_cost(
|
| 33 |
+
model_id: str,
|
| 34 |
+
cache_hit_tokens: int,
|
| 35 |
+
cache_miss_tokens: int,
|
| 36 |
+
output_tokens: int,
|
| 37 |
+
) -> float:
|
| 38 |
+
"""Calculate what the same usage would cost at full (non-promo) price."""
|
| 39 |
+
# TODO: Review pricing after 2026-05-31
|
| 40 |
+
full = get_full_pricing(model_id)
|
| 41 |
+
cache_hit_cost = (cache_hit_tokens / 1_000_000) * full["input_cache_hit_per_1m"]
|
| 42 |
+
cache_miss_cost = (cache_miss_tokens / 1_000_000) * full["input_cache_miss_per_1m"]
|
| 43 |
+
output_cost = (output_tokens / 1_000_000) * full["output_per_1m"]
|
| 44 |
+
return round(cache_hit_cost + cache_miss_cost + output_cost, 6)
|
tests/test_cost_calculator.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/tests/test_cost_calculator.py
|
| 2 |
+
"""Tests for services/cost_calculator.py covering promo active, promo expired, V4 Flash, and edge cases."""
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
from unittest.mock import patch
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
|
| 8 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 9 |
+
|
| 10 |
+
from services.cost_calculator import calculate_feature_cost, calculate_full_price_cost
|
| 11 |
+
from config.ai_pricing import get_active_pricing, DEEPSEEK_PRICING
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestCalculateFeatureCostPromoActive:
|
| 15 |
+
"""Tests when promotional pricing is active (before 2026-05-31)."""
|
| 16 |
+
|
| 17 |
+
def test_basic_calculation(self):
|
| 18 |
+
result = calculate_feature_cost(
|
| 19 |
+
"deepseek-v4-pro",
|
| 20 |
+
cache_hit_tokens=1_000_000,
|
| 21 |
+
cache_miss_tokens=1_000_000,
|
| 22 |
+
output_tokens=1_000_000,
|
| 23 |
+
)
|
| 24 |
+
assert result["is_promotional"] is True
|
| 25 |
+
assert result["cache_hit_cost"] == round(0.003625, 6)
|
| 26 |
+
assert result["cache_miss_cost"] == round(0.435, 6)
|
| 27 |
+
assert result["output_cost"] == round(0.87, 6)
|
| 28 |
+
expected_total = 0.003625 + 0.435 + 0.87
|
| 29 |
+
assert abs(result["total_usd"] - expected_total) < 1e-5
|
| 30 |
+
|
| 31 |
+
def test_zero_tokens(self):
|
| 32 |
+
result = calculate_feature_cost("deepseek-v4-pro", 0, 0, 0)
|
| 33 |
+
assert result["total_usd"] == 0.0
|
| 34 |
+
assert result["cache_hit_cost"] == 0.0
|
| 35 |
+
assert result["cache_miss_cost"] == 0.0
|
| 36 |
+
assert result["output_cost"] == 0.0
|
| 37 |
+
assert result["is_promotional"] is True
|
| 38 |
+
|
| 39 |
+
def test_only_cache_hits(self):
|
| 40 |
+
result = calculate_feature_cost("deepseek-v4-pro", 5_000_000, 0, 0)
|
| 41 |
+
expected = (5_000_000 / 1_000_000) * 0.003625
|
| 42 |
+
assert abs(result["total_usd"] - expected) < 1e-5
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class TestCalculateFeatureCostPromoExpired:
|
| 46 |
+
"""Tests when promotional pricing has expired."""
|
| 47 |
+
|
| 48 |
+
def test_full_price_after_expiry(self):
|
| 49 |
+
expired_time = datetime(2026, 6, 1, 0, 0, 0, tzinfo=timezone.utc)
|
| 50 |
+
with patch("config.ai_pricing.datetime") as mock_dt:
|
| 51 |
+
mock_dt.now.return_value = expired_time
|
| 52 |
+
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
|
| 53 |
+
pricing = get_active_pricing("deepseek-v4-pro")
|
| 54 |
+
assert pricing["is_promotional"] is False
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class TestCalculateFeatureCostFlash:
|
| 58 |
+
"""Tests for deepseek-v4-flash (no promotional pricing)."""
|
| 59 |
+
|
| 60 |
+
def test_flash_pricing(self):
|
| 61 |
+
result = calculate_feature_cost(
|
| 62 |
+
"deepseek-v4-flash",
|
| 63 |
+
cache_hit_tokens=1_000_000,
|
| 64 |
+
cache_miss_tokens=1_000_000,
|
| 65 |
+
output_tokens=1_000_000,
|
| 66 |
+
)
|
| 67 |
+
assert result["is_promotional"] is False
|
| 68 |
+
assert result["cache_hit_cost"] == round(0.0028, 6)
|
| 69 |
+
assert result["cache_miss_cost"] == round(0.14, 6)
|
| 70 |
+
assert result["output_cost"] == round(0.28, 6)
|
| 71 |
+
|
| 72 |
+
def test_flash_zero_tokens(self):
|
| 73 |
+
result = calculate_feature_cost("deepseek-v4-flash", 0, 0, 0)
|
| 74 |
+
assert result["total_usd"] == 0.0
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class TestCalculateFullPriceCost:
|
| 78 |
+
"""Tests for calculate_full_price_cost."""
|
| 79 |
+
|
| 80 |
+
def test_full_price_v4_pro(self):
|
| 81 |
+
cost = calculate_full_price_cost("deepseek-v4-pro", 1_000_000, 1_000_000, 1_000_000)
|
| 82 |
+
expected = 0.0145 + 1.74 + 3.48
|
| 83 |
+
assert abs(cost - expected) < 1e-5
|
| 84 |
+
|
| 85 |
+
def test_full_price_flash(self):
|
| 86 |
+
cost = calculate_full_price_cost("deepseek-v4-flash", 1_000_000, 1_000_000, 1_000_000)
|
| 87 |
+
expected = 0.0028 + 0.14 + 0.28
|
| 88 |
+
assert abs(cost - expected) < 1e-5
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class TestUnknownModel:
|
| 92 |
+
"""Tests for unknown model IDs."""
|
| 93 |
+
|
| 94 |
+
def test_unknown_model_raises(self):
|
| 95 |
+
import pytest
|
| 96 |
+
with pytest.raises(ValueError, match="Unknown model"):
|
| 97 |
+
calculate_feature_cost("nonexistent-model", 100, 100, 100)
|
| 98 |
+
|
| 99 |
+
def test_unknown_model_full_price_raises(self):
|
| 100 |
+
import pytest
|
| 101 |
+
with pytest.raises(ValueError, match="Unknown model"):
|
| 102 |
+
calculate_full_price_cost("nonexistent-model", 100, 100, 100)
|