anthropics / anthropics/claude-code
Workflow agent() model silently replaced by CLAUDE_CODE_SUBAGENT_MODEL when the id is not in the gateway-discovered model list
- Vorherrschende Sprache
- Python
- Sterne
- 145k
- Forks
- 23.1k
- PR-Merge-Kennzahlen
- PR-Kennzahlen ausstehend
Beschreibung
# 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_URL` pointed at a self-hosted Anthropic-wire proxy, `ANTHROPIC_AUTH_TOKEN` set, `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`
- `CLAUDE_CODE_SUBAGENT_MODEL` set 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.json` records 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 requested `claude-fable-5`, `claude-fable-5[1m]`, and (in a later repro) a class id added to the gateway after session start all arrived as `model_requested=claude-delegate-capable`.
The main-loop model is unaffected (passes through verbatim); only subagent resolution is gated this way.
## Reproduction
1. Point Claude Code at any Anthropic-compatible gateway whose `/v1/models` returns a *partial* model list (e.g. it omits `claude-opus-5` while happily routing it on `/v1/messages`). Set `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` and a `CLAUDE_CODE_SUBAGENT_MODEL` that *is* advertised.
2. Run a Workflow with `agent('say hi', {model: 'claude-opus-5'})` (any valid id not in the discovery list).
3. Observe on the gateway that the request arrives with the `CLAUDE_CODE_SUBAGENT_MODEL` value 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:
1. **Refuse**: fail the `agent()` call with a clear error naming the unadvertised id, or
2. **Honor**: pass the id through and let the gateway decide (it may route ids it doesn't advertise), or
3. **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.
Beitragsleitfaden
Für dieses Repository ist kein Beitragsleitfaden indexiert
Rechercherichtung
Start at the gateway model-discovery flow and Workflow agent() subagent model-resolution entry points, then reproduce with a partial GET /v1/models response and an unlisted model id. Trace where the requested id becomes the wire model and how agent-*.meta.json is written. Done means the chosen handling is visible and the requested and resolved models are no longer misleadingly represented.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- python
- Bereich
- ai, api
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Größtenteils klar
- Anfängerfreundlichkeit
- 45/100