feat: 完善模型用量趋势与全局手账卡片并补齐阶段验收

This commit is contained in:
2026-09-05 20:40:36 +08:00
parent 02dd585a4e
commit 8d626ee16b
38 changed files with 733 additions and 86 deletions
+23 -4
View File
@@ -6,7 +6,7 @@ import logging
import math
from contextlib import closing
from contextvars import ContextVar
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from uuid import uuid4
from app.database.db import connect
@@ -107,8 +107,8 @@ class UsageAttempt:
logger.warning("Usage persistence failed; model response remains available")
def aggregate(start, end, provider_id=None, model=None, source=None):
query = "SELECT counters_json,completed,capability FROM model_usage WHERE started_at>=? AND started_at<?"
def aggregate(start, end, provider_id=None, model=None, source=None, timezone_offset=0):
query = "SELECT counters_json,completed,capability,started_at,source FROM model_usage WHERE started_at>=? AND started_at<?"
args = [start.astimezone(timezone.utc).isoformat(), end.astimezone(timezone.utc).isoformat()]
for column, value in (("provider_id", provider_id), ("model", model), ("source", source)):
if value:
@@ -117,6 +117,18 @@ def aggregate(start, end, provider_id=None, model=None, source=None):
with closing(connection()) as conn:
rows = conn.execute(query, args).fetchall()
options = conn.execute("SELECT DISTINCT provider_id,model,source FROM model_usage ORDER BY provider_id,model").fetchall()
# Calendar buckets use the caller's UTC offset; absent counters remain null.
zone = timezone(timedelta(minutes=timezone_offset))
first = start.astimezone(zone).date()
last = (end - timedelta(microseconds=1)).astimezone(zone).date()
days = (last - first).days + 1
step = max(1, (days + 89) // 90)
series = []
for offset in range(0, days, step):
date = first + timedelta(days=offset)
series.append({"date": date.isoformat(), "end_date": (first + timedelta(days=min(days-1, offset+step-1))).isoformat(),
"local": {"requests": 0, "totals": {key: None for key in METRICS}, "coverage": {key: 0 for key in METRICS}},
"api": {"requests": 0, "totals": {key: None for key in METRICS}, "coverage": {key: 0 for key in METRICS}}})
totals = {key: None for key in METRICS}
coverage = {key: 0 for key in METRICS}
hits, eligible_input, cache_requests = 0, 0, 0
@@ -125,6 +137,13 @@ def aggregate(start, end, provider_id=None, model=None, source=None):
if row[2] in {"transcription", "speaker_matching"}:
audio_requests += 1
counts = json.loads(row[0])
date = datetime.fromisoformat(row[3]).astimezone(zone).date()
bucket = series[(date - first).days // step][row[4]]
bucket['requests'] += 1
for key in METRICS:
if counts.get(key) is not None:
bucket['totals'][key] = (bucket['totals'][key] or 0) + counts[key]
bucket['coverage'][key] += 1
if counts.get("audio_seconds") is not None:
audio_covered += 1
audio_seconds = (audio_seconds or 0) + counts["audio_seconds"]
@@ -140,4 +159,4 @@ def aggregate(start, end, provider_id=None, model=None, source=None):
"complete_requests": sum(row[1] for row in rows), "cache_covered_requests": cache_requests,
"cache_hit_rate": hits / eligible_input if eligible_input else None,
"options": [dict(row) for row in options], "start": start, "end": end,
"scope": "application_observed_usage"}
"scope": "application_observed_usage", "series": series, "timezone_offset": timezone_offset}