Workflow agent() model silently replaced by CLAUDE_CODE_SUBAGENT_MODEL when the id is not in the gateway-discovered model list
Workflow agent() model silently replaced by CLAUDE_CODE_SUBAGENT_MODEL when the id is not in the gateway-discovered model list
Environment
- Claude Code 2.1.245 (also observed on 2.1.226), macOS (darwin arm64)
- Custom gateway:
ANTHROPIC_BASE_URLpointed at a self-hosted Anthropic-wire proxy,ANTHROPIC_AUTH_TOKENset,CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 CLAUDE_CODE_SUBAGENT_MODELset to a gateway-advertised id (claude-delegate-capable)
What happens
With gateway model discovery enabled, Claude Code fetches GET /v1/models from the gateway and treats the returned list as the exhaustive vocabulary for subagent model resolution (Workflow agent() model: opts). Any requested model id absent from that list is silently replaced with CLAUDE_CODE_SUBAGENT_MODEL before the request leaves the machine:
- No error, no warning, no UI indication at call time.
- The subagent's
agent-*.meta.jsonrecords only the requested id (requestedModel), which actively misleads post-hoc inspection — the transcript's served model is the only local trace, and nothing correlates the two. - The substituted request reaches the wire as the fallback model. Verified at the gateway with a live SSE tap of per-request events: agents whose
agent()opts requestedclaude-fable-5,claude-fable-5[1m], and (in a later repro) a class id added to the gateway after session start all arrived asmodel_requested=claude-delegate-capable.
The main-loop model is unaffected (passes through verbatim); only subagent resolution is gated this way.
Reproduction
- Point Claude Code at any Anthropic-compatible gateway whose
/v1/modelsreturns a partial model list (e.g. it omitsclaude-opus-5while happily routing it on/v1/messages). SetCLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1and aCLAUDE_CODE_SUBAGENT_MODELthat is advertised. - Run a Workflow with
agent('say hi', {model: 'claude-opus-5'})(any valid id not in the discovery list). - Observe on the gateway that the request arrives with the
CLAUDE_CODE_SUBAGENT_MODELvalue instead; observe locally that no error or warning was emitted and the agent meta records the requested id.
A second wrinkle makes this sharper: the discovery list appears to be fetched once per session. A model added to the gateway's /v1/models after session start keeps being silently substituted for the rest of that session, so even a "fixed" gateway doesn't stop the behavior for running sessions.
Impact
A multi-day silent failure in our case: an adversarial-review stage explicitly specified as "strongest model" ran on a different (cheaper) model class for three days across three sessions. Nothing surfaced it; diagnosis required tapping the gateway's event stream and hand-correlating subagent transcripts. The failure mode is the worst kind for model-quality work — outputs remain plausible, so nothing looks wrong.
Expected behavior
Gating subagent models on the discovered list is a defensible safety choice (an unknown id might 404). The defect is the silence. Any of these would be fine:
- Refuse: fail the
agent()call with a clear error naming the unadvertised id, or - Honor: pass the id through and let the gateway decide (it may route ids it doesn't advertise), or
- Substitute loudly: keep the fallback but emit a visible warning and record both requested and resolved ids in the subagent metadata.
Silent substitution converts a configuration gap into invisible model downgrades.