feat(routing)!: task rules become per-complexity model maps
The single per-task «Complexity» pin was the wrong shape — what you actually
decide is WHICH model serves a task at WHICH complexity. A task rule is now a
per-tier map, edited in the console as rows of “model ▾ + complexity ▾” pairs
(plus ×/add) inside each task:
[routing.tasks.code]
low = ["cheap-coder"]
high = ["best-coder"]
any = ["generic-coder"] # leads at every tier after the tier pick
Chain order: the task's tier-specific list → (at high: the global high chain,
so escalation still beats any-tier pins) → the task's any-tier list → the
global tier chain → default as the last resort; deduplicated. Plain-array
rules keep meaning "any tier" unchanged, and the short-lived {models, tier}
form from the previous commit is normalized into the map on load.
Verified live: legacy config migrated visibly in the editor, pairs survive
save+reload, a math prompt at low ignored the math@high pair, per-tier leads
confirmed by unit tests (19 passing).
This commit is contained in:
Vendored
+4
-2
@@ -592,8 +592,10 @@ body.modal-open { overflow: hidden; }
|
||||
|
||||
.ok { color: var(--good); }
|
||||
|
||||
.task-tier-wrap { display: flex; align-items: center; gap: 6px; margin-left: 10px; white-space: nowrap; }
|
||||
.task-tier { max-width: 130px; padding: 4px 8px; font-size: 12px; }
|
||||
.pairs { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
||||
.pair-row { display: flex; gap: 6px; align-items: center; }
|
||||
.pair-model { min-width: 220px; max-width: 340px; padding: 4px 8px; font-size: 12.5px; }
|
||||
.pair-tier { max-width: 150px; padding: 4px 8px; font-size: 12.5px; }
|
||||
|
||||
/* Benchmark view */
|
||||
@keyframes bm-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
Vendored
+3
-1
@@ -626,5 +626,7 @@ export default {
|
||||
"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.",
|
||||
"routing.tasks.tierHint": "Welches Modell diese Aufgabe bei welcher Komplexität bedient; \"jede\" führt auf allen Stufen. Reihenfolge = Fallback-Reihenfolge. High eskaliert weiterhin über \"jede\"-Einträge.",
|
||||
"routing.tasks.tierAny": "jede Komplexität",
|
||||
"routing.tasks.addModel": "Modell",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -619,5 +619,7 @@ export default {
|
||||
"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.",
|
||||
"routing.tasks.tierHint": "Which model serves this task at which complexity; \"any\" leads at every tier. Order = fallback order. High verdicts still escalate over any-tier pins.",
|
||||
"routing.tasks.tierAny": "any complexity",
|
||||
"routing.tasks.addModel": "model",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -626,5 +626,7 @@ export default {
|
||||
"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.",
|
||||
"routing.tasks.tierHint": "Qué modelo atiende esta tarea en qué complejidad; «cualquiera» lidera en todos los niveles. El orden = orden de respaldo. Un veredicto high sigue escalando sobre las entradas «cualquiera».",
|
||||
"routing.tasks.tierAny": "cualquier complejidad",
|
||||
"routing.tasks.addModel": "modelo",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -626,5 +626,7 @@ export default {
|
||||
"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.",
|
||||
"routing.tasks.tierHint": "Quel modèle sert cette tâche à quelle complexité ; « toute » mène à tous les niveaux. L'ordre = ordre de repli. Un verdict high escalade toujours au-dessus des entrées « toute ».",
|
||||
"routing.tasks.tierAny": "toute complexité",
|
||||
"routing.tasks.addModel": "modèle",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -618,5 +618,7 @@ export default {
|
||||
"keys.col.costAll": "$ за всё время",
|
||||
"routing.tasks.tier": "Сложность",
|
||||
"routing.tasks.tierAuto": "Авто (по роутеру)",
|
||||
"routing.tasks.tierHint": "Закрепить уровень сложности для задачи: вердикт роутера заменяется, high по-прежнему эскалирует поверх списка моделей.",
|
||||
"routing.tasks.tierHint": "Какая модель обслуживает задачу на какой сложности; «любая» ведёт на всех уровнях. Порядок = порядок фолбэка. Вердикт high по-прежнему эскалирует поверх «любых».",
|
||||
"routing.tasks.tierAny": "любая сложность",
|
||||
"routing.tasks.addModel": "модель",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -626,5 +626,7 @@ export default {
|
||||
"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.",
|
||||
"routing.tasks.tierHint": "Bu görevi hangi karmaşıklıkta hangi model karşılar; \"her\" tüm katmanlarda önde gider. Sıra = yedekleme sırası. High kararı \"her\" girdilerinin üzerine yine tırmanır.",
|
||||
"routing.tasks.tierAny": "her karmaşıklık",
|
||||
"routing.tasks.addModel": "model",
|
||||
};
|
||||
|
||||
Vendored
+3
-1
@@ -626,5 +626,7 @@ export default {
|
||||
"keys.col.costAll": "$ 累计",
|
||||
"routing.tasks.tier": "复杂度",
|
||||
"routing.tasks.tierAuto": "自动(路由器)",
|
||||
"routing.tasks.tierHint": "为该任务固定复杂度层级:替换路由器的判断,high 仍会越过模型列表进行升级。",
|
||||
"routing.tasks.tierHint": "哪个模型在哪个复杂度上处理该任务;「任意」在所有层级领先。顺序即回退顺序。high 判定仍会越过「任意」条目升级。",
|
||||
"routing.tasks.tierAny": "任意复杂度",
|
||||
"routing.tasks.addModel": "模型",
|
||||
};
|
||||
|
||||
Vendored
+56
-31
@@ -54,17 +54,23 @@ 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 = {};
|
||||
const taskTiers = {};
|
||||
// A task rule is edited as ordered PAIRS (model, tier): "this model serves
|
||||
// this task at this complexity" (tier "any" = at every complexity).
|
||||
// GET returns {any:[], low:[], medium:[], high:[]}; legacy arrays = any.
|
||||
const taskPairs = {};
|
||||
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 pairs = [];
|
||||
if (Array.isArray(v)) for (const m of v) pairs.push({ model: m, tier: "any" });
|
||||
else if (v && typeof v === "object") {
|
||||
for (const tier of ["low", "medium", "high", "any"])
|
||||
for (const m of v[tier] || []) pairs.push({ model: m, tier });
|
||||
}
|
||||
taskPairs[l] = pairs;
|
||||
}
|
||||
const st = {
|
||||
tiers,
|
||||
tasks,
|
||||
taskTiers,
|
||||
taskPairs,
|
||||
default: routing.default || ids[0] || "",
|
||||
policy: routing.policy || { mode: "fixed_table", min_class: {} },
|
||||
};
|
||||
@@ -137,27 +143,43 @@ 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")),
|
||||
tierSelect(label)
|
||||
pairsEditor(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);
|
||||
// Pairs editor: each row is [model ▾][complexity ▾][×]; "any" serves every
|
||||
// tier. Order matters — it is the fallback order inside the tier.
|
||||
function pairsEditor(label) {
|
||||
const pairs = st.taskPairs[label];
|
||||
const wrap = h("div", { class: "pairs grow" });
|
||||
function paint() {
|
||||
mount(
|
||||
wrap,
|
||||
...pairs.map((p, i) => {
|
||||
const modelSel = h(
|
||||
"select",
|
||||
{ class: "pair-model" },
|
||||
...ids.map((id) => h("option", { value: id }, id))
|
||||
);
|
||||
modelSel.value = p.model;
|
||||
modelSel.addEventListener("change", () => { p.model = modelSel.value; });
|
||||
const tierSel = h(
|
||||
"select",
|
||||
{ class: "pair-tier", title: t("routing.tasks.tierHint") },
|
||||
h("option", { value: "any" }, t("routing.tasks.tierAny")),
|
||||
...Object.keys(st.tiers).map((tr) => h("option", { value: tr }, tr))
|
||||
);
|
||||
tierSel.value = p.tier;
|
||||
tierSel.addEventListener("change", () => { p.tier = tierSel.value; });
|
||||
const del = h("button", { class: "btn sm ghost danger", onClick: () => { pairs.splice(i, 1); paint(); } }, "×");
|
||||
return h("div", { class: "pair-row" }, modelSel, tierSel, del);
|
||||
}),
|
||||
pairs.length === 0 ? h("div", { class: "muted small", style: "align-self:center" }, t("routing.tasks.inherit")) : null,
|
||||
h("button", { class: "btn ghost sm", onClick: () => { pairs.push({ model: ids[0] || "", tier: "any" }); paint(); } }, "+ " + t("routing.tasks.addModel"))
|
||||
);
|
||||
}
|
||||
paint();
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function tasksCard() {
|
||||
@@ -165,8 +187,8 @@ export async function renderRouting() {
|
||||
const addBtn = h("button", { class: "btn ghost sm" }, "+ " + t("routing.tasks.addCustom"));
|
||||
addBtn.addEventListener("click", () => {
|
||||
const v = addIn.value.trim().toLowerCase();
|
||||
if (!v || st.tasks[v]) return;
|
||||
st.tasks[v] = [];
|
||||
if (!v || st.taskPairs[v]) return;
|
||||
st.taskPairs[v] = [];
|
||||
render();
|
||||
});
|
||||
return h(
|
||||
@@ -177,7 +199,7 @@ export async function renderRouting() {
|
||||
h("span", { class: "card-head sub" },
|
||||
labelsSource === "router" ? t("routing.tasks.fromRouter") : t("routing.tasks.builtin"))),
|
||||
h("div", { class: "muted small", style: "margin-bottom:10px" }, t("routing.tasks.hint")),
|
||||
...Object.keys(st.tasks).map(taskRow),
|
||||
...Object.keys(st.taskPairs).map(taskRow),
|
||||
h("div", { class: "flex", style: "gap:8px; margin-top:10px" }, addIn, addBtn)
|
||||
);
|
||||
}
|
||||
@@ -227,10 +249,13 @@ export async function renderRouting() {
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const tasks = {};
|
||||
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;
|
||||
for (const [k, pairs] of Object.entries(st.taskPairs)) {
|
||||
const rule = {};
|
||||
for (const p of pairs) {
|
||||
if (!p.model) continue;
|
||||
(rule[p.tier] = rule[p.tier] || []).push(p.model);
|
||||
}
|
||||
if (Object.keys(rule).length) tasks[k] = rule;
|
||||
}
|
||||
await api.putRouting({ tiers: st.tiers, tasks, default: st.default, policy: st.policy });
|
||||
toast(t("toast.saved"), "good");
|
||||
|
||||
Reference in New Issue
Block a user