[BUG]

Open 💬 0 comments Opened Jul 15, 2026 by linuxid10t

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-server build 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] (role user): a <system-reminder> block with date/context, plus the actual prompt text.
  • messages[1] (role system): 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 trailing role: "system" message triggers the failure.
  • CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 does not help. Verified via capture: the trailing system message is still sent (it removes context_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 the raise_exception branch with a rendered <|im_start|>system turn, 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)

  1. 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.
  2. Detect the 400 and retry with the fallback (the documented pattern for role 'system' is not supported on this model errors), tolerating third-party error text.
  3. 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

  1. Start llama.cpp with any Qwen-family GGUF:

``
llama-server --host 0.0.0.0 -m model.gguf -c 64000
``

  1. 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."
``

  1. 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_

View original on GitHub ↗