Model Router (δ)
δ is the layer that decides which underlying LLM executes a given task. It consults the task’s requirements, the agent’s ξ profile, the κ rule engine’s admission constraints, and a scoring algorithm to produce a single named model. The result is recorded in the task’s ζ trail so later audits can reproduce what decision was made and why.
Phase 0 reality: δ ships library stubs that route every task to Claude. Per ADR-005 §Decision, the router interface is present from Phase 0 — scoring returns a constant (claude: 1.0), fallback has one member, adapter is Anthropic-only — so Phase 1.5 is a logic swap, not a new interface. R75 Wave I landed the stubs (PR #149 scoring, PR #150 fallback); multi-model routing activates no earlier than Phase 1.5. The spec below describes δ’s target shape; the §”Phase 0 posture” section at the bottom records what actually ships today.
Phase 0: Claude-only stubs (shipped R75 Wave I)
In Phase 0, δ is a trivial function: route(task) → "claude". The scaffolding — scoring module, fallback module, candidate table schema — is in place so that the Phase 1.5 change is a scoring-formula replacement and a new row in the candidate table, not an interface rewrite.
Every Phase 0 task runs on Claude. The model id is recorded in the task’s thought_records so that even though the router is trivial today, the audit trail is already structured for multi-model tomorrow.
Target shape: scoring algorithm
For Phase 1.5+, δ scores each candidate model against the task and picks the highest score. The scoring inputs, and their rough weights:
| Input | Weight | Source |
|---|---|---|
| Task domain fit | high | task.domain |
| Context-window fit | high | task.estimated_input_size |
| Cost ceiling | medium | task.budget_bps |
| Latency tier | medium | task.deadline |
| Reliability history (execution score) | medium | λ execution reputation for the model |
| Skill compatibility | medium | ε skill set filtered by model |
| Operator preference | low | operator config override |
All weights and formulas live in κ rule bodies. Changing them is a π proposal, which means routing policy is governed, not coded.
Scoring formula
For a task T and candidate model m:
score(m, T) = Σ ( weight_i × normalized_input_i(m, T) ) for i ∈ 7 dimensions
Each normalized input is bounded [0, 1]; the weight sum is 1.0 by construction (renormalized per rule version). Deterministic integer math: in practice all weights are stored as bps (0–10000) and the score accumulates as int64 with final scaling.
| Dimension | Formula | Default weight |
|---|---|---|
task_domain_match |
1 if task.domain ∈ model.profile else 0 |
0.20 |
context_window_fit |
min(1, model.window / task.tokens) |
0.15 |
cost_efficiency |
1 − (model.cost_per_1k / max_cost) clamped to [0,1] |
0.15 |
latency_fit |
1 − (model.p50_ms / task.deadline_ms) clamped |
0.15 |
reliability |
success rate over last 100 invocations | 0.15 |
skill_match |
overlap of model declared strengths with task skill requirement | 0.15 |
operator_preference |
operator-supplied override weight | 0.05 |
Ties break by (a) higher reliability, (b) lower cost, (c) alphabetical on model_id. Ties in practice are rare with 7 continuous dimensions.
The candidate table
δ reads from a model_candidates table (schema-ready in Phase 0, populated later) whose columns include:
model_id(e.g."claude-opus-4-6")providercontext_window_tokenslatency_tier(fast | balanced | slow)cost_bps_per_kilotokendomain_fit_profile(bitmask over the 8 ξ domains)enabled(boolean)
Eight model candidates are sketched in the spec as the initial post-Phase-0 cohort, covering the four major provider families and a local-model fallback. The exact list is not a Phase 0 fact; it will land when multi-model activates.
Phase 1.5 candidate cohort (spec)
| Model | Context window | Cost / 1k (indicative bps) | p50 latency | Notable strengths |
|---|---|---|---|---|
| Claude 3.5 Sonnet | 200k | high | balanced | code review, structured output, long-context reasoning |
| Claude 3.5 Haiku | 200k | low | fast | classification, triage, high-throughput calls |
| GPT-4o | 128k | high | balanced | multimodal, general reasoning |
| GPT-4o mini | 128k | low | fast | simple tasks, cheap fallback |
| Gemini 1.5 Pro | 1M | medium | slow | huge-context synthesis, video |
| Llama 3.3 70B | 128k | low (self-host) | varies | data-sovereign deploys, customization |
| Mixtral 8x22B | 64k | low | fast | permissive open-weight, code-heavy |
| Kimi K2 | 200k | medium | balanced | Chinese/English parity, long-context reasoning |
Costs and latencies are indicative; real routing uses fresh figures from the candidate table, which a post-task callback updates.
Execution modes (spec-only)
δ is expected to support three execution modes once active:
- Single-model — one model executes the whole task.
- Ensemble — multiple models run independently; their outputs are judged by a separate model or by β’s VERIFY step.
- Pipeline — a fast model does triage; a precise model handles the hard subtask.
Phase 0 runs only single-model (Claude). Ensemble and pipeline are in the δ spec so schema columns (routing_mode, secondary_model_id) exist.
Worked routing example
Task: “Code review of 50KB pull request, response budget ≤ 5s.” Task shape: domain=code_review, tokens≈12_000, deadline_ms=5_000, skill=code_review.
| Model | domain_match | window_fit | cost_eff | latency_fit | reliability | skill_match | pref | score |
|---|---|---|---|---|---|---|---|---|
| Claude Sonnet | 1.0 | 1.00 | 0.55 | 0.80 (4s) | 0.96 | 1.0 | 0.5 | 0.87 |
| GPT-4o | 1.0 | 1.00 | 0.55 | 0.20 (8s) | 0.92 | 1.0 | 0.5 | 0.79 |
| Claude Haiku | 0.8 | 1.00 | 0.90 | 0.95 (1s) | 0.75 | 0.7 | 0.5 | 0.58 |
Claude Sonnet wins — high domain + skill match, fits context, ~4s latency meets the budget. GPT-4o is a close second but misses the latency target. Haiku is the cheapest and fastest but its lower reliability on code-review skill pulls it below the threshold.
Execution modes (detail)
- Single-model — top scorer wins; one invocation; simplest path; recorded in ζ as
{"routing_mode": "single", "model_id": "..."}. - Ensemble — top 3 scorers all invoked in parallel; outputs arrive independently; β’s VERIFY step (or a designated judge model) adjudicates. Used when correctness matters more than cost (e.g. high-stakes verification).
- Pipeline — stage 1 fast model triages (classify / extract / decompose); stage 2 precise model handles the reduced task. Stage 1 output feeds stage 2 as structured input. Used when full-input token count would blow past the high-precision model’s context or budget.
Phase 0 runs only single-model. Phase 1.5+ enables ensemble and pipeline via the routing_mode column populated by δ’s rule evaluation.
Feedback loop
Post-task outcome feeds back into the scoring weights (specifically reliability and cost_efficiency) via a retrospective update:
new_weight = old_weight × (1 + 0.05 × feedback_delta)
new_weight = clamp(new_weight, 0.5, 1.5)
feedback_delta ∈ [−1, +1] where +1 = task succeeded under budget; −1 = failed outright. Clamp to [0.5, 1.5] prevents runaway accumulation — a model has a bounded window within which its score can drift before a governance event is required to adjust the floor/ceiling via π.
Fallback chain
When a chosen model is unavailable — rate-limited, deprecated mid-task, network-failed — δ walks a fallback chain. The chain is ordered: an operator declares a priority list; δ attempts models in order; each attempt logs a decision-type thought record.
Fallback rules in the target shape:
- Primary fails (timeout, rate-limit, 5xx, malformed response) → try 2nd-ranked score.
- 2nd fails → try 3rd.
- Beyond 2 fallbacks → escalate to β, which may reject the task with a typed
ModelUnavailableerror or re-queue for a later epoch.
Each fallback attempt is its own ζ record, so an audit can reconstruct the cascade: which model was first choice, which timed out, which eventually answered.
In Phase 0 the fallback chain has one entry: Claude. If Claude is unavailable, the task fails with a typed ModelUnavailable error; there is no silent downgrade.
Admission interaction
Before δ scores candidates, κ’s admission layer runs per candidate. A candidate that fails admission (e.g. exceeds the caller’s budget cap, or violates a governance freeze on a specific provider) is removed from the pool. Admission failure is not scored against; a rejected candidate is simply absent.
Concretely:
eligible = []
for m in model_candidates.where(enabled=true):
verdict = kappa.admit(caller=task.actor, tool="model:"+m.model_id, ctx=task.context)
if verdict == ADMISSION_ACCEPTED:
eligible.append(m)
if eligible.is_empty():
emit ζ {"routing_mode": "fail", "reason": "no_eligible_models"}
return ModelUnavailable
chosen = argmax(score(m, task) for m in eligible)
The interaction is deliberately ordered: κ filters first, δ scores second. A model that cannot run at all for this caller should never contribute to a score comparison.
Decision-trail recording
Every routing decision — primary pick, each fallback attempt, ensemble participants, pipeline stage boundaries — emits a ζ record so the decision is reconstructable. Minimum shape:
{
"type": "routing_decision",
"routing_mode": "single" | "ensemble" | "pipeline" | "fail",
"chosen_model_id": "claude-sonnet-3.5",
"candidates_considered": ["claude-sonnet-3.5", "gpt-4o", "claude-haiku-3.5"],
"scores": {"claude-sonnet-3.5": 0.87, "gpt-4o": 0.79, "claude-haiku-3.5": 0.58},
"fallback_attempts": 0,
"rule_version_hash": "rv:sha256:...",
"decision_hash": "SHA-256(inputs || chosen)"
}
The decision_hash is load-bearing for θ consensus: arbiters include routing-decision hashes in the Merkle root, so two arbiters with the same κ rule version and the same task context must arrive at the same model pick. δ is deterministic by construction — no per-invocation randomness enters scoring.
What δ is not
- Not a prompter. δ picks a model; it does not write the prompt. Prompt assembly happens in the skill invocation layer (ε + β dispatch).
- Not a learner. δ’s scoring is deterministic: the same inputs produce the same model. Per-task feedback updates λ reputation, which indirectly reshapes future δ scores, but δ itself is stateless.
- Not a multiplexer. When pipeline mode is in use, δ picks an ordered pair of models; at any instant, exactly one model is running a given sub-step.
Phase 0 posture
- Library-only stubs shipped in R75 Wave I (PR #149 scoring, PR #150 fallback).
colibri_code: partial— the interface exists but the scoring factors, multi-model fallback chain, and circuit breaker are Phase 1.5 work. - Router decision is a hardcoded constant:
route(task) → "claude". Implemented insrc/domains/router/scoring.ts(scoring returns{ claude: 1.0 }for every input). - Fallback chain has one member (Claude). Implemented in
src/domains/router/fallback.ts. If Claude fails the call fails — no cascade, no circuit breaker in Phase 0. mcp_model_candidatestable is defined and populated with a single row (Claude Sonnet); the table is not mutated by any Phase 0 code path.- No δ-facing tool in the 14-tool Phase 0 surface (ADR-004). δ is consulted internally by β’s dispatch layer;
router_*tools are deferred to Phase 1.5. - The routing decision is still logged to ζ so the audit trail is structured correctly for multi-model’s arrival.
- First real δ activation target: R91+ (Phase 1.5) per
../../5-time/roadmap.md; follows ADR-005.
See also
identity.md— ξ, whose domain profile δ consultsreputation.md— λ, whose execution score feeds δ’s reliability weight../execution/decision-trail.md— ζ, where every routing decision is logged../execution/task-pipeline.md— β, whose dispatch layer calls δ../execution/skill-registry.md— ε, whose skills constrain the model pool../physics/laws/rule-engine.md— κ, where δ’s scoring weights live../../architecture/decisions/ADR-005-multi-model-defer.md— why δ is Claude-only in Phase 0