Skip to content

feat(guardrails): add Compresr guardrail for query-aware context compression - #33019

Open
charafkamel wants to merge 7 commits into
BerriAI:litellm_internal_stagingfrom
charafkamel:compresr-pr-staging
Open

feat(guardrails): add Compresr guardrail for query-aware context compression#33019
charafkamel wants to merge 7 commits into
BerriAI:litellm_internal_stagingfrom
charafkamel:compresr-pr-staging

Conversation

@charafkamel

@charafkamel charafkamel commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Changes

A new Compresr guardrail that shrinks bulky context (tool outputs, RAG chunks, search results) before the request hits the model, so you spend fewer tokens with zero application changes. Runs on apply_guardrail, so it covers /chat/completions, /v1/messages, and /v1/responses.

Two things make it different from whole-conversation compressors:

  • Query-aware — each chunk is compressed against what produced it (a tool result against its tool call, otherwise the last user message), so the parts that matter for the turn survive.
  • Recoverable — every compressed chunk leaves a marker and the request gets a compresr_retrieve tool; if the model needs the original, it asks and the agentic loop feeds it back. Originals live in memory briefly, scoped per caller (no cross-tenant reads), with a TTL and byte cap.

Fails closed by default (configurable), validates api_base, redacts upstream errors, and sanitizes model-supplied input before logging. Nothing compressed -> request passed through untouched. Auto-discovered via the guardrail_hooks registry. Defaults: latte_v2 model, adaptive compression on, recovery on.

6 files:

File What it is
.../compresr/compresr.py the guardrail
.../compresr/__init__.py wiring
.../compresr/README.md usage
litellm/types/proxy/guardrails/guardrail_hooks/compresr.py config model
litellm/types/guardrails.py registration
tests/.../test_compresr.py 76 tests

Same shape as the existing guardrail integrations.

Quickstart

guardrails:
  - guardrail_name: compresr
    litellm_params:
      guardrail: compresr
      mode: pre_call
      default_on: true
      api_key: os.environ/COMPRESR_API_KEY

Note

High Risk
Sits on the critical request path, mutates messages/tools, makes outbound calls with API keys, and holds tenant-scoped originals in per-process memory (multi-worker recovery limitations); security controls are thoughtful but the blast radius is large if misconfigured.

Overview
Adds a new Compresr proxy guardrail (guardrail: compresr) that runs on pre-call requests and calls the Compresr API to shrink large message text (tool outputs by default) before the LLM sees it. Compression is query-aware (tool results use the originating tool call intent; otherwise the last user message), with optional flags for system/history/last-user targets, adaptive latte_v2 settings, and passthrough compression_params.

When recovery is enabled (default), compressed chunks get hash markers, originals are stored in a bounded in-process cache keyed by virtual-key hash + framework call id, and a compresr_retrieve tool is injected. The guardrail hooks the agentic loop to satisfy retrieve calls and rebuild follow-up turns for chat, Anthropic, and Responses API shapes—without echoing unrelated parallel tool calls.

Also wires initializer/class registries, a Pydantic config model, and extends shared guardrail types (SupportedGuardrailIntegrations.COMPRESR, LitellmParams, unreachable_fallback docs). Behavior includes fail_closed / fail_open on Compresr outages, optional x-compresr-bypass, api_base scheme/metadata checks, safe error redaction, and mirroring compression into the Responses texts channel when unambiguous. Covered by a large test_compresr.py suite.

Reviewed by Cursor Bugbot for commit e109f51. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant

CLAassistant commented Jul 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@charafkamel charafkamel changed the title add Compresr guardrail for query-aware context compression feat(guardrails): add Compresr guardrail for query-aware context compression Jul 12, 2026
@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a new Compresr guardrail that runs on apply_guardrail and compresses bulky context (tool outputs, RAG chunks, system messages) before the request reaches the LLM, with optional recoverable-compression via an injected compresr_retrieve tool and agentic-loop hooks. A small fix to _merge_tools_after_guardrail in the Responses API handler accompanies it, preventing guardrail-appended tools from being silently dropped when the original request already carried tools.

  • compresr/compresr.py (~1180 lines): implements query-aware compression, a per-process bounded recovery store (OrderedDict with TTL + byte caps), SSRF/metadata-IP validation on api_base, error redaction, and agentic-loop hooks for chat, Anthropic, and Responses API shapes.
  • handler.py fix (result.extend(remapped[j:])): correctly preserves guardrail-injected tools (like compresr_retrieve) that have no matching slot in the original tools list; covered by a new regression test.
  • Config / types wiring: CompresrGuardrailConfigModel, SupportedGuardrailIntegrations.COMPRESR, and LitellmParams mixin follow the existing per-guardrail pattern exactly.

Confidence Score: 4/5

The change is on the critical request path and mutates messages before the LLM sees them; the recovery store is per-process and silently disabled when virtual-key auth is absent, meaning compressed content can become unrecoverable in some deployment configurations.

The core logic is carefully implemented with good security controls (SSRF mitigations, upstream-error redaction, cross-tenant store partitioning). The outstanding concern is that recovery is silently disabled without per-key auth scope while compression still runs, affecting operators who deploy the guardrail without configuring virtual keys.

litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py - specifically the recovery-enabled/disabled logic around _scoped_store_key and the _warned_no_scope_recovery one-shot warning.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py Core guardrail: ~1180 lines implementing compression, recovery store, and agentic-loop hooks. One minor inconsistency in null-guard pattern for model_call_details in async_build_agentic_loop_plan.
litellm/proxy/guardrails/guardrail_hooks/compresr/init.py Wiring module: registers initializer and class in the guardrail registry; correctly defers litellm import into the function body to avoid circular imports.
litellm/types/proxy/guardrails/guardrail_hooks/compresr.py Pydantic config model with well-documented fields; correctly follows the existing GuardrailConfigModel[OptionalParams] pattern seen in other integrations.
litellm/types/guardrails.py Adds COMPRESR to SupportedGuardrailIntegrations enum and mixes CompresrGuardrailConfigModel into LitellmParams; follows the exact same pattern as all other guardrail integrations.
litellm/llms/openai/responses/guardrail_translation/handler.py Adds result.extend(remapped[j:]) to keep guardrail-appended tools that exceed the original_tools length; fix is minimal and has a direct regression test.
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_compresr.py 2002-line test suite covering compression, recovery, agentic-loop hooks, fail-open/closed, multimodal content, bypass header, and batch mismatches - all via mocks with no real network calls.
tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py Adds two tests for the _merge_tools_after_guardrail fix: a unit test for the merge logic and an end-to-end async test verifying an injected tool survives the Responses API write-back path.

Reviews (4): Last reviewed commit: "refactor(guardrails): extract _existing_..." | Re-trigger Greptile

Comment on lines +1 to +5
# Compresr Guardrail — query-aware, recoverable context compression

[Compresr](https://compresr.ai) compresses bulky message content (tool outputs,
RAG chunks, search results) before the request reaches the LLM, cutting prompt
tokens without losing the information the model actually needs for the current

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Documentation belongs in the litellm-docs repo

Per the team's rule, documentation must not be added here — it should live in the litellm-docs repo instead. Please remove this README from the main codebase and add it there.

Rule Used: Prevent documentation from being added - needs to ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.12219% with 49 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...xy/guardrails/guardrail_hooks/compresr/compresr.py 94.02% 34 Missing ⚠️
...xy/guardrails/guardrail_hooks/compresr/__init__.py 37.50% 15 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py
@veria-ai

veria-ai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 3 · PR risk: 0/10

…ression

Adds a first-class guardrail that compresses bulky message content (tool
outputs, RAG chunks, search results) through the Compresr API before the
request reaches the LLM, via the apply_guardrail / structured_messages hook
so it covers /chat/completions, /v1/messages, and /v1/responses (the latter
through the texts channel, mirrored only when the replacement is
unambiguous; anything ambiguous is left uncompressed).

Distinct from whole-conversation compressors:
- Query-aware: each message is compressed against the intent that produced
  it (a tool output against its originating tool call's name + arguments,
  resolved via tool_call_id; otherwise the last user message).
- Recoverable: each compressed message carries a hash marker and the request
  gains a compresr_retrieve tool, so the model can pull the original content
  back through the agentic loop when the compressed version is not enough.
  Originals are cached in-process, scoped to the caller's virtual-key hash
  plus the request's litellm_call_id, with a TTL and a per-call byte cap;
  recovery is skipped when no caller scope is available so one caller can
  never read another's originals. The store is per-process, so multi-worker
  deployments need sticky routing (or enable_retrieval=false).

Fail-closed by default (fail_open configurable), SSRF-validated api_base
(alternate IP-literal encodings included), cross-tenant-isolated recovery
store, and upstream errors redacted from client-facing responses. The
outbound client follows redirects and re-resolves DNS per request, so the
api_base host/IP checks are defense-in-depth, not a full SSRF guarantee;
this is documented as a known limitation. Requests where nothing was
actually compressed are returned untouched (same object identity) so
handlers skip the write-back. Auto-discovered via the guardrail_hooks
registry.
@charafkamel
charafkamel force-pushed the compresr-pr-staging branch from 431dd46 to 29411b6 Compare July 12, 2026 20:51
@codspeed-hq

codspeed-hq Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing charafkamel:compresr-pr-staging (43a640d) with litellm_internal_staging (477ef3a)

Open in CodSpeed

The recovery store bounded bytes per call and entry count, but had no
aggregate cap: 256 tracked call ids at the 10 MiB per-call default could
retain ~2.5 GiB per worker. A flood of requests with distinct
x-litellm-call-id values and large compressible tool outputs could
exhaust a shared proxy worker.

Add a global byte budget (_MAX_TOTAL_STORE_BYTES, 256 MiB) across all
entries. A running total is maintained on every insert/eviction so the
cap is enforced without re-encoding the whole store on the request path;
oldest entries are evicted once the budget is exceeded, always keeping
the most-recent entry so recovery still works for the request populating
the store. +2 regression tests.
@charafkamel
charafkamel force-pushed the compresr-pr-staging branch from 92934c5 to 672969a Compare July 13, 2026 08:20
Comment thread litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py
Comment thread litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py Outdated
Two hardening fixes to the compresr_retrieve agentic loop:

1. Only run the loop when a retrieve call resolves to recovery state this
   guardrail actually created for the request. Previously the gate checked
   only that the caller-supplied tool list contained a compresr_retrieve
   function and that the model emitted a call, so a caller could define
   their own same-named tool and force an extra provider round-trip with
   nothing to recover. The plan now returns run_agentic_loop=False when no
   requested hash resolves.

2. Bound the follow-up against retrieval amplification: each distinct hash
   is expanded at most once (repeats get a short marker) and at most
   _MAX_RETRIEVALS_PER_LOOP calls are honored, so prompting the model to
   call compresr_retrieve many times with the same marker cannot balloon
   the follow-up. _retrieve_original now returns None on miss.

+3 regression tests; two existing security tests updated to assert the
stronger veto behavior (forged/cross-tenant hashes now stop the loop
entirely instead of returning a not-found follow-up).
@charafkamel
charafkamel force-pushed the compresr-pr-staging branch from df5f912 to 69f0662 Compare July 13, 2026 15:04
…scope

When enable_retrieval is on (the default) but the proxy has no per-key
auth, the request has no caller scope, so recovery is silently disabled:
content is compressed but the compresr_retrieve tool is never injected and
the originals are dropped, with no runtime indication. Emit a one-shot
call-time warning so operators can see recovery is being suppressed and
configure virtual-key auth. +1 regression test.
Condense the verbose multi-line inline comments and the api_base docstring
to concise form. No behavior change.
@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai

@yucheng-berri

Copy link
Copy Markdown
Contributor

bugbot run

@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai review

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e109f51. Configure here.

else:
merged_tools = list(existing_tools) if isinstance(existing_tools, list) else [retrieve_tool]

compressed_inputs["tools"] = merged_tools

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responses drops recovery tool

High Severity

The CompresrGuardrail adds the compresr_retrieve tool to requests. However, for the Responses API, if a request already has tools, the API's tool write-back mechanism drops the injected compresr_retrieve tool. This prevents the model from recovering compressed content, even when compression markers are present.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e109f51. Configure here.

else:
merged_tools = list(existing_tools) if isinstance(existing_tools, list) else [retrieve_tool]

compressed_inputs["tools"] = merged_tools

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Markers outlive byte-cap eviction

Medium Severity

Recovery markers and the compresr_retrieve tool are decided from applied.originals before _store_originals runs _bound_call_bytes. If the per-call byte cap evicts some or all hashes, those markers still ship and the tool may still be injected even though nothing retrievable remains for those hashes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e109f51. Configure here.

@yucheng-berri yucheng-berri Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charafkamel Can you take a look at this issue? Thanks

… markers by byte cap

Two fixes for reviewer-flagged defects in the Compresr guardrail:

- Responses API: _merge_tools_after_guardrail iterated only over the
  request's original tools, dropping any tool a guardrail appended (the
  compresr_retrieve recovery tool) whenever the request already had tools.
  Keep the appended tools so recovery works on /v1/responses.

- Recovery markers: markers + originals were built for every compressed
  target before the per-call byte cap trimmed the store, so an evicted
  original left a marker the model could never retrieve. Attach recovery
  only while the store (existing entries under the same key + this call's
  originals) stays within the cap, so a shipped marker is always retrievable
  -- including on a later turn that reuses the store key.

Adds regression tests for both paths.
…rail under the complexity gate

The byte-cap fix added a branch to apply_guardrail, tipping it past the
C901 complexity ceiling. Move the store lookup into a small helper; no
behavior change.
@yucheng-berri

Copy link
Copy Markdown
Contributor

@greptileai review

@charafkamel

Copy link
Copy Markdown
Contributor Author

@yucheng-berri can you please let me know what the status is here? is there any issue I have to resolve?

@yucheng-berri

Copy link
Copy Markdown
Contributor

merged in #33295

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants