fix(anthropic): fold guardrail-modified leading system rows into top-level system param - #37231
Conversation
…level system param
Greptile SummaryThe PR updates Anthropic guardrail write-back so leading system rows are folded into the provider’s top-level system parameter while mid-turn rows retain their position.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/chat/guardrail_translation/handler.py | Adds top-level folding for leading system rows and keeps the existing mid-turn and tool-exchange conversion behavior. |
| tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py | Extends guardrail rewrite coverage across masked top-level prompts, leading rows, mid-turn rows, and system-only outputs. |
Reviews (2): Last reviewed commit: "refactor(anthropic): drop bare generics ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
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 0cbec3f. Configure here.
tin-berri
left a comment
There was a problem hiding this comment.
LGTM — critical regression fix, reproduced live with the actual Claude Code TUI plus curl edge cases (leading system row, mid-turn after user/assistant, masked-prompt leak), and verified for error parity against raw api.anthropic.com. Good scope: leading system rows fold into the top-level system param while mid-turn directives keep their position, and the guardrail-masked prompt now actually reaches Anthropic instead of leaking the unmasked original. Cross-provider Bedrock check confirms the PII leak was closed there too.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a customer running a guardrail sees every Claude Code turn through the proxy fail with an Anthropic 400 after upgrading to 1.98.0
testAPI Error: 400 ... messages.0: use the top-level 'system' parameter for the initial system prompt ... Received Model Group=claude-opus-5and the turn is lost; the same happens for any client whose conversation history contains a system entry (a mid-turn directive, or a leading system row)After: the same turns return the model's answer and Anthropic receives the guardrail's version of the system prompt
Relevant issues
Regression introduced by #34290, first shipped in the 1.98.0 dev and rc images. Reported by a customer whose upgrade from 1.97.0 broke every Claude Code request through the proxy; reverting to 1.97.0 avoided it because the old write-back silently dropped system rows instead
Linear ticket
Resolves LIT-5696
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're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Live A/B against the real Anthropic API, no mocks. Both legs boot the identical proxy config and differ only in the checked-out commit. The reporter's client is Claude Code, so the first case drives the real Claude Code TUI interactively under tmux at base 973329e and tip 0cbec3f. The curl cases were captured at a972f17; commit 0cbec3f on top of it only tightens type annotations (no runtime change), so those captures stand for the tip. The config is one claude-opus-5 model plus a custom pre_call guardrail (default_on) whose apply_guardrail masks bob@example.com to
<EMAIL>in texts and in a deep copy of structured_messages, returning a new list, which is what triggers the write-back under test. The Claude Code case adds aclaude-*wildcard route and widens the mask to any email plus/Users/<name>paths so Claude Code's own system prompt gets rewritten, everything else identicalconfig.yaml and guardrail used by both legs
payloads req_leading.json, req_midturn_valid.json, req_echo.json, req_midturn.json, req_midturn_user.json, req_plain.json, req_chat.json
Before (973329e)
Claude Code first turn (reporter's client)
test, press Enter, and read the pane backrole: systemrow right after the first user turn, so the write-back treats the request as one that preserves system rows and emits the guardrail-masked system prompt intomessages[0]. The proxy's outgoing request in the--detailed_debuglog confirms it:messages[0]is{'role': 'system', ...}carrying the masked Claude Code system prompt, while the top-levelsystemlist still holds the unmasked original (the raw home-directory path appears once)Leading system row in messages
Mid-turn system after a user turn
Masked system prompt delivery
Mid-turn system after an assistant turn
Plain top-level system
Same flow on /v1/chat/completions
After (0cbec3f)
Claude Code first turn (reporter's client)
systemlist carries the masked prompt (/Users/<USER>twice, raw home-directory path zero times)The curl cases below were captured at a972f17 and carry over to 0cbec3f unchanged (annotation-only commit)
Leading system row in messages
Mid-turn system after a user turn
Masked system prompt delivery
Mid-turn system after an assistant turn
Plain top-level system
Same flow on /v1/chat/completions
Cross-provider check: the same handler serves every model behind /v1/messages, so the same config was rerun with
bedrock/us.anthropic.claude-sonnet-5at base 973329e and tip 0cbec3f (real Bedrock calls). req_leading, req_midturn_valid, and req_plain return 200 on both sides (Bedrock's own transform already hoists in-sequence system rows), and the masked-echo probe flips frombob@example.comat base to<EMAIL>at tip, so the PII leak was cross-provider and is closed cross-providerNotes observed during QA, none caused by this PR and all left alone by it:
system; pre-existingType
🐛 Bug Fix
Caveats (if any)
Final Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
0cbec3f passes /live-pr-risk