[BUG]
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Bug: mid-conversation system role messages break third-party Anthropic-compatible backends (llama.cpp) — every request 400s
Environment
- Claude Code version: 2.1.208
- Platform: Linux (Arch, kernel 7.1.3-arch1-2)
- Backend: llama.cpp
llama-serverbuild b9860,ANTHROPIC_BASE_URL=http://127.0.0.1:8080/ - Model: ThinkingCap-Qwen3.6-27B (GGUF, Qwen-family chat template)
Summary
Recent Claude Code versions inject harness context (the available-agents list for the Agent tool, the available-skills list for the Skill tool) as a {"role": "system", ...} message placed after the user turn in messages[]. This happens on effectively every request — including the very first turn of a brand-new claude -p session — and regardless of which model is configured.
Per Anthropic's own documentation, mid-conversation system messages are supported on Claude Opus 4.8 only, and the documented behavior for unsupported models is a 400 with a fallback to embedding the content as a <system-reminder> block inside the user turn. Claude Code does not appear to apply any such gating or fallback when ANTHROPIC_BASE_URL points at a third-party backend.
The result: Claude Code is unusable against llama.cpp (and likely other Anthropic-compatible backends) with any model whose chat template enforces the near-universal "system message must be first" convention. Qwen-family templates (and many others) contain an explicit raise_exception('System message must be at the beginning.'), so every single request fails with a 400 before the model ever runs.
Evidence: captured request body
I captured the raw request Claude Code 2.1.208 sends via a logging proxy. For a fresh claude -p "Say hi." (no prior conversation), the messages array is:
messages roles: ['user', 'system']
messages[0](roleuser): a<system-reminder>block with date/context, plus the actual prompt text.messages[1](rolesystem): a<system-reminder>block listing the available agent types for the Agent tool (or, if the Agent tool is disallowed, the available skills for the Skill tool — disallowing tools just swaps which reminder gets injected as a trailing system message).
So a non-leading system role message is present on the first request of a session, before any assistant turn exists.
What I ruled out
- Not adaptive thinking / not beta request fields. Direct curl tests against the same backend with
thinking: {"type": "adaptive"}, tools, and system-array-with-cache_control all succeed. Only the trailingrole: "system"message triggers the failure. CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1does not help. Verified via capture: the trailing system message is still sent (it removescontext_management, but the message roles remain['user', 'system']).- No env var or flag I could find suppresses the injection.
Why the available workarounds are not acceptable
--chat-template chatml: llama.cpp then can't generate the model's native tool-call parser, so Claude Code's tool calls no longer parse — the agent is broken in a different way.--no-jinja: same problem; tool calling doesn't work with Claude Code.- Manually patching each model's embedded chat template (extract via
/props, replace theraise_exceptionbranch with a rendered<|im_start|>systemturn, reload via--chat-template-file): this works — I verified a full end-to-end Claude Code session with working tool calls — but it is per-model surgery that every llama.cpp user would have to perform on every model they use.
What Should Happen?
Expected behavior (suggested fixes, any one of which would resolve this)
- Gate the
role: "system"injection on models/hosts documented to support mid-conversation system messages (per Anthropic docs: Claude Opus 4.8 only), and fall back to the previous behavior —<system-reminder>text inside the user turn — everywhere else. - Detect the 400 and retry with the fallback (the documented pattern for
role 'system' is not supported on this modelerrors), tolerating third-party error text. - At minimum, provide an env var (e.g.
CLAUDE_CODE_DISABLE_MID_CONVERSATION_SYSTEM=1) so gateway/local-LLM users can restore the user-turn injection behavior.
Error Messages/Logs
## Debug log excerpt
[DEBUG] [API:timing] dispatching to firstParty model=claude-sonnet-5
[DEBUG] [API REQUEST] /v1/messages source=sdk
[ERROR] API error (attempt 1/11): 400 400 {"error":{"code":400,"message":"Unable to generate parser for this template. Automatic parser generation failed: \n------------\nWhile executing CallExpression at line 85, column 32 in source:\n...first %}↵ {{- raise_exception('System message must be at the beginnin...\n ^\nError: Jinja Exception: System message must be at the beginning.","type":"invalid_request_error"}}
Steps to Reproduce
Reproduction
- Start llama.cpp with any Qwen-family GGUF:
````
llama-server --host 0.0.0.0 -m model.gguf -c 64000
- Run the most trivial possible Claude Code command against it:
````
ANTHROPIC_BASE_URL="http://127.0.0.1:8080/" ANTHROPIC_API_KEY="x" claude -p "Say hi."
- Every request fails immediately:
````
API Error: 400 Unable to generate parser for this template. Automatic parser generation failed:
------------
While executing CallExpression at line 85, column 32 in source:
...first %}↵ {{- raise_exception('System message must be at the beginnin...
^
Error: Jinja Exception: System message must be at the beginning.
Claude Model
Other
Is this a regression?
Yes, this worked in a previous version
Last Working Version
Sometime within the week.
Claude Code Version
2.1.208
Platform
Other
Operating System
Other Linux
Terminal/Shell
Other
Additional Information
_No response_