fix: hide stale and effort-preset models without losing checks
Build and deploy / build-deploy (push) Waiting to run
Build and deploy / build-deploy (push) Waiting to run
This commit is contained in:
+39
-12
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -95,6 +96,7 @@ def init_db():
|
|||||||
text_model_match TEXT,
|
text_model_match TEXT,
|
||||||
agent_model_match TEXT,
|
agent_model_match TEXT,
|
||||||
updated_at TEXT,
|
updated_at TEXT,
|
||||||
|
active INTEGER NOT NULL DEFAULT 1,
|
||||||
FOREIGN KEY(text_model_match) REFERENCES arena_text_leaderboard(model_name),
|
FOREIGN KEY(text_model_match) REFERENCES arena_text_leaderboard(model_name),
|
||||||
FOREIGN KEY(agent_model_match) REFERENCES arena_agent_leaderboard(model_name)
|
FOREIGN KEY(agent_model_match) REFERENCES arena_agent_leaderboard(model_name)
|
||||||
);
|
);
|
||||||
@@ -133,6 +135,11 @@ def init_db():
|
|||||||
if col not in cols:
|
if col not in cols:
|
||||||
db.execute(f"ALTER TABLE checks ADD COLUMN {col} {decl}")
|
db.execute(f"ALTER TABLE checks ADD COLUMN {col} {decl}")
|
||||||
|
|
||||||
|
# Историю checks не трогаем; видимость модели живёт отдельно.
|
||||||
|
model_cols = [r[1] for r in db.execute("PRAGMA table_info(models)").fetchall()]
|
||||||
|
if "active" not in model_cols:
|
||||||
|
db.execute("ALTER TABLE models ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -232,7 +239,9 @@ def fetch_omni_models() -> list[dict]:
|
|||||||
)
|
)
|
||||||
with urllib.request.urlopen(req, timeout=15) as r:
|
with urllib.request.urlopen(req, timeout=15) as r:
|
||||||
data = json.loads(r.read())
|
data = json.loads(r.read())
|
||||||
return data.get("data", [])
|
if not isinstance(data, dict) or not isinstance(data.get("data"), list):
|
||||||
|
raise ValueError("OmniRoute returned an invalid models catalog")
|
||||||
|
return data["data"]
|
||||||
|
|
||||||
|
|
||||||
def is_skip(model_id: str) -> bool:
|
def is_skip(model_id: str) -> bool:
|
||||||
@@ -242,11 +251,17 @@ def is_skip(model_id: str) -> bool:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def is_effort_variant(model_id: str, catalog_ids: set[str]) -> bool:
|
||||||
|
"""Пресет только при наличии базового ID на том же маршруте.
|
||||||
|
|
||||||
|
Так не скрываем самостоятельные модели вроде qwen3.8-max.
|
||||||
|
"""
|
||||||
|
base = re.sub(r"-(?:low|medium|high|xhigh|max|ultra|none)(?=(:free)?$)", "", model_id)
|
||||||
|
return base != model_id and base in catalog_ids
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Arena matching (simple) ─────────────────────────────────────────────────
|
# ─── Arena matching (simple) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def _norm(s: str) -> str:
|
def _norm(s: str) -> str:
|
||||||
return re.sub(r"[^a-z0-9]", "", s.lower())
|
return re.sub(r"[^a-z0-9]", "", s.lower())
|
||||||
|
|
||||||
@@ -332,7 +347,12 @@ def sync_models():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
if not omni:
|
||||||
|
print("[sync] OmniRoute returned an empty catalog; keeping previous visibility")
|
||||||
|
db.close()
|
||||||
|
return
|
||||||
|
catalog_ids = {m["id"] for m in omni}
|
||||||
|
seen = set()
|
||||||
for m in omni:
|
for m in omni:
|
||||||
mid = m["id"]
|
mid = m["id"]
|
||||||
if is_skip(mid):
|
if is_skip(mid):
|
||||||
@@ -340,6 +360,9 @@ def sync_models():
|
|||||||
# openrouter — только :free
|
# openrouter — только :free
|
||||||
if mid.startswith("openrouter/") and not mid.endswith(":free"):
|
if mid.startswith("openrouter/") and not mid.endswith(":free"):
|
||||||
continue
|
continue
|
||||||
|
if is_effort_variant(mid, catalog_ids):
|
||||||
|
continue
|
||||||
|
seen.add(mid)
|
||||||
|
|
||||||
provider = mid.split("/")[0]
|
provider = mid.split("/")[0]
|
||||||
raw = mid.split("/")[-1].lower()
|
raw = mid.split("/")[-1].lower()
|
||||||
@@ -353,20 +376,24 @@ def sync_models():
|
|||||||
|
|
||||||
db.execute("""
|
db.execute("""
|
||||||
INSERT INTO models (id, provider, name, raw_base, norm_base, clean_base,
|
INSERT INTO models (id, provider, name, raw_base, norm_base, clean_base,
|
||||||
text_model_match, agent_model_match, updated_at)
|
text_model_match, agent_model_match, updated_at, active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
text_model_match = excluded.text_model_match,
|
text_model_match = excluded.text_model_match,
|
||||||
agent_model_match = excluded.agent_model_match,
|
agent_model_match = excluded.agent_model_match,
|
||||||
updated_at = excluded.updated_at
|
updated_at = excluded.updated_at,
|
||||||
|
active = 1
|
||||||
""", (
|
""", (
|
||||||
mid, provider, mid, base, _norm(base), _norm(cleaned),
|
mid, provider, mid, base, _norm(base), _norm(cleaned),
|
||||||
text_match, agent_match, now
|
text_match, agent_match, now
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Снимок каталога целиком: устаревшие/пресетные строки сохраняются для
|
||||||
|
# истории checks, но не попадают в список и следующий cron-прогон.
|
||||||
|
db.execute("UPDATE models SET active = 0 WHERE updated_at IS NULL OR updated_at != ?", (now,))
|
||||||
db.commit()
|
db.commit()
|
||||||
db.close()
|
db.close()
|
||||||
print(f"[sync] synced {len(omni)} models at {now} with SQL foreign keys")
|
print(f"[sync] active {len(seen)} models from {len(omni)} OmniRoute entries at {now}")
|
||||||
|
|
||||||
|
|
||||||
# ─── Stats helpers ────────────────────────────────────────────────────────────
|
# ─── Stats helpers ────────────────────────────────────────────────────────────
|
||||||
@@ -538,7 +565,7 @@ def api_models(
|
|||||||
FROM models m
|
FROM models m
|
||||||
LEFT JOIN arena_text_leaderboard t ON m.text_model_match = t.model_name
|
LEFT JOIN arena_text_leaderboard t ON m.text_model_match = t.model_name
|
||||||
LEFT JOIN arena_agent_leaderboard a ON m.agent_model_match = a.model_name
|
LEFT JOIN arena_agent_leaderboard a ON m.agent_model_match = a.model_name
|
||||||
WHERE 1=1
|
WHERE m.active = 1
|
||||||
"""
|
"""
|
||||||
params: list = []
|
params: list = []
|
||||||
if provider:
|
if provider:
|
||||||
@@ -585,7 +612,7 @@ def api_models(
|
|||||||
@app.get("/api/providers")
|
@app.get("/api/providers")
|
||||||
def api_providers():
|
def api_providers():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
rows = db.execute("SELECT DISTINCT provider FROM models ORDER BY provider").fetchall()
|
rows = db.execute("SELECT DISTINCT provider FROM models WHERE active = 1 ORDER BY provider").fetchall()
|
||||||
db.close()
|
db.close()
|
||||||
return [r["provider"] for r in rows]
|
return [r["provider"] for r in rows]
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -81,7 +81,11 @@ def _ensure_usage_columns(db: sqlite3.Connection):
|
|||||||
def run():
|
def run():
|
||||||
db = sqlite3.connect(DB_PATH, check_same_thread=False)
|
db = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
_ensure_usage_columns(db)
|
_ensure_usage_columns(db)
|
||||||
models = [r[0] for r in db.execute("SELECT id FROM models").fetchall()]
|
model_cols = [r[1] for r in db.execute("PRAGMA table_info(models)")]
|
||||||
|
if "active" not in model_cols:
|
||||||
|
db.execute("ALTER TABLE models ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
|
||||||
|
db.commit()
|
||||||
|
models = [r[0] for r in db.execute("SELECT id FROM models WHERE active = 1").fetchall()]
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
print(f"[probe] checking {len(models)} models @ {datetime.now(timezone.utc).isoformat()}")
|
print(f"[probe] checking {len(models)} models @ {datetime.now(timezone.utc).isoformat()}")
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ def _reset_omni(omni_server):
|
|||||||
"""Изолировать тесты: состояние мока не должно протекать между тестами."""
|
"""Изолировать тесты: состояние мока не должно протекать между тестами."""
|
||||||
with omni_server.lock:
|
with omni_server.lock:
|
||||||
omni_server.state["per_model"] = {}
|
omni_server.state["per_model"] = {}
|
||||||
|
omni_server.state["models"] = {"data": [
|
||||||
|
{"id": "zai/test-model-a"}, {"id": "agy/test-model-b"},
|
||||||
|
]}
|
||||||
omni_server.state["global_delay"] = 0.0
|
omni_server.state["global_delay"] = 0.0
|
||||||
omni_server.state["completions"] = {"status": 200, "usage": None, "content": "4"}
|
omni_server.state["completions"] = {"status": 200, "usage": None, "content": "4"}
|
||||||
omni_server.requests.clear()
|
omni_server.requests.clear()
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Каталог OmniRoute: пропавшие ID и effort-пресеты не должны загрязнять витрину."""
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_hides_stale_and_effort_but_preserves_history(core, omni_server):
|
||||||
|
db = sqlite3.connect(core.db_path)
|
||||||
|
db.execute("INSERT INTO checks (model_id,checked_at,ok) VALUES ('zai/test-model-a','2026-10-26T00:00:00+00:00',1)")
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
omni_server.state["models"] = {"data": [
|
||||||
|
{"id": "agy/test-model-b"},
|
||||||
|
{"id": "agy/test-model-b-high"},
|
||||||
|
{"id": "cx/gpt-5.6-terra"},
|
||||||
|
{"id": "cx/gpt-5.6-terra-low"},
|
||||||
|
{"id": "openrouter/foo/model-high:free"},
|
||||||
|
{"id": "openrouter/foo/model:free"},
|
||||||
|
{"id": "agy/kimi-highway"},
|
||||||
|
{"id": "freellmapi/qwen3.8-max"},
|
||||||
|
]}
|
||||||
|
assert core.client.post("/api/sync").status_code == 200
|
||||||
|
ids = {m["id"] for m in core.client.get("/api/models").json()}
|
||||||
|
assert ids == {"agy/test-model-b", "cx/gpt-5.6-terra", "openrouter/foo/model:free", "agy/kimi-highway", "freellmapi/qwen3.8-max"}
|
||||||
|
assert "zai" not in core.client.get("/api/providers").json()
|
||||||
|
db = sqlite3.connect(core.db_path)
|
||||||
|
assert db.execute("SELECT active FROM models WHERE id='zai/test-model-a'").fetchone()[0] == 0
|
||||||
|
assert db.execute("SELECT COUNT(*) FROM checks WHERE model_id='zai/test-model-a'").fetchone()[0] == 1
|
||||||
|
db.close()
|
||||||
|
omni_server.requests.clear()
|
||||||
|
core.probe.run()
|
||||||
|
requested = {x["body"].get("model") for x in omni_server.requests if x["path"] == "/v1/chat/completions"}
|
||||||
|
assert requested == ids
|
||||||
|
# ID возвращается в каталог — снова активен, история на месте.
|
||||||
|
omni_server.state["models"]["data"].append({"id": "zai/test-model-a"})
|
||||||
|
core.client.post("/api/sync")
|
||||||
|
assert "zai/test-model-a" in {m["id"] for m in core.client.get("/api/models").json()}
|
||||||
|
db = sqlite3.connect(core.db_path)
|
||||||
|
assert db.execute("SELECT active FROM models WHERE id='zai/test-model-a'").fetchone()[0] == 1
|
||||||
|
assert db.execute("SELECT COUNT(*) FROM checks WHERE model_id='zai/test-model-a'").fetchone()[0] == 1
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_catalog_does_not_hide_everything(core, omni_server):
|
||||||
|
original = {m["id"] for m in core.client.get("/api/models").json()}
|
||||||
|
omni_server.state["models"] = {"data": []}
|
||||||
|
core.client.post("/api/sync")
|
||||||
|
assert {m["id"] for m in core.client.get("/api/models").json()} == original
|
||||||
Reference in New Issue
Block a user