Sonnet 4.6 substitutes `gpt-4.1-mini` for `gpt-5.4-mini` in emitted tool_use commands (~11.5% rate)
Sonnet 4.6 substitutes gpt-4.1-mini for gpt-5.4-mini when emitting tool_use commands
Summary
Claude Sonnet 4.6 rewrites the model-name string gpt-5.4-mini as gpt-4.1-mini in emitted tool_use inputs, even when the source skill / user prompt clearly and repeatedly says gpt-5.4-mini. The substitution happens at generation time — the upstream skill text arrives in context correctly; the downstream tool_use block carries the wrong name.
In the repro, the tool the model is invoking is bramble code-review, a thin CLI that forwards --backend codex --model <name> to the OpenAI Codex endpoint and records findings. It does no string rewriting on the --model value — it just passes the argument through — so OpenAI is the one that returns HTTP 400: "The 'gpt-4.1-mini' model is not supported when using Codex with a ChatGPT account." The bramble layer is not contributing to the bug; it's simply the first place downstream where the wrong name becomes observable.
Reproduces on
- Claude Code CLI: 2.1.116
- Model: claude-sonnet-4-6 (every real case observed)
- Tool involved:
Monitor(background tool call); the same substitution could plausibly surface in any tool_use that takes a shell command string.
Not observed on Opus 4.7.
Minimal reproduction from one session
Session JSONL (sanitized excerpts below), session id 193fc056-…, agent model claude-sonnet-4-6.
L5 — system/skill load, correct name
...
bramble code-review --backend codex --model gpt-5.4-mini \
--goal "{PR_SUMMARY}" --skip-test-execution \
--verbose --timeout 10m --envelope-file "$ENVELOPE_CODEX"
...
L102 — assistant-emitted tool_use, wrong name
{
"type": "tool_use",
"name": "Monitor",
"input": {
"description": "bramble codex r1 for PR …",
"timeout_ms": 720000,
"persistent": false,
"command": "WORK_DIR=$(pwd) bramble code-review \\\n --backend codex --model gpt-4.1-mini \\\n --goal \"…\" --skip-test-execution \\\n --verbose --timeout 10m --envelope-file \"$ENVELOPE_CODEX\""
}
}
L106 — downstream tool result, the 400
{
"type": "error",
"status": 400,
"error": {
"type": "invalid_request_error",
"message": "The 'gpt-4.1-mini' model is not supported when using Codex with a ChatGPT account."
}
}
The substitution is entirely within Claude's generation: the input context has gpt-5.4-mini verbatim, the tool_use output has gpt-4.1-mini.
What I've ruled out
- Not the skill text. The source markdown consistently says
gpt-5.4-mini; verified present in L5 of each repro session. - Not an SDK / transport rewrite. Tool inputs are forwarded as-is; no component between the model and the tool dispatcher touches the
commandstring. - Not a user typo. The correct name flows through cleanly in the 185 sessions where substitution did not occur.
Likely cause (speculation): gpt-4.1-mini is a real, well-known OpenAI model name in training data; gpt-5.4-mini looks similar but is a bramble-internal alias. The model appears to favor the training-distribution-common identifier when emitting what it treats as a "plausible OpenAI model name" inside a shell command.
Attachment available on request
I have a 5-message redacted JSONL extract from the reproducing session (session id 193fc056-…): original L5 (skill load with correct gpt-5.4-mini), L102 (assistant tool_use Monitor with wrong gpt-4.1-mini), L106 (the OpenAI 400 error event), L107 (terminal task-notification), L108 (next assistant turn).
All repo/org/ticket/PR identifiers replaced with placeholders. The --goal argument to bramble is elided since it's not relevant to the bug. Preserved unchanged: Claude model IDs, tool names, the two model-name strings at the heart of the bug, OpenAI error body, token counts, timestamps.
Happy to share it as a gist or inline excerpt — let me know which works best.
Related repos / context
- My Claude-SDK wrapper + orchestrator work lives at https://github.com/bazelment/yoloswe (public) — that repo calls the CLI via
claude-agent-sdk-shaped interfaces. No repo-side change would prevent this: the substitution is upstream of anything that repo sees.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗