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")
  • provider
  • context_window_tokens
  • latency_tier (fast | balanced | slow)
  • cost_bps_per_kilotoken
  • domain_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:

  1. Single-model — one model executes the whole task.
  2. Ensemble — multiple models run independently; their outputs are judged by a separate model or by β’s VERIFY step.
  3. 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)

  1. Single-model — top scorer wins; one invocation; simplest path; recorded in ζ as {"routing_mode": "single", "model_id": "..."}.
  2. 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).
  3. 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 ModelUnavailable error 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 in src/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_candidates table 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


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.