"""Ручной путь (main.py /api/check) + API-агрегаты token usage.""" import json import sqlite3 from datetime import datetime, timedelta, timezone def _con(db_path): con = sqlite3.connect(db_path) con.row_factory = sqlite3.Row return con def _insert_check(db_path, model_id, at, ok=1, inp=None, out=None, tot=None, ujson=None): con = _con(db_path) con.execute( "INSERT INTO checks (model_id, checked_at, ok, latency_ms, error, " "input_tokens, output_tokens, total_tokens, usage_json) VALUES (?,?,?,?,?,?,?,?,?)", (model_id, at, ok, 50, None, inp, out, tot, ujson), ) con.commit() con.close() def _iso(**kw): return (datetime.now(timezone.utc) + timedelta(**kw)).isoformat() # ─── ручная проверка /api/check ─────────────────────────────────────────────── def test_api_check_captures_usage(core, omni_server): omni_server.set_completions(usage={"prompt_tokens": 12, "completion_tokens": 3, "total_tokens": 15}) r = core.client.get("/api/check/zai/test-model-a") assert r.status_code == 200 body = r.json() assert body["ok"] is True assert body["input_tokens"] == 12 assert body["output_tokens"] == 3 assert body["total_tokens"] == 15 row = _con(core.db_path).execute( "SELECT * FROM checks WHERE model_id='zai/test-model-a'").fetchone() assert row["input_tokens"] == 12 and row["total_tokens"] == 15 assert json.loads(row["usage_json"])["prompt_tokens"] == 12 def test_api_check_missing_usage_null(core, omni_server): omni_server.set_completions(no_usage=True) body = core.client.get("/api/check/zai/test-model-a").json() assert body["ok"] is True assert body["input_tokens"] is None and body["total_tokens"] is None row = _con(core.db_path).execute( "SELECT input_tokens, total_tokens, usage_json FROM checks WHERE model_id='zai/test-model-a'").fetchone() assert row["input_tokens"] is None and row["total_tokens"] is None and row["usage_json"] is None def test_api_check_failed_content_preserves_usage(core, omni_server): omni_server.set_completions(usage={"prompt_tokens": 8, "completion_tokens": 1, "total_tokens": 9}, omit_choices=True) body = core.client.get("/api/check/agy/test-model-b").json() assert body["ok"] is False assert body["input_tokens"] == 8 # usage пережил ошибку контента row = _con(core.db_path).execute( "SELECT ok, error, total_tokens FROM checks WHERE model_id='agy/test-model-b'").fetchone() assert row["ok"] == 0 and row["error"] assert row["total_tokens"] == 9 def test_api_check_no_response_stores_null(core, omni_server): # недоступный OmniRoute: ответа нет — usage остаётся NULL (не нулями) saved = core.main.OMNIROUTE_URL core.main.OMNIROUTE_URL = "http://127.0.0.1:1" # connection refused, мгновенно try: body = core.client.get("/api/check/zai/test-model-a").json() finally: core.main.OMNIROUTE_URL = saved assert body["ok"] is False assert body["input_tokens"] is None and body["total_tokens"] is None row = _con(core.db_path).execute( "SELECT ok, error, total_tokens, usage_json FROM checks WHERE model_id='zai/test-model-a'").fetchone() assert row["ok"] == 0 and row["error"] assert row["total_tokens"] is None and row["usage_json"] is None # ─── истории: usage в /api/checks и /api/models.history ────────────────────── def test_api_checks_history_includes_usage(core, omni_server): omni_server.set_completions(usage={"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7}) core.client.get("/api/check/zai/test-model-a") omni_server.set_completions(no_usage=True) core.client.get("/api/check/zai/test-model-a") data = core.client.get("/api/checks/zai/test-model-a").json() assert len(data["checks"]) == 2 with_usage = [c for c in data["checks"] if c["total_tokens"] == 7] without = [c for c in data["checks"] if c["total_tokens"] is None] assert len(with_usage) == 1 and with_usage[0]["input_tokens"] == 5 assert with_usage[0]["usage_json"] assert len(without) == 1 and without[0]["input_tokens"] is None # сегментная история тоже несёт токены (для тултипов UI) models = core.client.get("/api/models").json() model = next(m for m in models if m["id"] == "zai/test-model-a") toks = [h["total_tokens"] for h in model["history"]] assert set(toks) == {7, None} # ─── /api/token_stats ───────────────────────────────────────────────────────── def test_token_stats_empty_db(core): ts = core.client.get("/api/token_stats").json() assert ts["window_24h"]["checks"] == 0 assert ts["window_24h"]["coverage_pct"] is None assert ts["all_time"]["checks"] == 0 assert ts["providers"] == [] def test_token_stats_windows_coverage_providers(core): # zai: 2 свежих с usage + 1 старая (>24h) с usage _insert_check(core.db_path, "zai/test-model-a", _iso(minutes=-10), inp=10, out=5, tot=15) _insert_check(core.db_path, "zai/test-model-a", _iso(minutes=-20), inp=20, out=2, tot=22) _insert_check(core.db_path, "zai/test-model-a", _iso(hours=-30), inp=100, out=50, tot=150) # agy: 1 свежая без usage + 1 старая с usage _insert_check(core.db_path, "agy/test-model-b", _iso(minutes=-5)) _insert_check(core.db_path, "agy/test-model-b", _iso(hours=-40), inp=7, out=3, tot=10) # модель, которой нет в таблице models: провайдер выводится из model_id _insert_check(core.db_path, "orphan/x", _iso(minutes=-1), inp=5, out=5, tot=10) ts = core.client.get("/api/token_stats").json() w = ts["window_24h"] assert w["checks"] == 4 and w["known"] == 3 and w["unknown"] == 1 assert w["coverage_pct"] == 75 assert (w["input_tokens"], w["output_tokens"], w["total_tokens"]) == (35, 12, 47) a = ts["all_time"] assert a["checks"] == 6 and a["known"] == 5 and a["unknown"] == 1 assert (a["input_tokens"], a["output_tokens"], a["total_tokens"]) == (142, 65, 207) provs = {p["provider"]: p for p in ts["providers"]} assert set(provs) == {"zai", "agy", "orphan"} assert provs["zai"]["window_24h"]["total_tokens"] == 37 assert provs["zai"]["all_time"]["total_tokens"] == 187 assert provs["agy"]["window_24h"]["known"] == 0 assert provs["agy"]["window_24h"]["unknown"] == 1 assert provs["agy"]["window_24h"]["total_tokens"] is None # сумма по NULL-группе assert provs["agy"]["all_time"]["total_tokens"] == 10 assert provs["orphan"]["window_24h"]["total_tokens"] == 10 # fallback из model_id def test_token_stats_counts_both_paths(core, omni_server): """probe.run() и ручной /api/check пишут usage, агрегат считает оба пути.""" omni_server.set_completions(usage={"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}) core.probe.run() # путь cron: zai 15 + agy 15 core.client.get("/api/check/zai/test-model-a") # путь ручной: +15 omni_server.set_completions(no_usage=True) core.client.get("/api/check/agy/test-model-b") # путь ручной без usage: NULL ts = core.client.get("/api/token_stats").json() w = ts["window_24h"] assert w["checks"] == 4 # 2 probe + 2 ручных assert w["known"] == 3 # probe×2 + ручной zai assert w["unknown"] == 1 # ручной agy без usage assert w["total_tokens"] == 45 # 15 × 3 известных assert w["input_tokens"] == 30 and w["output_tokens"] == 15 provs = {p["provider"]: p for p in ts["providers"]} assert provs["zai"]["window_24h"]["total_tokens"] == 30 # probe + manual assert provs["agy"]["window_24h"]["total_tokens"] == 15 # только probe