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:
Vendored
+33
-3
@@ -54,10 +54,17 @@ export async function renderRouting() {
|
||||
// local editable state — ensure standard tiers exist for a friendly editor
|
||||
const tiers = Object.assign({ low: [], medium: [], high: [] }, routing.tiers || {});
|
||||
const tasks = {};
|
||||
for (const l of taskLabels) tasks[l] = (routing.tasks || {})[l] || [];
|
||||
const taskTiers = {};
|
||||
for (const l of taskLabels) {
|
||||
const v = (routing.tasks || {})[l];
|
||||
// GET returns the full form {models, tier}; older configs may be arrays
|
||||
tasks[l] = Array.isArray(v) ? v : (v && v.models) || [];
|
||||
taskTiers[l] = (v && !Array.isArray(v) && v.tier) || "";
|
||||
}
|
||||
const st = {
|
||||
tiers,
|
||||
tasks,
|
||||
taskTiers,
|
||||
default: routing.default || ids[0] || "",
|
||||
policy: routing.policy || { mode: "fixed_table", min_class: {} },
|
||||
};
|
||||
@@ -130,10 +137,29 @@ export async function renderRouting() {
|
||||
h("div", { class: "muted small mono" }, label),
|
||||
share != null ? h("div", { class: "muted small" }, t("routing.tasks.share", { p: Math.round(share * 100) })) : null
|
||||
),
|
||||
chipEditor(st.tasks, label, t("routing.tasks.inherit"))
|
||||
chipEditor(st.tasks, label, t("routing.tasks.inherit")),
|
||||
tierSelect(label)
|
||||
);
|
||||
}
|
||||
|
||||
// Per-task complexity pin: "авто" follows the router's verdict; a fixed
|
||||
// tier forces it for this task (legal is always high, chitchat always low)
|
||||
function tierSelect(label) {
|
||||
const sel = h(
|
||||
"select",
|
||||
{ class: "task-tier", title: t("routing.tasks.tierHint") },
|
||||
h("option", { value: "" }, t("routing.tasks.tierAuto")),
|
||||
...Object.keys(st.tiers).map((tr) =>
|
||||
h("option", { value: tr, selected: st.taskTiers[label] === tr ? "" : null }, tr))
|
||||
);
|
||||
sel.value = st.taskTiers[label] || "";
|
||||
sel.addEventListener("change", () => {
|
||||
st.taskTiers[label] = sel.value;
|
||||
});
|
||||
return h("div", { class: "task-tier-wrap" },
|
||||
h("div", { class: "muted small" }, t("routing.tasks.tier")), sel);
|
||||
}
|
||||
|
||||
function tasksCard() {
|
||||
const addIn = h("input", { placeholder: t("routing.tasks.addPlaceholder"), style: "max-width:220px" });
|
||||
const addBtn = h("button", { class: "btn ghost sm" }, "+ " + t("routing.tasks.addCustom"));
|
||||
@@ -201,7 +227,11 @@ export async function renderRouting() {
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const tasks = {};
|
||||
for (const [k, v] of Object.entries(st.tasks)) if (v.length) tasks[k] = v;
|
||||
for (const [k, v] of Object.entries(st.tasks)) {
|
||||
const tier = (st.taskTiers || {})[k] || "";
|
||||
if (tier) tasks[k] = { models: v, tier };
|
||||
else if (v.length) tasks[k] = v;
|
||||
}
|
||||
await api.putRouting({ tiers: st.tiers, tasks, default: st.default, policy: st.policy });
|
||||
toast(t("toast.saved"), "good");
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user