fix(guardrails): compress content-parts messages in headroom guardrail (Anthropic traffic) - #34586
Conversation
Greptile SummaryFixes Headroom compression for all-text content-part messages.
Confidence Score: 5/5The PR appears safe to merge. No blocking failures remain within the scope of the previous review threads.
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py | Adds content-part flattening and shape restoration around the existing Headroom compression call without leaving an eligible follow-up defect. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py | Adds focused tests covering all-text flattening, mixed-content preservation, rewritten cache metadata, service restructuring, and fail-open restoration. |
Reviews (4): Last reviewed commit: "fix(guardrails): compress content-parts ..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 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 5af69dd. Configure here.
|
Note on codecov/patch: the failure is a stale-base artifact, not this diff. Codecov's patch report counts missed lines in files this PR does not touch (ui_sso.py 20, saml_sso.py 17, mcp_server/server.py 12, batches_endpoints/endpoints.py 6, proxy/utils.py 5 — all from upstream litellm_internal_staging commits swept into the comparison; none appear in this PR's file list). The lines actually changed by this PR are 51/53 covered (96%), with the 2 misses being defensive branches in headroom.py. Sibling PRs opened against a fresher base show codecov/patch green. |
|
Follow-up for the same positional-breakpoint hardening in the compresr guardrail (pre-existing there, deliberately not folded into this PR): LIT-4804 |
Anthropic-format requests translate to messages whose content is a list of part dicts, which the headroom compression service's transforms silently skip (they only rewrite string content), so compression never applied to Anthropic client traffic while the guardrail still reported itself as applied. Flatten all-text part lists to plain strings for /v1/compress and restore the original shapes from the response: untouched rows keep their exact original parts, a rewritten row collapses to one part carrying the last declared cache_control breakpoint (a breakpoint caches the prefix ending at its part, so the last one and its TTL still describe the merged row). Rows with any non-text part are never flattened, since merging text across a non-text part would move a later breakpoint to the other side of it; they pass through the service untouched, matching its own behavior for non-string content. Flattening and write-back use the shared content_text helpers that compresr's breakpoint fix also uses. Resolves LIT-4795 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5af69dd to
33fadd7
Compare
|
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 33fadd7. Configure here.
TLDR
Problem this solves:
How it solves it:
Relevant issues
isinstance(content, str)gates, so the request was forwarded uncompressed while the guardrail still reported itself as appliedLinear ticket
Resolves LIT-4795
Pre-Submission checklist
@greptileaito re-request a review after pushing changes)Changes
headroom.py: flatten ALL-TEXT list-of-parts content to plain strings in the /v1/compress request, then write returned text back; rows the service returned unchanged keep their exact original parts (allcache_controlbreakpoints intact), a rewritten all-text row collapses to one part carrying the LAST declared breakpoint (a breakpoint caches the prefix ending at its part, so after the merge the last one and its TTL still describe the row); rows with any non-text part are never flattened, because merging text across a non-text part would move a later breakpoint to the other side of it; if the service restructures the conversation (row count or roles change) its output is adopted as-is, which is the previous behaviorcontent_text.pyhelpers (content_to_text,is_all_text_parts,merge_rewritten_text_parts) that the compresr breakpoint fix (LIT-4804, fix(guardrails): preserve cache_control breakpoints in compresr write-back #34660) already landed on stagingcompression_interceptioncompresses in-process without the service and is unaffectedRoot cause
The headroom service's transforms only rewrite string content (
smart_crusher.py,code_compressor.py,kompress_compressor.pyall gate onisinstance(content, str)) and pass list-of-parts content through untouched. Every Anthropic/v1/messagesrequest translates to parts-list content instructured_messages, so compression was a silent no-op for all Anthropic client traffic. OpenAI clients mostly send plain strings, which made the bug look model-scoped. A/v1/chat/completionsrequest with content-parts skipped compression the same way, and a/v1/messagesrequest with plain-string content compressed fine, confirming content shape as the discriminator (matrix below, run against a live proxy).Proof of fix (live proxy + string-only compress mock + mock upstream)
Before, claude-cli-shaped
/v1/messagesrequest (parts content, cache_control, tools):After, same request on fixed code:
Negative control on fixed code, plain-string
/v1/chat/completionsstill compresses:Mutation check: reverting the flatten/restore wiring fails the three new shape tests; full suite is 145 passed.
Things a reviewer will ask about
/v1/chat/completionswhen a caller sends content-parts, so the invariant ("the compress service only rewrites strings") belongs at the litellm-to-service boundary where it covers every route_restore_content_shapesis only trusted when the row count and every role line up; on any mismatch the service output is adopted wholesale, which is exactly the pre-flattening behavior, so alignment can never make a request worse than todaycache_controlbreakpoints are positional, so merging text across a non-text part would silently cache a shorter prefix than the caller configured; image bytes therefore never round-trip through the compression serviceNote
Medium Risk
Changes request shaping for a guardrail on the proxy path; mistakes could alter
cache_controlor multimodal rows, though scope is limited to all-text flattening with positional restore guards and broad tests.Overview
Headroom compression now works for Anthropic-style list-of-parts message content, which previously skipped
/v1/compressbecause the service only rewrites stringcontent.Before calling compress, the guardrail flattens rows whose parts are all text into plain strings (via shared
content_texthelpers), then restores the original shapes after the response. Unchanged compressed text keeps the exact original parts (includingcache_control); rewritten all-text rows merge back into a single part with the last breakpoint. Rows with any non-text part (e.g. images) stay unflattened so positional cache breakpoints are not shifted.If the service changes message count or roles, its output is used as-is (unchanged fallback). New unit tests cover wire flattening, restore, row drops, and fail-open behavior.
Reviewed by Cursor Bugbot for commit 33fadd7. Bugbot is set up for automated code reviews on this repo. Configure here.