feat(context_management): compact_20260112 polyfill for non-Anthropic providers#28868
Conversation
…non-Anthropic providers - Add `context_management/` module with `clear_tool_uses_20250919` editor dispatched before chat-completions translation on `/v1/messages` - Hard-protect most-recently completed tool_result from being cleared - Attach `context_management.applied_edits` to both non-streaming and streaming (final `message_delta`) responses - Bedrock Converse: forward `context_management`; filter to `compact_20260112`-only edits with `compact-2026-01-12` beta header - token_counter: guard Anthropic-format tools (no `function` key) to prevent AttributeError during polyfill token counting - Streaming: handle empty-choices usage-only trailing chunks - Skip polyfill when `litellm.drop_params = True` Co-authored-by: Cursor <[email protected]>
…onverse If context_management is forwarded as None (e.g. when mapping returns None for an invalid format), _filter_context_management_for_bedrock_converse previously returned early without removing the key, leaving "context_management": null in the request and causing a validation error. Pop the key when the value is not a dict. Co-authored-by: Yassin Kortam <[email protected]>
…o fix PLR0915 Co-authored-by: Cursor <[email protected]>
Co-authored-by: Cursor <[email protected]>
…ect explicit False Co-authored-by: Yassin Kortam <[email protected]>
…agement constants Co-authored-by: Yassin Kortam <[email protected]>
…xcept Wrap apply_context_management() in a try/except so any failure (e.g. litellm.token_counter raising on an unknown tokenizer or unexpected message format) is logged but does not crash the underlying LLM request. The polyfill is a best-effort additive feature; on failure we forward the original messages without applied edits. Co-authored-by: Yassin Kortam <[email protected]>
Use `or {}` instead of `.get(..., {})` so explicit null parameters do not
raise AttributeError when formatting function definitions for token counting.
Co-authored-by: Cursor <[email protected]>
- Use None (not empty list) for polyfill_applied_edits when context management isn't requested, so semantics of 'feature not requested' vs 'feature requested but no edits applied' are distinct. - In the streaming iterator, only pass applied_edits to the per-chunk translator on the final (finish_reason) chunk; intermediate chunks ignore it anyway, and this makes intent explicit on both sync and async paths. Co-authored-by: Yassin Kortam <[email protected]>
Bring the context management PR up to date with the latest changes on litellm_internal_staging.
- _count_tool_uses now requires a string id, matching _collect_tool_use_ids_in_order so the tool_uses trigger can't fire on blocks that aren't clearable. - apply_context_management dispatcher now accepts the OpenAI list form and normalizes it via AnthropicConfig.map_openai_context_management_to_anthropic, so the polyfill path no longer silently no-ops on list input. Co-authored-by: Yassin Kortam <[email protected]>
…opic providers Implements an in-gateway compaction polyfill that summarizes long conversations using a configurable model when `compact_20260112` is requested for non-Anthropic targets (e.g. OpenAI, Gemini), matching Anthropic's context management beta behaviour for those providers. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
|
Greptile SummaryThis PR adds a
Confidence Score: 5/5Safe to merge; the polyfill is opt-in and all error paths degrade gracefully without affecting the main request. All five polyfill phases are correctly implemented and all previously flagged blocking issues have been addressed. Two style-level suggestions remain around private limiter method access and the base prompt used in Phase D for multi-round compaction safety. compact.py: rate-limit pre-check uses private underscore-prefixed limiter methods; Phase D uses the original system prompt rather than augmented_system as the base for summarized_system.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/context_management/editors/compact.py | New 1200-line async compaction editor; implements all five phases with access/budget/rate-limit guards, correct multi-round handling, and graceful error paths. Minor concern: _check_summary_model_rate_limit accesses private underscore-prefixed limiter methods that could silently become no-ops if the limiter is refactored. |
| litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py | Significant refactor: per-instance chunk_queue/content_block_start initialization fixes shared-state bugs; compaction SSE events emitted before the stream loop; _merge_usage_into_held_stop_reason_chunk deduplicated; StopAsyncIteration→StopIteration fix in sync next error handler. |
| litellm/llms/anthropic/experimental_pass_through/adapters/handler.py | Adds context_management polyfill dispatch to both async and sync handler paths; deliberately omits proxy llm_router from the sync path to avoid cross-event-loop issues; AnthropicContextManagementError propagates to the endpoint's exception handler correctly. |
| litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py | translate_completion_output_params_streaming now accepts polyfill_result and passes compaction_block, applied_edits, and iterations_usage to AnthropicStreamWrapper; translate_openai_response_to_anthropic inserts compaction block and context_management fields correctly. |
| litellm/llms/anthropic/experimental_pass_through/context_management/result.py | applied_edits_for_response() correctly includes compact edits when a new compaction block was synthesised, when an error string is set, or when warnings are present; silent under-threshold pass-through omits the edit as designed. |
| litellm/llms/anthropic/experimental_pass_through/context_management/dispatcher.py | Clean async dispatcher; correctly uses inspect.iscoroutinefunction to branch sync vs. async editors; aggregates PolyfillResult fields across multiple edit types. |
| litellm/llms/bedrock/chat/converse_transformation.py | Adds context_management to Anthropic Bedrock supported params; _filter_context_management_for_bedrock_converse correctly strips all non-compact edits and adds the compact beta header before forwarding to Bedrock. |
| litellm/litellm_core_utils/token_counter.py | Adds Anthropic-shape tool support (input_schema) to _format_function_definitions so token counts used by the polyfill threshold check are accurate; also guards against malformed tool dicts. |
| litellm/proxy/anthropic_endpoints/endpoints.py | Adds AnthropicContextManagementError handler that emits an Anthropic-shaped JSON error; 5xx variants also fire post_call_failure_hook for spend/alert parity. |
| litellm/types/llms/anthropic.py | Adds AppliedEdit, ContextManagementResponse, CompactionBlock, and UsageIteration TypedDicts; adds NotRequired context_management field to MessageBlockDelta. Clean additions. |
Reviews (32): Last reviewed commit: "fix(compact_20260112): propagate end-use..." | Re-trigger Greptile
PR overviewThis pull request adds a There is one open security issue remaining: the summary-model subrequest currently checks rate-limit counters without reserving or incrementing them, so an authenticated caller could exceed configured RPM or token limits for compaction requests. This creates a concrete quota-enforcement and resource-abuse risk, especially under concurrent request load. The PR has already worked down 12 prior issues, but this remaining limiter gap should be fixed before relying on the feature in production. Open issues (1)
Fixed/addressed: 12 · PR risk: 6/10 |
…text_management - compact_20260112 Phase D: when keeping the last user turn after a full summary, skip role=user turns whose content is exclusively tool_result blocks. Such turns translate to OpenAI tool-role messages with no preceding assistant tool_calls (those got summarized away), which non-Anthropic providers reject. Fall back to a synthetic continuation prompt if no eligible user question exists, so the downstream call always has a non-empty user message. - bedrock converse: chain the context_management param as elif so it follows the same if/elif pattern as the surrounding thinking/ reasoning_effort checks. Co-authored-by: Yassin Kortam <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Wrong message source for post-compaction user question selection
- Changed _select_last_user_question to operate on effective_messages (post-compaction slice) instead of the raw messages list, so already-summarized turns can't be re-selected.
- ✅ Fixed: Polyfill system can be list but parameter typed str
- Widened the system parameter type on _prepare_completion_kwargs to Optional[Union[str, List[Dict[str, Any]]]] to match PolyfillResult.system and what _augment_system_with_summary can return.
- ✅ Fixed: Sync streaming path missing context_management on merged usage
- Mirrored the async hold-and-merge logic in sync next: stop_reason chunks are held, then merged with the next usage chunk and context_management is attached to the merged final message_delta.
You can send follow-ups to the cloud agent here.
… stream merge - compact.py: select last user question from effective_messages (post-compaction slice) instead of raw messages, so prior summarized turns aren't reintroduced - handler.py: widen _prepare_completion_kwargs system parameter type to Union[str, List[Dict]] matching PolyfillResult.system - streaming_iterator.py: mirror async hold-and-merge logic in sync __next__ so context_management is attached to the final merged message_delta when stop_reason and usage arrive in separate chunks Co-authored-by: Yassin Kortam <[email protected]>
… held stop_reason chunk in async iterator - Sync `anthropic_messages_handler` was silently dropping the `context_management` kwarg via `ANTHROPIC_ONLY_REQUEST_KEYS` after the polyfill was moved into the async handler. Bridge to the async dispatcher with `run_async_function` so `litellm.messages.create()` callers keep working (regressed e.g. `clear_tool_uses_20250919`). - In the streaming iterator's `__anext__` `StopIteration` handler, clear `self.holding_stop_reason_chunk` after capturing it (matches `__next__`) so a subsequent call doesn't re-emit the same chunk. Co-authored-by: Yassin Kortam <[email protected]>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…sync polyfill; trailing-chunk passthrough Co-authored-by: Yassin Kortam <[email protected]>
…d _polyfill_result key - streaming_iterator: in sync __next__, after the usage chunk has been merged and emitted, silently consume any trailing provider events via 'continue' instead of forwarding them through the queue. Trailing chunks would translate to content_block_delta or message_delta and violate Anthropic SSE ordering after the final message_delta. The async __anext__ already drops these via 'if not self.queued_usage_chunk:' gating, so this aligns sync and async behavior. - handler: drop unused '_polyfill_result' from ANTHROPIC_ONLY_REQUEST_KEYS. PolyfillResult is passed as an explicit arg to the adapter methods, never through extra_kwargs, so the entry was dead code. Co-authored-by: Yassin Kortam <[email protected]>
…nored knobs - compact_20260112: read summary max_tokens from general_settings (context_management_summary_max_tokens) so operators can fit the chosen summary model's output budget; falls back to the compiled default for missing or invalid values. - clear_tool_uses_20250919: log unsupported knobs at warning level (was debug, which silently dropped misconfiguration) and surface them as warnings on the AppliedEdit so clients see what was ignored.
A slow or unresponsive summary model previously hung the parent /v1/messages request with no escape hatch. Pass a 60s timeout on the litellm.acompletion / llm_router.acompletion subrequest; on timeout the existing summary_call_failed path forwards the request without compaction rather than blocking indefinitely.
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
When a prior compaction block is present and the request is under threshold, the polyfill was reducing downstream messages to just the latest user question. The prior summary only covers turns before the compaction block, so dropping the post-compaction tail silently lost recent context — a multi-turn conversation that stayed below the threshold would arrive at the model with no memory of any turn after the prior compaction. Forward the already-stripped post-compaction tail unchanged on both the under-threshold path and apply_client_compaction_block_history. Fall back to _select_last_user_question only when the strip leaves nothing for the downstream call to answer.
…on summary subrequest The local gate previously only checked the parent key's and team's allowed-model lists. A caller restricted by a personal user, project, or per-team-member allowed_models scope could still trigger the configured summary model and receive its <summary> output as a compaction block, because llm_router.acompletion bypasses the proxy common_checks path. Extend _check_summary_model_access to also load the user_object, project_object, and team_membership and run the matching allowlist check at each scope before invoking the summary model.
…pagate budget metadata
| response = await limiter.should_rate_limit( | ||
| descriptors=descriptors, | ||
| parent_otel_span=getattr(user_api_key_auth, "parent_otel_span", None), | ||
| read_only=True, |
There was a problem hiding this comment.
High: Rate limit bypass
read_only=True only observes the current counters; it does not increment RPM or reserve TPM the way the normal proxy pre-call limiter does. An authenticated caller can repeatedly send over-threshold compact_20260112 requests and drive summary-model completions past the configured summary-model RPM, and concurrent requests can all pass the token check before post-call token reconciliation runs. Run the normal pre-call limiter/reservation path for the summary subrequest, or explicitly perform the same RPM increment and TPM reservation/refund around _call_summary_model.
There was a problem hiding this comment.
Disagree that this is an exploitable rate-limit bypass — the configured summary-model limits are enforced, just not exclusively by this read_only pass:
-
Deployment-level RPM/TPM is hard-enforced for every summary call.
_call_summary_modelroutes throughllm_router.acompletion, which runs the router's deployment pre-call checks under a per-deploymentmax_parallel_requestssemaphore and increments the deployment RPM concurrency-safe before the call (router.py~2898-2911,async_routing_strategy_pre_call_checks). So a caller cannot drive completions past the summary model's configured RPM/TPM — concurrent summary calls are serialized and counted there, not by this hook. -
Summary calls are strictly 1:1 with the parent
/v1/messagesrequest, which has already passed the full proxy pre-call limiter (RPM increment + atomicreserve_tpm_tokensTPM reservation). A caller cannot produce more summary calls than authorized parent requests, so there is no amplification beyond the caller's already-enforced request rate. -
Summary TPM is charged to the caller's per-scope counters exactly once, post-call, by
_PROXY_MaxParallelRequestsHandler_v3.async_log_success_eventvia the propagatedlitellm_metadata/userfields. Usingread_onlyhere is deliberate: a non-read-onlyshould_rate_limitwould+1those same:tokenscounters at pre-call and double-charge against the post-call reconciliation. -
This pass is intentionally the read side only: it denies when the caller is already
OVER_LIMITon their key/team/user/org RPM or TPM for the summary model (the only deny signal), giving best-effort pre-call backpressure without reserving. That matches how LiteLLM treats admin-opt-in system subcalls — the feature no-ops unless an admin setsgeneral_settings.context_management_summary_model.
Net: deployment capacity is hard-enforced and incremented by the router; per-customer volume is bounded 1:1 by the fully-rate-limited parent; summary tokens are attributed post-call. There is no path to exceed the configured summary-model RPM beyond what the parent rate already authorizes, so adding a second reserve/refund cycle here would only risk double-counting.
4cc3dd7
into
litellm_internal_staging
… providers (BerriAI#28868) * feat(anthropic/messages): in-gateway context_management polyfill for non-Anthropic providers - Add `context_management/` module with `clear_tool_uses_20250919` editor dispatched before chat-completions translation on `/v1/messages` - Hard-protect most-recently completed tool_result from being cleared - Attach `context_management.applied_edits` to both non-streaming and streaming (final `message_delta`) responses - Bedrock Converse: forward `context_management`; filter to `compact_20260112`-only edits with `compact-2026-01-12` beta header - token_counter: guard Anthropic-format tools (no `function` key) to prevent AttributeError during polyfill token counting - Streaming: handle empty-choices usage-only trailing chunks - Skip polyfill when `litellm.drop_params = True` Co-authored-by: Cursor <[email protected]> * fix(bedrock): pop None context_management before sending to Bedrock Converse If context_management is forwarded as None (e.g. when mapping returns None for an invalid format), _filter_context_management_for_bedrock_converse previously returned early without removing the key, leaving "context_management": null in the request and causing a validation error. Pop the key when the value is not a dict. Co-authored-by: Yassin Kortam <[email protected]> * fix(bedrock/converse): pop None context_management; extract helpers to fix PLR0915 Co-authored-by: Cursor <[email protected]> * fix(anthropic/messages): check per-request drop_params alongside global Co-authored-by: Cursor <[email protected]> * fix(anthropic/messages): preserve drop_params for downstream and respect explicit False Co-authored-by: Yassin Kortam <[email protected]> * fix: lazy debug logging in clear_tool_uses; remove unused context_management constants Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic/messages): guard context_management polyfill with try/except Wrap apply_context_management() in a try/except so any failure (e.g. litellm.token_counter raising on an unknown tokenizer or unexpected message format) is logged but does not crash the underlying LLM request. The polyfill is a best-effort additive feature; on failure we forward the original messages without applied edits. Co-authored-by: Yassin Kortam <[email protected]> * fix(token_counter): guard None input_schema in Anthropic tool fallback Use `or {}` instead of `.get(..., {})` so explicit null parameters do not raise AttributeError when formatting function definitions for token counting. Co-authored-by: Cursor <[email protected]> * fix: minimize context_management polyfill threading - Use None (not empty list) for polyfill_applied_edits when context management isn't requested, so semantics of 'feature not requested' vs 'feature requested but no edits applied' are distinct. - In the streaming iterator, only pass applied_edits to the per-chunk translator on the final (finish_reason) chunk; intermediate chunks ignore it anyway, and this makes intent explicit on both sync and async paths. Co-authored-by: Yassin Kortam <[email protected]> * fix(context_management): align tool_use counts and normalize list spec - _count_tool_uses now requires a string id, matching _collect_tool_use_ids_in_order so the tool_uses trigger can't fire on blocks that aren't clearable. - apply_context_management dispatcher now accepts the OpenAI list form and normalizes it via AnthropicConfig.map_openai_context_management_to_anthropic, so the polyfill path no longer silently no-ops on list input. Co-authored-by: Yassin Kortam <[email protected]> * feat(context_management): add compact_20260112 polyfill for non-Anthropic providers Implements an in-gateway compaction polyfill that summarizes long conversations using a configurable model when `compact_20260112` is requested for non-Anthropic targets (e.g. OpenAI, Gemini), matching Anthropic's context management beta behaviour for those providers. * fix(compact): skip tool_result-only user turns; bedrock: elif for context_management - compact_20260112 Phase D: when keeping the last user turn after a full summary, skip role=user turns whose content is exclusively tool_result blocks. Such turns translate to OpenAI tool-role messages with no preceding assistant tool_calls (those got summarized away), which non-Anthropic providers reject. Fall back to a synthetic continuation prompt if no eligible user question exists, so the downstream call always has a non-empty user message. - bedrock converse: chain the context_management param as elif so it follows the same if/elif pattern as the surrounding thinking/ reasoning_effort checks. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic): post-compaction question selection, system type, sync stream merge - compact.py: select last user question from effective_messages (post-compaction slice) instead of raw messages, so prior summarized turns aren't reintroduced - handler.py: widen _prepare_completion_kwargs system parameter type to Union[str, List[Dict]] matching PolyfillResult.system - streaming_iterator.py: mirror async hold-and-merge logic in sync __next__ so context_management is attached to the final merged message_delta when stop_reason and usage arrive in separate chunks Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic/messages): apply context_management on sync path; clear held stop_reason chunk in async iterator - Sync `anthropic_messages_handler` was silently dropping the `context_management` kwarg via `ANTHROPIC_ONLY_REQUEST_KEYS` after the polyfill was moved into the async handler. Bridge to the async dispatcher with `run_async_function` so `litellm.messages.create()` callers keep working (regressed e.g. `clear_tool_uses_20250919`). - In the streaming iterator's `__anext__` `StopIteration` handler, clear `self.holding_stop_reason_chunk` after capturing it (matches `__next__`) so a subsequent call doesn't re-emit the same chunk. Co-authored-by: Yassin Kortam <[email protected]> * fix(bugfixes): bedrock None context_mgmt; stream per-instance queue; sync polyfill; trailing-chunk passthrough Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic): silently drop trailing chunks after usage; remove dead _polyfill_result key - streaming_iterator: in sync __next__, after the usage chunk has been merged and emitted, silently consume any trailing provider events via 'continue' instead of forwarding them through the queue. Trailing chunks would translate to content_block_delta or message_delta and violate Anthropic SSE ordering after the final message_delta. The async __anext__ already drops these via 'if not self.queued_usage_chunk:' gating, so this aligns sync and async behavior. - handler: drop unused '_polyfill_result' from ANTHROPIC_ONLY_REQUEST_KEYS. PolyfillResult is passed as an explicit arg to the adapter methods, never through extra_kwargs, so the entry was dead code. Co-authored-by: Yassin Kortam <[email protected]> * refactor(anthropic): extract usage-merge helper; guard empty slice-only compaction result - Extract the duplicated hold-and-merge usage logic from the sync __next__ and async __anext__ paths into a shared _merge_usage_into_held_stop_reason_chunk helper so the subtle cache-token / context_management attachment lives in exactly one place. - In the compact_20260112 slice-only path, fall back to _select_last_user_question when _strip_compaction_blocks produces an empty list (e.g. messages ending on an assistant turn whose only content was the compaction block) so the downstream API never receives an empty messages array. Co-authored-by: Yassin Kortam <[email protected]> * refactor(anthropic/context_management): streaming iterator compaction fixes and compact polyfill improvements - Extract usage-merge helper; guard empty slice-only compaction result - Silently drop trailing chunks after usage; remove dead _polyfill_result key - Fix bedrock None context_mgmt; stream per-instance queue; sync polyfill; trailing-chunk passthrough - Apply context_management on sync path; clear held stop_reason chunk in async iterator - Fix post-compaction question selection, system type, sync stream merge - Skip tool_result-only user turns; bedrock: elif for context_management - Add streaming iterator compaction test suite Co-authored-by: Cursor <[email protected]> * revert(html): restore flat *.html naming in _experimental/out Reverses the accidental rename from *.html → */index.html introduced in 15ea941. All 35 files moved back to their original flat paths so the directory structure matches litellm_internal_staging. Co-authored-by: Cursor <[email protected]> * revert(config): restore proxy_server_config.yaml to litellm_internal_staging Co-authored-by: Cursor <[email protected]> * Fix: skip client compaction pre-processing when compact_20260112 polyfill will run The _prepare_context_managed_request helper unconditionally applied apply_client_compaction_block_history before invoking the polyfill. When the request also configured a compact_20260112 spec, that pre-processing consumed the client-sent compaction block and collapsed the message history to just the latest user question, starving the polyfill of conversation context. The polyfill's own Phase A (_slice_around_compaction_block) already handles client compaction blocks correctly and inspects the full post-compaction tail for the token-threshold check, so the pre-processing is both redundant and destructive in this case. Now the pre-processing only runs when no compact_20260112 polyfill spec will execute (no spec, drop_params on, or only non-compact edits like clear_tool_uses_20250919). Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic): plug compaction-block leak + iteration-usage gaps in streaming adapter - handler: when polyfill_will_run skipped client-history pre-processing and the polyfill ultimately returned None (best-effort swallow on unexpected error), apply the slice-only fallback before returning so Anthropic-specific 'compaction' content blocks don't leak to non- Anthropic backends that would reject them. - streaming_iterator: precompute will_merge_into_held so we don't pass applied_edits into the translator when the resulting processed_chunk will be discarded by the held stop-reason merge path. - streaming_iterator: augment processed_chunk with iterations usage in the holding_chunk branch (sync and async) for parity with the other emission branches; ensures usage.iterations is attached on the rare message_delta-reaches-holding_chunk path. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic): correct streaming usage iteration + translate tools for token counting - streaming_iterator: skip the trailing "message" iteration entry in the final message_delta when the held stop_reason chunk carries placeholder zero usage (no separate usage chunk arrived). Reporting zero tokens was misleading and inconsistent with the non-streaming path which always has real usage data. - streaming_iterator: drop two redundant type checks inside branches that are already guarded by an outer message_delta type check. - compact._count_effective_tokens: translate Anthropic-shaped tools (input_schema) to OpenAI shape before passing to litellm.token_counter so threshold checks aren't skewed by tokenizer paths that expect the OpenAI tool wrapper. Co-authored-by: Yassin Kortam <[email protected]> * Fix lint * fix(anthropic): plug content drop, compaction SSE shape, and compaction leak - Sync streaming __next__ no longer drops a buffered holding_chunk when the usage-merge path has already fired. Restoring the prior unconditional flush behavior preserves provider-emitted content (the SSE-ordering nit of a trailing content delta is preferable to silent content loss). - compaction content_block_start now carries the full block shape ({"type": "compaction", "content": ""}) to match the text-block pattern and Anthropic's native streaming shape, so clients that key off content_block_start see the field. - apply_compact_20260112 now slices around / strips compaction blocks before the opt-in gate check. Previously, when summary_model was not configured the editor returned the raw messages, leaking Anthropic-only compaction content blocks to non-Anthropic providers that reject them. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic): resolve mypy types in context management polyfill Use AppliedEdit and CompactionBlock consistently in the dispatcher and streaming adapter. Co-authored-by: Cursor <[email protected]> * fix(anthropic): flush held content chunk in async streaming path Mirror the sync __next__ behavior: always flush a buffered holding_chunk after the stream ends, even when usage was already merged + emitted. Previously the async __anext__ kept the flush inside the 'if not self.queued_usage_chunk:' guard, silently dropping the last content delta on the proxy's primary path. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic adapter): correct sync streaming, surface polyfill failures, decouple sync path from proxy router - translate_completion_output_params_streaming: add is_async flag so the sync handler returns Iterator[bytes] instead of an unusable AsyncIterator. Async callers keep the existing behavior via the default is_async=True. - _run_polyfill_if_enabled: when the polyfill crashes and the spec requested non-compact edits (e.g. clear_tool_uses_20250919), raise an AnthropicContextManagementError instead of silently returning None so those edits are not dropped without an error surface. The compaction-block-slicing safety net remains for compact-only specs. - anthropic_messages_handler (sync): stop auto-attaching the proxy llm_router. run_async_function bridges to a new thread's event loop; reusing the proxy's loop-bound httpx clients there causes 'Event loop is closed' errors. The summary editor falls back to litellm.acompletion when llm_router is None. Co-authored-by: Yassin Kortam <[email protected]> * fix: address bug detection findings in token counter and streaming iterator - token_counter: guard against non-dict 'function' field in tool dicts and skip tools missing a name to avoid emitting 'type None = ...' which would produce inaccurate token counts. - streaming_iterator: change sync __next__ generic-error path to raise StopIteration (was StopAsyncIteration), so sync iteration cleanly stops. - streaming_iterator: centralize context_management attachment so the held-stop_reason direct-flush path defensively re-attaches applied_edits to match the merge path's guarantee. Co-authored-by: Yassin Kortam <[email protected]> * Fix lint * fix: correct COMPACT_MIN_TRIGGER_TOKENS to 50_000 Co-authored-by: Yassin Kortam <[email protected]> * Fix lint * Fix lint * Fix lint * fix(compact): reduce to last user question when summary_model not configured but prior compaction block exists Aligns the summary_model_not_configured path with the under-threshold and client-compaction-block paths, which both reduce post-compaction messages to just the latest user question so the downstream provider doesn't get the summary on system prefix AND the full post-compaction history. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact): forward caller system prompt to summary model call The default summarization instructions reference "the initial task above" and "the raw history above", but the system prompt that holds that task was not being forwarded to the summary model. The summary call now prepends an OpenAI-shaped system message translated from the original Anthropic-shaped system (str or content-block list) so the summarizer has the agent role and initial task in scope. * fix(compact_20260112): set default max_tokens and merge prompt when last turn is user - Set COMPACT_SUMMARY_MAX_TOKENS default for the summary call so providers like Anthropic (which require max_tokens) don't silently fail and degrade to summary_call_failed. - When the trailing translated message is already a user turn, merge the summarization prompt into it instead of appending a second user turn. Avoids consecutive role=user messages that strict providers reject. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic adapter): move current_content_block_start to __init__ Move the default TextBlock dict from a class-level attribute to __init__ so concurrent stream instances don't share the same mutable dict. The class-level default could be mutated in-place via tool_block['name'] = original_name in _should_start_new_content_block, leaking state across streams. This mirrors the existing fix already applied to chunk_queue. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact_20260112): surface error states + strip tool_result blocks in last user question applied_edits_for_response() now includes compact_20260112 edits that carry an error field (summary_model_not_configured, summary_call_failed, summary_extraction_failed) so clients and operators can see why compaction was requested but not applied. _select_last_user_question() now strips tool_result blocks from mixed [tool_result, text] turns rather than passing them through as-is. After compaction the paired tool_use assistant turn no longer exists, so forwarding tool_result blocks translates to orphaned role=tool messages on non-Anthropic providers and produces a 400. * fix(compact_20260112): carry prior compaction summary into Phase C summary call When a request already contains a compaction block, Phase A slices `effective_messages` to the turns since that block. Previously Phase C passed the original `system` to the summary model, so multi-round compaction silently dropped accumulated history each time the polyfill fired. Pass `augmented_system` (original system + prior summary prefix) so the summary model can produce a comprehensive summary that incorporates both the prior round's context and the current slice. `summarized_system` for the downstream call stays built from the original `system` + new `summary_text`. * refactor: delegate handler spec normalization to dispatcher _normalize_spec_edits in adapters/handler.py duplicated the spec-shape normalization already implemented by _normalize_spec in context_management/dispatcher.py. The two could drift: a change in one (e.g. supporting a new spec shape) without the other would cause the handler's polyfill_will_run prediction to disagree with the dispatcher's actual behavior, breaking the client-history pre-processing skip. Have the handler delegate to the dispatcher's _normalize_spec while keeping handler-specific concerns (drop_params short-circuit, swallow mapping exceptions) at the wrapper level. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact_20260112): surface warning-only applied edits in response `applied_edits_for_response()` previously hid `compact_20260112` edits when they had only warnings (no compaction block, no error). This dropped diagnostically important warnings such as `unsupported_trigger_type_X_using_input_tokens` and `pause_after_compaction_ignored` whenever the conversation was under the trigger threshold. Operators now see these warnings in the response. Co-authored-by: Yassin Kortam <[email protected]> * fix: address two low-severity context_management edge cases - streaming_iterator: keep `sent_content_block_finish` in sync with the compaction block's emitted start/delta/stop lifecycle and reset it when the next text block's start is queued. - bedrock _map_context_management_param: match dispatcher `_normalize_spec` behavior — only run the OpenAI→Anthropic mapper on list inputs; pass dict inputs through unchanged so already-Anthropic-format values aren't silently dropped. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact_20260112): use beta-header constant; require type discriminator; skip sync bridge when idle - bedrock: replace hardcoded "compact-2026-01-12" beta string with ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value in both Converse (_filter_context_management_for_bedrock_converse) and Invoke (anthropic_claude3) compact-edit handlers. - types: mark the "type" discriminator as Required[...] on the new CompactionBlock and UsageIteration TypedDicts so the discriminator is not silently optional under total=False. - adapters/handler: short-circuit the sync /v1/messages adapter path before spawning the run_async_function worker-thread event loop when the request has no context_management spec and no client-sent compaction block in the message history. Test plan: - uv run pytest tests/test_litellm/llms/anthropic/experimental_pass_through/ tests/test_litellm/llms/bedrock/test_converse_context_management.py -q (370 + 10 = 380 passed) - uv run pytest tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py -k compact (3 passed) * fix(compact_20260112): include system prompt tokens in threshold check The threshold check in Phase B previously counted only message tokens and the compaction-block content, omitting the system prompt entirely. When the system carried a prior compaction summary (via _augment_system_with_summary) or was otherwise large, the threshold could fire later than intended, allowing the conversation to exceed the model's context window before compaction activated. _count_effective_tokens now also counts the (augmented) system prompt text. The caller passes compaction_block=None when augmented_system already includes the prior summary, to avoid double-counting. Co-authored-by: Yassin Kortam <[email protected]> * Fix SSE ordering and compaction state machine bugs in AnthropicStreamWrapper - Suppress holding_chunk flush after final message_delta has been emitted (queued_usage_chunk == True) so a trailing content_block_delta cannot follow message_delta, which strict Anthropic SDK clients may reject. When usage has not yet been merged, flush the holding_chunk *before* the held stop_reason chunk so SSE ordering remains correct. - Replace _queue_compaction_block_events with _next_compaction_event, emitting the compaction start/delta/stop events one at a time. The state machine flags (sent_content_block_finish) and content block index now advance atomically with the terminal stop event actually being returned to the caller, eliminating the transient inconsistent state where flags say the block is finished while its stop event is still buffered. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact_20260112): enforce parent key/team allowlist on summary model The compact_20260112 polyfill summary subrequest used llm_router.acompletion directly, bypassing the proxy auth checks that gate model access for the parent key/team. A caller whose key/team was not authorized for the configured context_management_summary_model could still cause the proxy to invoke that model and return its output as a compaction block. Pull the parent's UserAPIKeyAuth out of litellm_metadata in the handler, thread it through the dispatcher into apply_compact_20260112, and gate the summary call on _can_object_call_model for both key-level and team-level allowlists. Failures land as applied_edits[0].error = summary_model_access_denied without raising. SDK callers (no UserAPIKeyAuth) remain unaffected. * fix(compact_20260112): distinguish access-denied from transient errors; greedy summary regex - _check_summary_model_access now catches ProxyException explicitly for access denials and logs unexpected exceptions separately. Both still fail closed, but operators can now tell a denied key/team apart from a router internal raising during the check. - _SUMMARY_TAG_RE switches from non-greedy to greedy so a stray </summary> inside the model's summary content no longer silently truncates the captured text. * fix(compact_20260112): type object_type as Literal for mypy * fix(compact_20260112): attribute summary subcall spend to parent key/team The compact_20260112 polyfill summary subrequest propagated metadata via the Anthropic-shape `metadata` parameter, which only carries `user_id`. The proxy auth fields used for spend attribution (`user_api_key`, `user_api_key_team_id`, `litellm_call_id`, ...) live in `data["litellm_metadata"]`. As a result, summary subcalls landed on the router with an empty propagated metadata and the resulting tokens were not attributed to the caller's key/team budget. Rename the polyfill chain's spend-propagation parameter to `litellm_metadata` and pull it from `kwargs["litellm_metadata"]` in both the async and sync handlers, so the post-call hooks see the parent key/team and bill the summary tokens accordingly. Add an `_extract_proxy_litellm_metadata` helper and refactor `_extract_user_api_key_auth` to use it. * chore(anthropic adapters): remove unused _extract_user_api_key_auth helper Co-authored-by: Yassin Kortam <[email protected]> * chore(compact_20260112): non-greedy summary regex; use COMPACT_EDIT_TYPE in bedrock filter - Make _SUMMARY_TAG_RE non-greedy so a response with multiple <summary> blocks captures only the first complete block. - Replace the hardcoded 'compact_20260112' literal in _filter_context_management_for_bedrock_converse with the shared COMPACT_EDIT_TYPE constant. * fix: bug fixes from PR review - streaming_iterator: don't set sent_content_block_finish during compaction block lifecycle; that flag tracks the regular text/tool_use/thinking block state machine, conflating the two leaks bad state to introspection paths. - compact._call_summary_model: send propagated proxy auth/spend-attribution fields as 'litellm_metadata' instead of 'metadata' so the router's post-call hooks attribute summary tokens to the caller's key/team budget. Co-authored-by: Yassin Kortam <[email protected]> * fix(anthropic-streaming): insert content_block_stop between held delta and final message_delta When the stream exhausts with both `holding_chunk` (a content_block_delta) and `holding_stop_reason_chunk` (a message_delta) buffered, the after-loop cleanup previously emitted them back-to-back, producing the invalid Anthropic SSE sequence `content_block_delta -> message_delta`. Insert a `content_block_stop` between them in both the sync `__next__` and async `__anext__` paths so the emitted ordering remains `content_block_delta -> content_block_stop -> message_delta`. Co-authored-by: Yassin Kortam <[email protected]> * fix(compact_20260112): propagate allowed_model_region to summary subrequest The router enforces region restrictions by reading allowed_model_region from top-level request kwargs (Router._common_checks_available_deployment), but the compact_20260112 summary subrequest only forwarded litellm_metadata. A region-restricted caller could trigger compaction and have their conversation summarized by a deployment outside the permitted region. Extract allowed_model_region from user_api_key_auth and pass it through _call_summary_model as a top-level kwarg so the router applies the same region constraints the parent request would. * fix(anthropic adapter): emit content_block_stop before held message_delta in drain paths Co-authored-by: Yassin Kortam <[email protected]> * feat(context_management): configurable summary max_tokens; surface ignored knobs - compact_20260112: read summary max_tokens from general_settings (context_management_summary_max_tokens) so operators can fit the chosen summary model's output budget; falls back to the compiled default for missing or invalid values. - clear_tool_uses_20250919: log unsupported knobs at warning level (was debug, which silently dropped misconfiguration) and surface them as warnings on the AppliedEdit so clients see what was ignored. * fix(compact_20260112): bound _call_summary_model with timeout A slow or unresponsive summary model previously hung the parent /v1/messages request with no escape hatch. Pass a 60s timeout on the litellm.acompletion / llm_router.acompletion subrequest; on timeout the existing summary_call_failed path forwards the request without compaction rather than blocking indefinitely. * fix(compact_20260112): preserve post-compaction tail on slice-only path When a prior compaction block is present and the request is under threshold, the polyfill was reducing downstream messages to just the latest user question. The prior summary only covers turns before the compaction block, so dropping the post-compaction tail silently lost recent context — a multi-turn conversation that stayed below the threshold would arrive at the model with no memory of any turn after the prior compaction. Forward the already-stripped post-compaction tail unchanged on both the under-threshold path and apply_client_compaction_block_history. Fall back to _select_last_user_question only when the strip leaves nothing for the downstream call to answer. * fix(compact_20260112): enforce user/project/team-member model scopes on summary subrequest The local gate previously only checked the parent key's and team's allowed-model lists. A caller restricted by a personal user, project, or per-team-member allowed_models scope could still trigger the configured summary model and receive its <summary> output as a compaction block, because llm_router.acompletion bypasses the proxy common_checks path. Extend _check_summary_model_access to also load the user_object, project_object, and team_membership and run the matching allowlist check at each scope before invoking the summary model. * fix(compact_20260112): enforce summary model per-model budget and propagate budget metadata * fix(compact_20260112): forward post-compaction tail when summary model unconfigured * fix(anthropic endpoints): run failure hook on 500-level context management errors * fix(compact_20260112): enforce summary model rate limit before summary call * fix(compact_20260112): propagate end-user/project budget scope to summary call --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Yassin Kortam <[email protected]> Co-authored-by: mateo-berri <[email protected]>
Summary
compact_20260112polyfill that runs only for non-Anthropic providers (OpenAI, Gemini, etc.); Anthropic / Bedrock-Anthropic / Vertex-Anthropic forward the beta header unchangedlitellm.acompletion/llm_router.acompletion; (D) shape downstream call with summary as system prefix; (E) prependcompactionblock to response and populateusage.iterationsgeneral_settings.context_management_summary_modelis configured; requests without it pass through withapplied_edits[0].error = "summary_model_not_configured"AnthropicContextManagementError(new) produces an Anthropic-format 400 response for validation failures (e.g.trigger.value < 50 000)PolyfillResultdataclass threads compaction state through the adapter chain without touching the streaming path or Anthropic forward pathNew files
litellm/llms/anthropic/experimental_pass_through/context_management/editors/compact.py— async compaction editorlitellm/llms/anthropic/experimental_pass_through/context_management/errors.py—AnthropicContextManagementErrorlitellm/llms/anthropic/experimental_pass_through/context_management/result.py—PolyfillResultdataclasstests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py— 33 unit testsTest plan
uv run pytest tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py -v— 33 tests: trigger validation, opt-in gate, slice-only path, full summary path, error cases, endpoint wiringuv run pytest tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py -v— 73 tests including 4 new polyfill transformation tests (compaction_block, iterations_usage, combined, no-polyfill baseline)uv run pytest tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_dispatcher.py -v— updated for async dispatcherTest
Note
High Risk
Introduces extra summarization LLM calls, rewrites conversation history, and proxy auth/spend attribution for summary subrequests—failures or misconfiguration can change request content or billing behavior.
Overview
Adds an in-gateway Anthropic
context_managementpolyfill on the/v1/messagesadapter path (non-native providers): a dispatcher runsclear_tool_uses_20250919andcompact_20260112, mutatingmessages/systembefore OpenAI-shaped completion and surfacingcontext_management.applied_edits, optionalcompactioncontent, andusage.iterationson non-streaming and streaming responses.compact_20260112slices around client compaction blocks, counts tokens (with improved Anthropic tool counting), and when over threshold calls a proxy-configuredcontext_management_summary_model(with key/team access checks and spend metadata propagation). Summary text is injected as a system prefix; downstream payloads strip compaction blocks. Syncmessages.createbridges the async editor viarun_async_function(with careful router/event-loop handling).Bedrock gains
context_managementpassthrough for Anthropic models, filtering Converse/invoke requests to compact-only edits plus the compact beta header. The proxy mapsAnthropicContextManagementErrorto Anthropic-shaped JSON errors. Streaming wrapper fixes per-stream state and emits compaction SSE in valid event order.Reviewed by Cursor Bugbot for commit 32d9937. Bugbot is set up for automated code reviews on this repo. Configure here.