Files
claude/.claude/tasks/contracts/2026-10-09-model-router-tiers-1237.md
T

8.3 KiB

CONTRACT — model-router-tiers (wave 1-C: absolute tiers, availability fallback, derived phases)

  • date: 2026-10-09 | flow: feat | branch: feature/model-router-mod
  • status: active

REQUEST (verbatim — IMMUTABLE)

User (fr): "j'aimerais ne pas avoir a reflechir a tout ca, donc ne pas lancer les /route moi meme par exemple. On vois le reflect, orchestratm escalate et plan session (fable) par du preincipe que la sessino est sur fable de base ? Si on est sur haiku j'aimerias que ca fonctionne aussi avec les models adapte. et d'ailleurs que se passe til quand on a plus de credit fable, cela passe sur opus xhigh ? il faut se fallback. ET surtout oui avec un systeme adaptatif et pour la reflexion pour trouver une solution ou des idees, il faut le meilleur, pour en faire le plan en se basant sur cette reflexion. Bref un systeme logique et optimise. /ultrathink . ensuite je reload puginm tu test, on commit puis on passe a la vague 2"

CLARIFICATIONS

Q: "session" phases / A: no phase keeps "the session model" any more: every phase names a TIER (best, big, work, cheap), an ordered list of aliases; the first AVAILABLE alias wins. plan/reflect/orchestrate/escalate → best (fable, then opus, then sonnet). A haiku session asked to plan runs the plan on fable. [user: "si on est sur haiku j'aimerais que ca fonctionne aussi avec les models adaptés"] Q: what is "available" / A: the mod cannot read per-model quota (rateLimits are account windows: five_hour, seven_day). Availability = a circuit breaker: a model is DOWN for cooldownMinutes (default 15) after a main or agent turn ends with reason: 'error' or 'refusal' on it, or when the engine itself switched away from it (classic.PostModelSwitch, source: 'auto'). A down model is skipped in every tier and in the fallback chain; /route show lists down models with their reset time; /route reload clears the breaker. [orchestrator — derived from the engine's declarations] Q: no credit left on fable / A: main loop on a down model → next available alias of the fallback chain (fable, opus, sonnet, haiku), effort unchanged (plan stays xhigh → "opus xhigh"), always allowed (a down model yields nothing), logged and shown. [user: "il faut se fallback"] Q: main-loop model moves / A: UPGRADE (phase tier ranks above the current model) allowed by default (mainUpgrade: true); DOWNGRADE (cheaper model) still gated by mainModelSwitch (default false: one cold-cache step per switch, and haiku's window may not fit); same rank → no switch. [orchestrator — cost/quality trade-off, stated to the user] Q: no /route typed by the user / A: phases come from (1) prompt rules at turn start (keyword → phase as the turn's DEFAULT route, source 'prompt', overridable; ultrathink stays a FLOOR), (2) the model's own route calls and the skills table, (3) derived: an Agent dispatch from main pushes orchestrate and the previous route is restored when the last live agent ends, (4) optional classifier (classifier: false by default) that asks the engine's small model for a phase label when no rule matched. [user: "ne pas lancer les /route moi-même"] Q: default prompt rules / A: floor: \bultrathink\b → escalate. Defaults: \b(plan|planifie|planning|brainstorm|architecture|con[cç]ois|design)\b → plan; \b(pourquoi|why|explique|explain|analyse|analyze|comprendre|understand|review|audit)\b → reflect. No default rule lowers a turn (no mechanical rule): lowering is explicit (route tool, skills). [orchestrator — conservative defaults, user-editable in the override] Q: typed /effort-<l> / A: unchanged (floor + default of the turn, B1).

ACCEPTANCE CRITERIA

  1. Suite green: claude plugin test passes with at least 40 test( calls; claude plugin validate passes with no warning; no line over 80 chars; no any type. CHECK: cd mods/model-router && out=(claude plugin test . 2>&1); rc=?; echo "$out" | tail -n 3; [ $rc -eq 0 ] && [ "$(grep -cE '^\stest(' hooks/register.test.ts)" -ge 40 ] && v=$(claude plugin validate . 2>&1) && echo "$v" | grep -q 'Validation passed' && ! echo "$v" | grep -qi 'warning' && ! grep -nE '.{81,}' hooks/register.ts hooks/register.test.ts && ! grep -nE ':\sany\b||as any\b' hooks/register.ts && echo TIERS-SUITE-OK EXPECT: TIERS-SUITE-OK EVIDENCE: pending
  2. Type-check clean against this build's declarations. CHECK: T=/Users/b.chanot/Documents/claude/mods/model-router/.claude-plugin/types; W=$(mktemp -d) && printf '{"compilerOptions":{"target":"es2023","lib":["es2023"],"types":[],"module":"esnext","moduleResolution":"bundler","strict":true,"noUncheckedIndexedAccess":true,"noEmit":true,"skipLibCheck":true,"jsx":"react","jsxFactory":"h","jsxFragmentFactory":"Fragment"},"include":["%s/claude-code/index.d.ts","%s/claude-code-tools/index.d.ts","%s/hooks"]}' "$T" "$T" "$PWD/mods/model-router" > "$W/tsconfig.json" && (cd "$W" && npx --yes -p typescript@5 tsc -p tsconfig.json) && echo TSC-OK EXPECT: TSC-OK EVIDENCE: pending
  3. Tiers in the config: tiers (best/big/work/cheap), fallback, cooldownMinutes, mainUpgrade, classifier exist in DEFAULT_CONFIG; every default phase names a tier, none a bare model; /route show prints the resolved model of each phase and a down: line. CHECK: cd mods/model-router/hooks && grep -q "tiers:" register.ts && grep -q "fallback:" register.ts && grep -q "cooldownMinutes" register.ts && grep -q "mainUpgrade" register.ts && grep -q "classifier" register.ts && [ "$(awk '/^const DEFAULT_CONFIG/,/^}/' register.ts | grep -cE "tier: '(best|big|work|cheap)'")" -ge 10 ] && ! awk '/^const DEFAULT_CONFIG/,/^}/' register.ts | grep -qE "^\s+[a-z]+: { model: '" && grep -q "down:" register.ts && echo TIERS-CONFIG-OK EXPECT: TIERS-CONFIG-OK EVIDENCE: pending
  4. Tests prove (names contain the quoted word): tier — a plan route on a session model claude-haiku-4-5-20251001 makes the main step run on claude-fable-5-1 at xhigh (upgrade, default on); downgrade — a mechanical route on a fable session leaves the model unchanged while mainModelSwitch is off; fallback — after a main turn.complete with reason: 'error' on fable, the next main step runs on claude-opus-5-5 with its effort unchanged, and after /route reload fable is used again; breaker — a classic.PostModelSwitch with source: 'auto' from fable marks fable down and /route show lists it; spawn — Explore spawns on claude-opus-5-5 while sonnet is down; derived — an Agent tool call from main sets orchestrate and the previous route (plan) is back after the last agent's turn.complete; default rule — a prompt "planifie la migration" sets the plan route as the turn default and a later route tool call overrides it; floor tests from B1 still pass. CHECK: cd mods/model-router/hooks && for w in tier downgrade fallback breaker spawn derived "default rule"; do grep -qE "test('[^']*$w" register.test.ts || { echo "missing test: $w"; exit 1; }; done && echo TIERS-TESTS-OK EXPECT: TIERS-TESTS-OK EVIDENCE: pending
  5. Judged by reading: ONE resolver (resolveModel) turns a tier name, an alias or a full id into the first AVAILABLE full id (tier → list → skip down → alias → id; a bare alias or id passes through even when down, since explicit means explicit); rank = position in fallback; the main-loop decision is: route model wanted → upgrade allowed by mainUpgrade, downgrade gated by mainModelSwitch + window, same rank → keep; no route and current model down → next available in fallback; the breaker is fed only by turn.complete reason in error|refusal (main: the last main step's model; agent: the loop's spawn model) and by classic.PostModelSwitch source: 'auto' (from_model), never by an aborted turn; the derived orchestrate push/pop never overrides a route the model declared after the dispatch; prompt default rules write turnMain (source 'prompt'), floor rules write turnFloor; the classifier runs only when classifier is true and no rule matched, through $.model.classify, labels limited to the phase names plus other, any failure → no route; explicit Agent params still win; agent model fixed at spawn; every B1/1-A criterion still holds; no function over 25 logic lines; truthful texts name the resolved model and say "fallback" when the breaker chose it.

FILE SCOPE

mods/model-router/hooks/register.ts · mods/model-router/hooks/register.test.ts