feat: учёт token usage в checks с агрегатами и UI
Build and deploy / build-deploy (push) Has been cancelled
Build and deploy / build-deploy (push) Has been cancelled
- миграция checks: nullable input_tokens/output_tokens/total_tokens/usage_json (идемпотентно, старые строки остаются NULL; probe мигрирует сам — cron стартует раньше uvicorn) - probe.py и /api/check фиксируют usage ДО валидации контента: токены сохраняются при пустом/сломанном ответе; таймауты и ответы без usage = NULL, не нули - usage_json хранит сырой usage провайдера (OpenAI/Anthropic/camelCase форматы) - /api/checks и сегментная история несут токены; новый /api/token_stats: rolling 24h + all time, покрытие known/unknown, разбивка по провайдерам (провайдер из models с fallback на префикс model_id) - UI: статы «Токены 24h» и «Покрытие usage» в шапке, токены в тултипах сегментов и логе проверок - статика через __file__-относительный путь (работает и в /app/app, и локально) - тесты (30, реальные HTTP против мока OmniRoute): миграция/идемпотентность, NULL при missing/zero usage, usage при упавшем контенте, таймаут, агрегаты, оба пути записи (probe.run + /api/check)
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""Реальный smoke: uvicorn + мок OmniRoute, проверка живых HTTP-запросов."""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from conftest import OmniMock
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TMP = os.path.join(REPO, ".smoke")
|
||||
os.makedirs(TMP, exist_ok=True)
|
||||
|
||||
mock = OmniMock()
|
||||
mock.set_completions(usage={"prompt_tokens": 9, "completion_tokens": 4, "total_tokens": 13})
|
||||
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"DB_PATH": os.path.join(TMP, "arena.db"),
|
||||
"OMNIROUTE_URL": mock.url,
|
||||
"OMNIROUTE_API_KEY": "smoke",
|
||||
"ARENA_CACHE_TEXT": "/nonexistent/a.json",
|
||||
"ARENA_CACHE_AGENT": "/nonexistent/b.json",
|
||||
})
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[os.path.join(REPO, ".venv/bin/uvicorn"), "main:app", "--host", "127.0.0.1", "--port", "18700"],
|
||||
cwd=os.path.join(REPO, "app"), env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
)
|
||||
|
||||
# pip install uvicorn into venv if missing handled outside
|
||||
try:
|
||||
base = "http://127.0.0.1:18700"
|
||||
for _ in range(50):
|
||||
try:
|
||||
urllib.request.urlopen(f"{base}/api/providers", timeout=1)
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
|
||||
# 1. UI отдаётся (статика через __file__-путь)
|
||||
ui = urllib.request.urlopen(f"{base}/", timeout=5).read().decode()
|
||||
assert "Arena Dashboard" in ui and "s-tokens-24h" in ui and "loadTokenStats" in ui, "UI не содержит token-элементов"
|
||||
|
||||
# 2. Ручная проверка с usage
|
||||
chk = json.loads(urllib.request.urlopen(f"{base}/api/check/zai/test-model-a", timeout=10).read())
|
||||
assert chk["input_tokens"] == 9 and chk["total_tokens"] == 13, chk
|
||||
|
||||
# 3. Агрегат
|
||||
ts = json.loads(urllib.request.urlopen(f"{base}/api/token_stats", timeout=5).read())
|
||||
assert ts["window_24h"]["known"] >= 1 and ts["window_24h"]["total_tokens"] >= 13, ts
|
||||
print("SMOKE_OK", json.dumps({
|
||||
"check_usage": [chk["input_tokens"], chk["output_tokens"], chk["total_tokens"]],
|
||||
"token_stats_24h": ts["window_24h"],
|
||||
"providers": [(p["provider"], p["window_24h"]["total_tokens"]) for p in ts["providers"]],
|
||||
}, ensure_ascii=False))
|
||||
finally:
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
try:
|
||||
out, _ = proc.communicate(timeout=10)
|
||||
print("--- uvicorn log tail ---")
|
||||
print("\n".join(out.splitlines()[-8:]))
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
mock.close()
|
||||
Reference in New Issue
Block a user