github-actions[bot] commited on
Commit
bdf8f9d
·
1 Parent(s): 5ad314c

🚀 Auto-deploy backend from GitHub (cffaf6f)

Browse files
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)