5c8c13891f
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)
78 lines
3.0 KiB
Python
78 lines
3.0 KiB
Python
"""Миграция схемы 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
|