Control / Model routing and host identity

Control

Model routing and host identity

How Guild Stack decides which model a run uses — per-run host identity, evidence-graded catalogs, purpose floors, and a read-only inspection command.
Last verified
Aug 2, 2026

Page evidence and guidance

Read as StartReading the inspection output and trusting the honest unknowns it prints. SteerExecution targets, evidence grades, purpose floors, the rollout flags, and per-leg rollback. ExtendThe four routing contracts, discovery adapter boundaries, and the two-axis independence rule.

Use this page to answer three questions about a run: which host and execution target it is bound to, which models that target can actually be asked for, and why a given model was chosen.

Try it: /guild:models inspect prints the whole picture for a run and writes nothing.

What shipped, and what has not

Model routing lands in milestones. Read this table before anything else on the page — the Status column is the current shipped posture, not a roadmap.

Milestones were designed to be independently reversible by a flag in .guild/settings.json, but only some of that shipped: model_routing.discovery, model_routing.inspect, model_routing.shadow, and model_routing.enabled really do gate behaviour, while model_routing.identity_v2, model_routing.binding_enforce, and teams.proposal_v2 are declared but not implemented — turning them off changes nothing. See Rolling back below.

MilestoneStatusWhat it does
M0 — inspect-onlyShipped / live, on by default — inspect-only; 2 of its 5 rollback legs are wired (discovery, inspect)Per-run host identity, fail-closed run binding, discovery adapters + cache, the uncapped team proposal path, and the read-only /guild:models inspect surface. Changes no model selection anywhere.
M1 — shadowShadow — gated, off by default. Not part of the default postureRuns the resolver alongside your real dispatch and records what it would have chosen. Still no effect on the dispatch.
M2 — opt-in routingOpt-in, OFF by default — NOT the active selection posture, not released. Turning it on also requires evidenced M0 inspection + M1 shadow artifactsThe v2 policy actually selects the model, per project or per run.
M3 — default onPlanned / deferred — NOT shippedDeferred. No released build turns it on.

If you have never edited .guild/settings.json, you are on M0: routing is observable but not yet deciding.

A model listing is not an entitlement

The rule the whole design rests on: a model appearing on a provider’s public page, in a static registry, or in an unauthenticated list is not proof that your account can dispatch it today. Every catalog row therefore carries an evidence grade.

EvidenceMeansWhere it can come from
availableThis authenticated target can dispatch this model for this account.An authenticated list whose published contract states account availability (today only the Claude API GET /v1/models), or real dispatch evidence from this exact target.
advertisedThe target listed the model. Entitlement is unproven.codex debug models, the Codex app-server model/list, account-scoped pickers whose contract says nothing about availability.
unknownNo usable evidence — the honest answer for a target with no discovery surface.The Claude desktop app and web surface, and the Bedrock / Vertex / Foundry gateways before a first dispatch.
unavailableA dispatch failed with model-not-found or permission-denied here.Dispatch evidence only.

Eligibility is then closed and purpose-aware. Research, advisory, adversarial, and security work require available. Ordinary implementation work may attempt an advertised model only when you opt in with allow_advertised_attempt: true. unknown and unavailable are never eligible.

Static registry rows and built-in mappings can fill in missing metadata. They can never promote a row to available.

Targets, not hosts

A host family is not the unit of routing. The execution target is, exactly: host.family + host.surface + provider_kind + auth_mode + account_fingerprint + endpoint_fingerprint + org_fingerprint. Those seven fields are the whole tuple. The same CLI binary under two auth modes is two targets, and neither inherits the other’s entitlement; org_fingerprint is in the tuple so one org’s entitlement is never projected onto another’s.

The catalog cache key is a different, larger thing — don’t confuse the two. It hashes the seven tuple fields plus target_id, tool_version, adapter_id, adapter_version, and run_scope. Tool and adapter versions extend the cache key; they are not a substitute for the target’s org scope. When org_fingerprint is unknown, run_scope becomes the run id, so the entry is run-local and never reused across runs.

  • claude-cli-subscription — the picker is interactive-only, so entitlement cannot be proven before a dispatch; rows stay advertised or unknown until an ordinary dispatch upgrades them.
  • claude-app and claude-webno programmatic discovery surface exists. The catalog reports unknown rather than borrowing the API listing.
  • claude-api — the one surface today whose contract states account availability.
  • claude-gateway-bedrock / -vertex / -foundry — exact rows, gateway-native evidence only, honest unknown until dispatch evidence upgrades them.
  • codex-cli-chatgptcodex debug models behind a versioned adapter; advertised at best.
  • codex-app-server — the richest Codex surface (provider-ordered reasoning efforts, visibility metadata) but with undefined entitlement semantics, so still advertised.
  • codex-cli-api-key and openai-api — separate targets again.

Discovery is read-only, never issues a paid inference request, is bounded at three seconds, degrades to the newest valid cache or an honest unknown, and never puts credentials into a cache, receipt, or log.

Identity is per run, and unknown is safe

Each run gets one immutable identity record: host family, surface, adapter, target tuple, and a trust value of asserted or verified.

  • Unset, malformed, or unrecognized identity resolves to unknown. It never falls back to Claude.
  • Precedence is not trust. GUILD_HOST_ID outranks adapter injection for precedence, but it stays asserted until a handshake verifies it.
  • There is no workspace-wide “current host”. Two sessions in one project produce two distinct records, and a runtime write with a missing or mismatched run binding fails closed rather than landing in the wrong run.

Purpose floors

Complexity stays easy | medium | hard, but the purpose of the work can raise the floor and can never be talked below it.

  • purpose: research always resolves hard and the powerful tier, even when a caller, lane, or heuristic labels the task easy. The receipt records forced_floor_reason: research_always_hard.
  • The floor is inherited by nested delegations, retries, and resumes. A child can raise a floor; it cannot lower one.
  • Research-shaped work with no authoritative purpose metadata fails closed with purpose_unbound instead of being treated as general work.
  • Security review pins the exact model id rather than an alias, because alias drift is real — the opus alias has already moved to a newer model, so a security route that resolved through an alias could quietly change model.

Strong versus weak review

Cross-family adversarial review claims the reviewer is independent of the producer. That claim is checked on two axes, stored separately and never compared across each other: host_family (the verified dispatch substrate) and model_family (the family of the model actually served).

A strong verdict needs verified identity on both sides, known and different values on both axes, and a bound actual served model on both finalized receipts. Everything else is weak — including a different host family with asserted identity, and including a provider that will not report which model answered. A weak review still runs and is still useful; it just cannot sign off as strong.

Today: the check is shipped and the degradation is correct, but no production path yet writes the finalized served-model evidence a strong verdict requires, so real runs report weak or unknown. That is the safe direction, and /guild:models inspect says so rather than implying a sign-off you do not have.

Inspect a run

/guild:models inspect                     # the candidate run, labelled as such
/guild:models inspect --run-id <id>       # pin a run
/guild:models inspect --json              # same view model, machine readable

It writes no file, starts no run, discovers nothing, and resolves no policy. It reports host and identity trust, the target, catalog age and per-model evidence state, the policy rule path, the selection, the frozen fallback chain, the outcome with the actual model, the review independence verdict, any degradation, and a list of every field it could not bind.

Three reading rules:

  • unknown is a finding, not a formatting artifact. Do not fill one in from context or from the requested model.
  • asserted is not verified. An asserted identity can never back a strong-independence claim.
  • Exit code 3 means it refused. A field or the whole report tripped the redaction check. The refusal is the output; the value is not reconstructed.

Every row is rebuilt from artifacts that verify themselves. A persisted inspection file is treated as an untrusted pointer, and a claim it makes that the evidence does not support is printed as an explicit untrusted-claim warning naming the fields.

Teams are no longer capped

The same rollout retired the old six-specialist cap and the three-to-four default target. There is no size cap, no default target size, and no path that drops a role to make a team fit.

  • The composer proposes every specialist it can justify, each with a stated reason and the obligations it owns, plus the roles it considered and rejected.
  • The proposal is an approval artifact. Nothing dispatches until you approve it, and any edit produces a new proposal needing fresh approval. Omitting a participant is never the same as removing one.
  • When the backend cannot run everyone at once, the work is split into dependency-ordered waves. Capacity changes the schedule, never the roster.
  • --team-size=N and defaults.team.size are migration-window concurrency hints clamped by real backend capacity. allow_larger does nothing and warns.

Rolling back

Seven closed keys in .guild/settings.json. No flag rewrites your existing settings bytes. An explicit off only wins on the four keys that are actually wired — the other three are declared but not implemented, so an off on them wins nothing.

Four of the seven keys actually do something; three do not yet. discovery, inspect, shadow, and enabled each have code that reads them, so turning one off really changes behaviour. identity_v2, binding_enforce, and teams.proposal_v2 are validated and printed by /guild:models inspect, but nothing in Guild branches on their value — turning them off today does nothing. Those rollback paths are not implemented yet; we would rather say so than let you plan a rollback around a switch that is not connected.

KeyDefaultWired?Turning it off, today
model_routing.discoveryonYesAdapters and cache writes stop; inspection reports “discovery disabled” rather than a stale claim.
model_routing.inspectonYesThe inspection surface disappears and no report is produced; nothing else changes.
model_routing.shadowoffYesShadow comparison does not run. On, it writes run-local records only.
model_routing.enabledoffYesv2 selection never engages; your settings bytes are untouched and frozen run snapshots stay valid.
model_routing.identity_v2onNoNothing — not implemented. Unknown-safe identity is unconditional; the old Claude-by-default behaviour was removed, not put behind this switch.
model_routing.binding_enforceonNoNothing — not implemented. Fail-closed run binding is unconditional; there is no observe-only mode to fall back to.
teams.proposal_v2onNoNothing — not implemented. The v2 composer is the only composer, and the old cap no longer exists in the code, so there is nothing for an off to restore.

These keys are read directly from .guild/settings.json and are not scaffolded by /guild:config init yet — set them by hand. A value other than "on" or "off" is rejected and the flag keeps its default.

Legacy tier maps keep working through a two-release compatibility window: they are read as lower-precedence preferences, never as availability truth, they never fill a research or review purpose, and nothing is written back to your settings file.

Known limits

  • No production path publishes a catalog snapshot yet, so a run today reports an absent catalog and an opt-in resolve fails closed rather than choosing on no evidence.
  • Strong review independence cannot yet be produced end to end, for the same reason.
  • The published performance figures (cached inspection under 500 ms at p95, a three-second discovery budget) are measured on local fixtures with no network. Treat them as budgets, not as a network guarantee.
  • Rejecting the legacy sizing keys after the compatibility window is designed but not yet implemented. The window has not started, so the gap is not reachable in a released build.