feat: per-key spend accounting and per-task complexity pinning

API keys page: the table now answers "which department spends what" — per-key
requests, tokens in/out and cost for a selectable window (24h/7d/30d) plus a
cumulative all-time cost column, joined from stats by the key's account.

The stats log stays bounded while the accounting survives: once stats.jsonl
exceeds max_file_mb (default 16, 0 disables), records older than the
retention window fold into cumulative per-account totals in a small sidecar
file (tmp+rename on both) and leave the log. alltime = folded + in-file, a
record is never counted twice, verified end-to-end on synthetic data
(1.2 MB → 301 KB, totals identical before/after compaction and restarts).

Routing: a task rule can now PIN the complexity tier —
`[routing.tasks.legal] models=[...] tier="high"` (old plain-array rules parse
unchanged). The pinned tier replaces the router's verdict before the
escalation check and reports itself as route_source "router+task-rule@high".
The Routing page grows a per-task «Complexity» selector (Auto/low/medium/
high); verified live: a code prompt with a pinned high went
router+task-rule@high → the expensive model.

13 new strings in all 7 languages; docs/ROUTING(.ru) updated.
This commit is contained in:
infosave2007
2026-08-08 08:42:11 +03:00
parent ef28a6633d
commit 95e560dc72
18 changed files with 441 additions and 50 deletions
+6
View File
@@ -621,4 +621,10 @@ export default {
"bm.cutByCap": "Budget ging fürs Denken drauf — keine Antwort mehr",
"shell.uiStale": "Das Gateway wurde aktualisiert — diese Seite zeigt eine ältere Oberfläche.",
"shell.reload": "Neu laden",
"keys.col.requests": "Anfragen",
"keys.col.tokens": "Tokens ein/aus",
"keys.col.costAll": "$ gesamt",
"routing.tasks.tier": "Komplexität",
"routing.tasks.tierAuto": "Auto (Router)",
"routing.tasks.tierHint": "Komplexität für diese Aufgabe festlegen: das Router-Urteil wird ersetzt, high eskaliert weiterhin über die Modellliste.",
};
+6
View File
@@ -614,4 +614,10 @@ export default {
"bm.cutByCap": "budget spent on reasoning — answer never came",
"shell.uiStale": "The gateway was updated — this page is running an older interface.",
"shell.reload": "Reload",
"keys.col.requests": "Requests",
"keys.col.tokens": "Tokens in/out",
"keys.col.costAll": "$ all-time",
"routing.tasks.tier": "Complexity",
"routing.tasks.tierAuto": "Auto (router)",
"routing.tasks.tierHint": "Pin the complexity tier for this task: the router's verdict is replaced, high still escalates over the model list.",
};
+6
View File
@@ -621,4 +621,10 @@ export default {
"bm.cutByCap": "presupuesto gastado en razonar — sin respuesta final",
"shell.uiStale": "La pasarela se actualizó — esta página usa una interfaz antigua.",
"shell.reload": "Recargar",
"keys.col.requests": "Peticiones",
"keys.col.tokens": "Tokens ent/sal",
"keys.col.costAll": "$ total",
"routing.tasks.tier": "Complejidad",
"routing.tasks.tierAuto": "Auto (router)",
"routing.tasks.tierHint": "Fijar la complejidad de esta tarea: el veredicto del router se sustituye, high sigue escalando por encima de la lista de modelos.",
};
+6
View File
@@ -621,4 +621,10 @@ export default {
"bm.cutByCap": "budget épuisé en raisonnement — pas de réponse",
"shell.uiStale": "La passerelle a été mise à jour — cette page utilise une ancienne interface.",
"shell.reload": "Recharger",
"keys.col.requests": "Requêtes",
"keys.col.tokens": "Tokens in/out",
"keys.col.costAll": "$ au total",
"routing.tasks.tier": "Complexité",
"routing.tasks.tierAuto": "Auto (routeur)",
"routing.tasks.tierHint": "Fixer la complexité de cette tâche : le verdict du routeur est remplacé, high continue d'escalader au-dessus de la liste de modèles.",
};
+6
View File
@@ -613,4 +613,10 @@ export default {
"bm.cutByCap": "бюджет ушёл на размышления — ответ не успел",
"shell.uiStale": "Шлюз обновился — на этой странице старый интерфейс.",
"shell.reload": "Перезагрузить",
"keys.col.requests": "Запросы",
"keys.col.tokens": "Токены вх/исх",
"keys.col.costAll": "$ за всё время",
"routing.tasks.tier": "Сложность",
"routing.tasks.tierAuto": "Авто (по роутеру)",
"routing.tasks.tierHint": "Закрепить уровень сложности для задачи: вердикт роутера заменяется, high по-прежнему эскалирует поверх списка моделей.",
};
+6
View File
@@ -621,4 +621,10 @@ export default {
"bm.cutByCap": "bütçe düşünmeye gitti — yanıt gelemedi",
"shell.uiStale": "Ağ geçidi güncellendi — bu sayfa eski arayüzü çalıştırıyor.",
"shell.reload": "Yeniden yükle",
"keys.col.requests": "İstekler",
"keys.col.tokens": "Token giriş/çıkış",
"keys.col.costAll": "$ toplam",
"routing.tasks.tier": "Karmaşıklık",
"routing.tasks.tierAuto": "Otomatik (yönlendirici)",
"routing.tasks.tierHint": "Bu görev için karmaşıklık katmanını sabitle: yönlendiricinin kararı değiştirilir, high yine model listesinin üzerine tırmanır.",
};
+6
View File
@@ -621,4 +621,10 @@ export default {
"bm.cutByCap": "预算全用于思考——未能给出答案",
"shell.uiStale": "网关已更新 — 此页面运行的是旧界面。",
"shell.reload": "重新加载",
"keys.col.requests": "请求数",
"keys.col.tokens": "Token 入/出",
"keys.col.costAll": "$ 累计",
"routing.tasks.tier": "复杂度",
"routing.tasks.tierAuto": "自动(路由器)",
"routing.tasks.tierHint": "为该任务固定复杂度层级:替换路由器的判断,high 仍会越过模型列表进行升级。",
};