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:
infosave2007
2026-08-08 08:54:27 +03:00
parent 95e560dc72
commit 06813b3e97
15 changed files with 316 additions and 183 deletions
+6 -13
View File
@@ -232,22 +232,15 @@ impl Pipeline {
match live.router.route(&text, prof).await {
Ok(Some(dec)) => {
let src = dec.source.clone();
let mut dec = dec;
let (cands, task_rule, forced) = live
let (cands, task_rule) = live
.routing
.candidates_for(&dec.task_label, &dec.complexity_tier);
// surface which rule chose the head of the chain,
// and when the rule pinned the complexity —
// "…+task-rule@high" — reflect it in the
// decision so headers/stats tell the truth
let src = match (task_rule, &forced) {
(true, Some(tier)) => format!("{src}+task-rule@{tier}"),
(true, None) => format!("{src}+task-rule"),
_ => src,
// surface which rule chose the head of the chain
let src = if task_rule {
format!("{src}+task-rule")
} else {
src
};
if let Some(tier) = forced {
dec.complexity_tier = tier;
}
(dec, src, cands)
}
Ok(None) | Err(_) => {