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,77 @@
|
||||
"""Миграция схемы checks: идемпотентность и сохранение старых NULL."""
|
||||
import sqlite3
|
||||
|
||||
|
||||
def _cols(db_path):
|
||||
con = sqlite3.connect(db_path)
|
||||
cols = [r[1] for r in con.execute("PRAGMA table_info(checks)").fetchall()]
|
||||
con.close()
|
||||
return cols
|
||||
|
||||
|
||||
def _make_old_db(db_path, with_error_col=False):
|
||||
"""БД со старой схемой checks (до учёта usage) и живыми данными."""
|
||||
con = sqlite3.connect(db_path)
|
||||
con.execute("""CREATE TABLE checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
model_id TEXT NOT NULL,
|
||||
checked_at TEXT NOT NULL,
|
||||
ok INTEGER NOT NULL,
|
||||
latency_ms INTEGER)""")
|
||||
if with_error_col:
|
||||
con.execute("ALTER TABLE checks ADD COLUMN error TEXT")
|
||||
con.execute(
|
||||
"INSERT INTO checks (model_id, checked_at, ok, latency_ms, error) VALUES (?,?,?,?,?)"
|
||||
if with_error_col else
|
||||
"INSERT INTO checks (model_id, checked_at, ok, latency_ms) VALUES (?,?,?,?)",
|
||||
("zai/old-model", "2026-01-01T00:00:00+00:00", 1, 123, None)
|
||||
if with_error_col else ("zai/old-model", "2026-01-01T00:00:00+00:00", 1, 123),
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
|
||||
|
||||
def test_migration_old_schema_preserves_null(fresh_main):
|
||||
_make_old_db(fresh_main.DB_PATH, with_error_col=True)
|
||||
fresh_main.init_db()
|
||||
|
||||
cols = _cols(fresh_main.DB_PATH)
|
||||
for c in ("input_tokens", "output_tokens", "total_tokens", "usage_json"):
|
||||
assert c in cols, f"колонка {c} не добавлена"
|
||||
|
||||
con = sqlite3.connect(fresh_main.DB_PATH)
|
||||
con.row_factory = sqlite3.Row
|
||||
row = con.execute("SELECT * FROM checks WHERE model_id='zai/old-model'").fetchone()
|
||||
con.close()
|
||||
assert row["ok"] == 1 and row["latency_ms"] == 123
|
||||
# старые данные: неизвестное потребление = NULL, не нули
|
||||
assert row["input_tokens"] is None
|
||||
assert row["output_tokens"] is None
|
||||
assert row["total_tokens"] is None
|
||||
assert row["usage_json"] is None
|
||||
|
||||
|
||||
def test_migration_from_ancient_schema_without_error(fresh_main):
|
||||
_make_old_db(fresh_main.DB_PATH, with_error_col=False)
|
||||
fresh_main.init_db() # не должен падать: добавит и error, и usage-колонки
|
||||
cols = _cols(fresh_main.DB_PATH)
|
||||
assert "error" in cols
|
||||
assert "usage_json" in cols
|
||||
|
||||
|
||||
def test_migration_idempotent(fresh_main):
|
||||
_make_old_db(fresh_main.DB_PATH, with_error_col=True)
|
||||
fresh_main.init_db()
|
||||
fresh_main.init_db() # повторный запуск — no-op без ошибок
|
||||
|
||||
con = sqlite3.connect(fresh_main.DB_PATH)
|
||||
n = con.execute("SELECT COUNT(*) FROM checks").fetchone()[0]
|
||||
con.close()
|
||||
assert n == 1 # данные не задвоились и не потерялись
|
||||
|
||||
|
||||
def test_fresh_db_has_usage_columns(fresh_main):
|
||||
fresh_main.init_db()
|
||||
cols = _cols(fresh_main.DB_PATH)
|
||||
for c in ("input_tokens", "output_tokens", "total_tokens", "usage_json"):
|
||||
assert c in cols
|
||||
Reference in New Issue
Block a user