fix(anthropic): preserve midturn system corrections - #34290
Conversation
Greptile SummaryPreserves in-sequence Anthropic system corrections throughout translation and guardrail processing.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/chat/guardrail_translation/handler.py | Separates trusted top-level prompts from midturn system input during guardrail extraction and preserves supported corrections during structured-message rewrites. |
| litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py | Adds Chat Completions translation for supported in-sequence system content while retaining its system role. |
| litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py | Maps supported in-sequence system content to Responses message items with input-text parts. |
| litellm/types/llms/anthropic.py | Introduces pass-through-specific typing for Anthropic in-sequence system entries. |
| tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py | Adds extensive coverage for extraction, masking, structured rewrites, compaction, cache metadata, and tool-exchange handling. |
Reviews (7): Last reviewed commit: "fix(anthropic): keep tool exchanges inta..." | Re-trigger Greptile
|
CI appears blocked by the stale daily base rather than this PR. The latest available branch is litellm_oss_daily_2026_07_20; neither the July 21 nor July 22 daily branch was created. The OpenAPI queued enum update and OSV dependency upgrades are already fixed on main. Could a maintainer cut the current daily OSS branch? I’ll rebase this PR once it’s available. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
daf3aeb to
a5154d0
Compare
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6b6f9f8. Configure here.
4f156cb to
2e47ad8
Compare
Generated with AI Co-Authored-By: Claude Code
2e47ad8 to
bae58eb
Compare
…urn_interjections
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b9b200b. Configure here.
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution!
1a8cd8a
into
BerriAI:litellm_internal_staging
Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) 400 with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM=true: system entries after index 0 are rewritten as user rows after the top-level system param is prepended. Default behavior unchanged (upstream preserves mid-turn system rows deliberately, PR BerriAI#34290).
Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) 400 with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM=true: system entries after index 0 are rewritten as user rows after the top-level system param is prepended. Default behavior unchanged (upstream preserves mid-turn system rows deliberately, PR BerriAI#34290).
Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) reject them with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM=true: system entries after index 0 are rewritten as user rows after the top-level system param is prepended. Default behavior unchanged: upstream preserves mid-turn system rows deliberately (PR BerriAI#34290), so this stays opt-in. (cherry picked from commit 625c8b3)
…m messages Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) reject them with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM: "true" (alias "demote") rewrites system entries after index 0 as user rows after the top-level system param is prepended; "drop" removes them entirely for backends where the reminder content is not wanted. Default behavior unchanged: upstream preserves mid-turn system rows deliberately (PR BerriAI#34290), so this stays opt-in.
…m messages Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) reject them with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM: "true" (alias "demote") rewrites system entries after index 0 as user rows after the top-level system param is prepended; "drop" removes them entirely for backends where the reminder content is not wanted. Default behavior unchanged: upstream preserves mid-turn system rows deliberately (PR BerriAI#34290), so this stays opt-in.
…m messages Clients like Claude Code send system-role reminder messages mid conversation. OpenAI accepts those anywhere, but OpenAI-compatible backends with strict chat templates (Qwen3 on vLLM) reject them with "System message must be at the beginning." Gated on LITELLM_DEMOTE_MIDTURN_SYSTEM: "true" (alias "demote") rewrites system entries after index 0 as user rows after the top-level system param is prepended; "drop" removes them entirely for backends where the reminder content is not wanted. Default behavior unchanged: upstream preserves mid-turn system rows deliberately (PR BerriAI#34290), so this stays opt-in.

TLDR
Claude Code can insert a correction as an in-sequence
systemmessage while a tool call is running. LiteLLM's Anthropic pass-through translators previously dropped that message, so the model continued with stale instructions.This change:
systemmessages at their exact position when translating to OpenAI Chat Completions and Responses.system; it does not convert them touser./v1/messagesguardrails.User Flow
Before: the correction the user types mid-task never reaches the model, so the agent keeps executing the stale plan.
ANTHROPIC_BASE_URL=https://litellm-domain,ANTHROPIC_AUTH_TOKEN=<litellm key>) with their model set to an OpenAI-backed deployment, and launchesclaude.POST https://litellm-domain/v1/messagesthat returns tool calls the agent then runs.POST https://litellm-domain/v1/messagesas an in-sequencesystementry at the point in the conversation where it was typed.200reply continues the original task as if nothing was said: the gateway dropped the mid-turnsystementry while translating the request for the OpenAI backend, so the model never saw the correction.After: the same correction reaches the model at the same point in the conversation, and the agent changes course.
ANTHROPIC_BASE_URL=https://litellm-domain,ANTHROPIC_AUTH_TOKEN=<litellm key>) with their model set to an OpenAI-backed deployment, and launchesclaude.POST https://litellm-domain/v1/messagesthat returns tool calls the agent then runs.POST https://litellm-domain/v1/messagesas an in-sequencesystementry at the point in the conversation where it was typed.200reply acknowledges the correction and switches course: the mid-turnsystementry is delivered to the OpenAI backend as asystemmessage at the same position, kept separate from the trusted top-level system prompt.Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you are seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Behavior
OpenAI Chat Completions
A valid in-sequence Anthropic
systementry becomes an OpenAIsystemmessage at the same sequence position. String and text-block content are supported. Text-blockcache_controlmetadata follows the adapter's existing best-effort model-name handling.OpenAI Responses
A valid in-sequence Anthropic
systementry becomes a Responsesmessageitem withrole: "system"andinput_textcontent at the same position. Responses input text has no equivalentcache_controlfield, so that metadata is not forwarded.Both translators omit empty, malformed, image-only, and otherwise unsupported system content.
Anthropic guardrails and compaction
For
/v1/messages, the top-levelsystemfield remains the trusted prompt. In-sequencesystementries are untrusted client input and are always included in guardrail text extraction andstructured_messages, even whenskip_system_message_in_guardrailskips the top-level prompt.Guardrail write-back removes only the actual hoisted top-level prompt and preserves other in-sequence system entries around converted non-system runs. A system entry that lands between an assistant tool call and its tool results is emitted right after that exchange completes, so the call/result pair always converts together. Compaction token counting and summary construction also retain corrections in sequence.
Scope
This PR does not add provider detection, provider-specific rejection, fallback behavior, or changes to native Anthropic/Bedrock system-message handling. It defines preservation for the direct OpenAI Chat Completions and Responses translations and keeps corrections available through guardrail processing.
Type
Bug Fix
Tests
Final local verification on the affected files:
315 passedgit diff --checkpassedFinal Attestation
Screenshots / Proof of Fix
E2E QA at PR head b9b200b (after) vs merge base 79d412e (before): the real Claude Code CLI v2.1.227 TUI, driven interactively under tmux, pointed at a LiteLLM proxy booted from each commit (own worktree, own venv, DB-less config,
--detailed_debugas wire capture), modelgpt-4ovia the real OpenAI API, no mocks. Both legs ran the identical User Flow: ask the agent to run a slow foreground Bash command then write a haiku, and while the tool is still running type "Forget the haiku completely. Reply with exactly: CORRECTION RECEIVED - PINEAPPLE". Claude Code queued the correction and delivered it in the nextPOST /v1/messagesas an in-sequencesystementry right after thetool_result, exactly as this PR's TLDR claimsBefore (79d412e): the correction is silently dropped and the user gets the stale haiku
What the end user saw in the TUI:
On the wire, the incoming
/v1/messagescarried five messages ending in{'role': 'system', 'content': [{'type': 'text', 'text': 'The user sent a new message while you were working:\nForget the haiku completely. Reply with exactly: CORRECTION RECEIVED - PINEAPPLE ...'}]}, but the outgoingPOST https://api.openai.com/v1/responsesbody contained zero occurrences of "PINEAPPLE" and zerosystem(ordeveloper) roles: itsinputended at thefunction_call_outputand both in-sequence system entries (a SessionStart hook entry and the correction) were gone, with HTTP 200 and no warningAfter (b9b200b): the same correction reaches OpenAI in place and the model obeys it
What the end user saw in the TUI (this leg's Claude Code session refused foreground
sleepwith a tool error, so the slow foreground command was a 60s python sleep instead, which changes nothing about the claim under test):Same incoming shape (five messages, correction as the in-sequence
systementry right after thetool_result), and the outgoing OpenAI Responses request preserved all five positions:input[4] = {'type': 'message', 'role': 'system', 'content': [{'type': 'input_text', 'text': 'The user sent a new message while you were working:\nForget the haiku completely. Reply with exactly: CORRECTION RECEIVED - PINEAPPLE ...'}]}directly after thefunction_call_output, the SessionStart hook system entry (string content) preserved asinput[1], and the top-level Claude Code system prompt sent exactly once asinstructions. OpenAI streamed back exactlyCORRECTION RECEIVED - PINEAPPLECaveats observed during QA (facts from the runs, not speculation from the diff):
thinkingby default, the Anthropic to Responses bridge maps it toreasoning.effort, and gpt-4o rejects that parameter; both legs needed client-sideMAX_THINKING_TOKENS=0, anddrop_params: truedid not help on this path. Identical before and after, so this PR leaves it alonerun_in_backgroundfor the Bash tool (which ends the turn immediately), and one leg's Claude Code session hard-blocked foregroundsleepwhile the concurrent other leg ran the identical command fine