Skip to content

Prompt Compression - add it to the proxy - #25729

Merged
krrish-berri-2 merged 17 commits into
litellm_internal_stagingfrom
litellm_dev_04_14_2026_p1
Apr 20, 2026
Merged

Prompt Compression - add it to the proxy#25729
krrish-berri-2 merged 17 commits into
litellm_internal_stagingfrom
litellm_dev_04_14_2026_p1

Conversation

@krrish-berri-2

@krrish-berri-2 krrish-berri-2 commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Long-context workloads (Claude Code, RAG pipelines, document processing) frequently send requests with 50k–200k+ token inputs. This drives up cost and latency even when most of that context isn't relevant to the model's actual reasoning for the current turn.

LiteLLM already has a compress() utility, but it was OpenAI-only and ran client-side. There was no way for a proxy operator to transparently apply compression to all traffic without clients opting in.


Solution

This PR adds server-side prompt compression as a first-class proxy callback. The proxy intercepts inbound Anthropic Messages requests, compresses them using BM25/embedding scoring, and fulfills content retrieval calls server-side — all transparently, with no client changes required.


Architecture

Client (e.g. Claude Code)
        │
        │  POST /v1/messages  (full ~100k token context)
        ▼
┌─────────────────────────────────────────────────────────┐
│  LiteLLM Proxy                                          │
│                                                         │
│  1. async_pre_call_deployment_hook                      │
│     ├─ token count > compression_trigger?               │
│     ├─ litellm.compress(messages, input_type=           │
│     │       "anthropic_messages")                       │
│     │    ├─ normalize content blocks → plain text       │
│     │    ├─ BM25/embedding score each message           │
│     │    ├─ stub low-score messages → key references    │
│     │    └─ return compressed messages + cache          │
│     ├─ inject litellm_content_retrieve tool             │
│     └─ store cache[call_id] = {key → original text}     │
│                                                         │
│  2. Model call  (compressed ~20k token context)         │
│     └─ model may call litellm_content_retrieve(key)     │
│                                                         │
│  3. async_should_run_agentic_loop                       │
│     └─ detect tool_use{name: litellm_content_retrieve}? │
│                                                         │
│  4. async_build_agentic_loop_plan                       │
│     ├─ resolve keys from in-memory cache                │
│     ├─ build tool_result messages                       │
│     └─ return AgenticLoopPlan (rerun spec)              │
│                                                         │
│  5. Rerun model with retrieved content                  │
│     └─ return final answer                              │
└─────────────────────────────────────────────────────────┘
        │
        │  Final answer (no tool calls visible to client)
        ▼
     Client

Streaming is fully supported. The new AgenticAnthropicStreamingIterator wraps the SSE byte stream, yields every chunk to the client in real time, reconstructs the full response on stream exhaustion, and chains a Phase 2 stream if an agentic hook fires.


What Changed

1. compression_interception callback

New file: litellm/integrations/compression_interception/handler.py

CompressionInterceptionLogger implements the full server-side flow as a CustomLogger:

Hook Role
async_pre_call_deployment_hook Compresses messages, injects retrieval tool, stores cache keyed by litellm_call_id
async_should_run_agentic_loop Returns True when the model response contains litellm_content_retrieve tool calls
async_build_agentic_loop_plan Resolves keys from cache, builds tool_result blocks, returns typed rerun spec

Guard rails built in:

  • Skips if _agentic_loop_depth > 0 (no double-compression on reruns)
  • Skips if a retrieval tool is already present (idempotent)
  • 15-minute in-memory cache TTL with automatic pruning

Enable via proxy config:

litellm_settings:
  callbacks: ["compression_interception"]
  compression_interception_params:
    enabled: true
    compression_trigger: 10000   # compress if input exceeds this many tokens
    # compression_target: 7000   # optional: target tokens after compression (default: 70% of trigger)
    # embedding_model: "..."     # optional: use semantic scoring in addition to BM25

2. Typed AgenticLoopPlan event hook

Modified: litellm/types/integrations/custom_logger.py, litellm/integrations/custom_logger.py

Previously, the agentic loop logic for web-search interception was baked into llm_http_handler.py with no clean extension point. This PR abstracts it into two typed hooks on CustomLogger:

async def async_should_run_agentic_loop(
    self, response, model, messages, tools, stream, custom_llm_provider, kwargs
) -> Tuple[bool, Dict]:
    """Return (should_rerun, context_dict) given the model's first response."""

async def async_build_agentic_loop_plan(
    self, tools, model, messages, response, ...
) -> AgenticLoopPlan:
    """Return a typed spec describing exactly how to rerun: messages, tools, params."""

AgenticLoopPlan carries a request_patch: AgenticLoopRequestPatch that specifies what to override on the next call (messages, tools, optional params, max_tokens). llm_http_handler.py executes the plan without knowing what triggered it.

Benefit: Any CustomLogger can now implement tool-driven multi-LLM loops without touching core handler code. Web-search interception has been ported to this pattern.


3. Anthropic Messages support in litellm.compress()

Modified: litellm/compression/compress.py, litellm/types/compression.py

litellm.compress() now accepts input_type: Literal["anthropic_messages", "openai_chat_completions"].

Key changes for Anthropic input:

Content normalization — Anthropic messages use structured content blocks (text, tool_use, tool_result, thinking). A new _content_to_text() helper extracts only text-bearing fields for BM25/embedding scoring while preserving the original block structure for the compressed output.

Atomic tool exchange spans — An assistant tool_use block and the following user tool_result block are treated as an indivisible pair. If the scorer decides to stub the assistant message, the subsequent tool_result is stubbed too (and vice versa). This prevents the proxy from emitting malformed Anthropic message sequences that the API would reject.

Tool schema remapping — The retrieval tool is built in OpenAI function-tool schema and then remapped to Anthropic's custom tool schema via AnthropicConfig._map_tools() when input_type="anthropic_messages".

New field: compression_skipped_reason: Optional[str] on CompressedResult — logged at DEBUG level to explain why compression was a no-op (e.g. below token threshold, no messages eligible).


4. Streaming agentic loop

New file: litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py

AgenticAnthropicStreamingIterator is an AsyncIterator that:

  1. Yields every SSE chunk to the caller as it arrives (real streaming, no buffering delay)
  2. Reconstructs the full AnthropicMessagesResponse from the SSE events on exhaustion
  3. Runs the reconstructed response through _call_agentic_completion_hooks
  4. If a hook fires, chains the follow-up response as Phase 2 of the same iterator

Previously, streaming requests bypassed the agentic loop entirely. This unblocks streaming support for both compression interception and web-search interception.


Tests

File What it covers
tests/test_litellm/integrations/compression_interception/test_compression_interception_handler.py Full compression+retrieval loop (unit); pre-call hook skipping logic; cache TTL; streaming path
tests/test_litellm/test_compression.py Anthropic input_type cases; atomic tool exchange span handling; budget allocation; compression_skipped_reason
tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py SSE parsing; Phase 1 → Phase 2 chaining; agentic hook dispatch from streaming
tests/test_litellm/integrations/websearch_interception/test_websearch_interception_handler.py Web-search handler updated for new agentic loop hook interface
tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py Agentic loop dispatch from llm_http_handler
tests/test_litellm/proxy/common_utils/test_callback_utils.py compression_interception wired up via callback utils

Type

  • New Featurecompression_interception callback; streaming agentic loop
  • Refactoring — typed AgenticLoopPlan hook abstraction; web-search ported to new pattern
  • Bug Fix — Anthropic structured content block support in compress()

Limitations / Follow-ups

  • The compression cache is in-process memory only — not shared across proxy replicas. A Redis-backed cache would be needed for multi-replica deployments (tracked separately).
  • compression_trigger is measured in tokens using token_counter; the BM25 scorer operates on whitespace-split terms — semantic compression quality improves significantly with embedding_model set.
  • The agentic loop is capped at max_agentic_loops (default 3) with fingerprint-based cycle detection to prevent infinite loops on malformed tool responses.

simplifies how to create logic for tool based multi llm calls
ensures claude code messages can run through proxy easily
@vercel

vercel Bot commented Apr 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 16, 2026 4:08pm

Request Review

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@krrish-berri-2 krrish-berri-2 changed the title Litellm dev 04 14 2026 p1 Prompt Compression - add it to the proxy Apr 15, 2026
@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds server-side prompt compression as a proxy callback (compression_interception), extends litellm.compress() to handle Anthropic Messages structured content, refactors the agentic-loop dispatch into a typed AgenticLoopPlan abstraction, and adds an AgenticAnthropicStreamingIterator for streaming support. Several issues flagged in previous review rounds have been addressed (empty-tools injection, safety-guard propagation, print-statement leaks, wrong monkeypatch target).

  • P1 — Streaming path is not transparent: AgenticAnthropicStreamingIterator yields all Phase 1 SSE bytes (including stop_reason: \"tool_use\" and the litellm_content_retrieve blocks) to the client before Phase 2. Standard anthropic-sdk-python streaming clients stop processing at the first message_stop event and never receive Phase 2, contradicting the architecture diagram's "no tool calls visible to client" guarantee.
  • P1 — Truncation token-count bug for structured Anthropic content: In _select_kept_indices_for_budget, truncated.get(\"content\", \"\") or \"\" passes a list to token_counter's text parameter for messages with block-list content, producing inaccurate budget accounting.

Confidence Score: 3/5

Not safe to merge: the streaming path exposes internal tool calls to clients, contradicting the transparent-compression contract.

Two P1 findings remain: the streaming iterator forwards Phase 1 tool-use blocks to clients (breaking transparency for any standard Anthropic SDK consumer), and the truncation token-count may silently undercount for Anthropic structured-content messages. Previous P0/P1 concerns (empty-tools injection, safety-guard propagation, print leaks, monkeypatch target) appear resolved, which raises the score from what would otherwise be lower.

litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py (streaming transparency), litellm/compression/compress.py (truncation token-count for Anthropic structured content)

Important Files Changed

Filename Overview
litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py New two-phase streaming iterator that yields Phase 1 SSE bytes live and chains Phase 2 on exhaustion; exposes internal litellm_content_retrieve tool calls to standard SSE clients before Phase 2 is sent, and contains a dead list comprehension (line 218–223) that computes but discards a debug summary string.
litellm/compression/compress.py Adds Anthropic-Messages input support: content-block normalisation, atomic tool-exchange span detection, and call_type dispatch; the truncation token-count branch passes a list to token_counter for structured Anthropic content, which may cause silent budget miscalculation.
litellm/integrations/compression_interception/handler.py New CustomLogger implementing pre-call compression, retrieval-tool injection, and agentic-loop plan building; previous issues (empty-tools injection, monkeypatch target) are fixed, in-memory cache is per-process as documented.
litellm/llms/custom_httpx/llm_http_handler.py Refactors agentic-loop dispatch to typed AgenticLoopPlan; safety guards now run outside per-callback try/except (previous issue fixed); streaming path delegates to AgenticAnthropicStreamingIterator; print-statement leaks removed.
litellm/proxy/litellm_pre_call_utils.py Adds generic x-<vendor>-session-id header extraction for chain-ID propagation; broad regex could collide across unrelated tenants sharing a short session ID value.
litellm/integrations/websearch_interception/handler.py Ported to new AgenticLoopPlan pattern; retains async_run_agentic_loop for backward compatibility with the old hook path; no regressions found.
litellm/types/integrations/custom_logger.py Adds AgenticLoopRequestPatch and AgenticLoopPlan typed models for the new hook abstraction; clean, well-typed addition.
litellm/proxy/common_utils/callback_utils.py Wires up compression_interception string callback to CompressionInterceptionLogger.initialize_from_proxy_config; straightforward and well-guarded.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant P as LiteLLM Proxy
    participant CI as CompressionInterceptionLogger
    participant M as Upstream Model
    participant H as AgenticAnthropicStreamingIterator

    C->>P: POST /v1/messages (large context, stream=true)
    P->>CI: async_pre_call_deployment_hook
    CI->>CI: compress() → stub low-score messages
    CI-->>P: kwargs[messages]=compressed, kwargs[tools]=[litellm_content_retrieve]
    P->>M: Compressed request (~20k tokens)
    M-->>H: SSE stream Phase 1
    Note over H,C: Phase 1 bytes yielded in real-time
    H-->>C: SSE: message_start, content (tool_use blocks), message_delta(stop_reason=tool_use), message_stop
    Note over C: Client sees litellm_content_retrieve tool_use — standard SDK stops here
    H->>H: _process_agentic_hooks (rebuild response)
    H->>CI: async_should_run_agentic_loop → True
    H->>CI: async_build_agentic_loop_plan → AgenticLoopPlan
    H->>M: Follow-up request with tool_results (Phase 2)
    M-->>H: SSE stream Phase 2
    H-->>C: SSE: Phase 2 events (final answer)
    Note over C: Standard clients already closed stream after Phase 1 message_stop
Loading

Reviews (12): Last reviewed commit: "Merge branch 'litellm_internal_staging' ..." | Re-trigger Greptile

Comment thread litellm/compression/compress.py Outdated
Comment on lines +34 to +42
if input_type == "anthropic_messages":
# Lazy import to avoid introducing provider transformation imports
# during module import for non-Anthropic call paths.
from litellm.llms.anthropic.chat.transformation import AnthropicConfig

anthropic_tools, _mcp_servers = AnthropicConfig()._map_tools(openai_tools)
return cast(List[dict], anthropic_tools)

return openai_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.

P2 Provider-specific code outside llms/

_build_retrieval_tools imports and calls AnthropicConfig()._map_tools() directly, embedding Anthropic-specific transformation logic inside litellm/compression/ — outside the llms/ directory. Per the project's style guide, provider-specific code should live in llms/ so it can evolve independently. If AnthropicConfig._map_tools is renamed or its return type changes, this will silently break Anthropic-format tool injection with no indication in the compression module itself.

A cleaner approach is to return the retrieval tool in OpenAI format unconditionally and let the existing Anthropic transformation layer (which already converts tools during request building) handle the format conversion.

Rule Used: What: Avoid writing provider-specific code outside... (source)

Comment on lines 391 to 396
@@ -150,7 +395,7 @@ def compress(

emb_scores = embedding_score_messages(

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 Inline import inside function body

embedding_score_messages is imported inside the compress() function body with no circular-import justification. CLAUDE.md requires all imports to be at module level — inline imports inside functions make dependencies harder to trace. The comment in _build_retrieval_tools explains the circular-import rationale for that lazy import, but no such comment or rationale exists here.

Suggested change
from litellm.compression.scoring.embedding_scorer import embedding_score_messages

Move this to the top of the file along with the other litellm.compression.* imports.

Context Used: CLAUDE.md (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!

@veria-ai veria-ai 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.

Medium: Agentic loop depth limits silently bypassed by broad exception handler

This PR adds prompt compression with an agentic loop mechanism that reruns LLM calls server-side. The new depth and fingerprint safety checks (depth >= max_loops, repeated fingerprint detection) raise ValueError, but these are caught by the surrounding except Exception handler that only logs and continues. While the loop does terminate in practice (no agentic response is returned), the safety mechanism is degraded — callers receive the last LLM response with no indication that a safety limit was hit.

  • medium: agentic loop safety checks swallowed — litellm/llms/custom_httpx/llm_http_handler.py
  • low: unbounded in-memory cache — litellm/integrations/compression_interception/handler.py

fingerprint=fingerprint,
)

except Exception as e:

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.

Medium: Agentic loop safety checks silently swallowed

The ValueError raised at lines 4631 and 4635 for repeated fingerprints and exceeded max_agentic_loops is caught by this broad except Exception. Instead of propagating the safety error to the caller, it's logged and discarded. The next callback in the loop (or no callback at all) then runs, and the original response is returned silently.

Consider either re-raising specific safety-check exceptions before this catch-all, or narrowing the catch to exclude ValueError:

            except (ValueError) as e:
                raise
            except Exception as e:

Alternatively, move the depth/fingerprint checks outside the per-callback try/except so they propagate correctly.

# First hook that runs agentic loop wins
return agentic_response

except Exception as e:

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.

Medium: Same swallowed safety check in chat completion path

Same issue as the Anthropic Messages path — the ValueError from depth/fingerprint checks at lines 4797-4803 is caught here and silently discarded. Should be handled consistently with the fix above.

@gitguardian

gitguardian Bot commented Apr 15, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
29203053 Triggered Generic Password 6dfad8b .circleci/config.yml View secret
29203065 Triggered JSON Web Token f123e55 tests/test_litellm/proxy/test_litellm_pre_call_utils.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@codspeed-hq

codspeed-hq Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing litellm_dev_04_14_2026_p1 (7dd39d4) with main (72a461b)

Open in CodSpeed


import litellm
from litellm._logging import verbose_logger
from litellm.integrations.custom_logger import CustomLogger
AgenticLoopPlan,
AgenticLoopRequestPatch,
)
from litellm.types.utils import CallTypes
Comment on lines +41 to +43
from litellm.integrations.compression_interception.handler import (
CompressionInterceptionLogger,
)
Comment on lines +4630 to +4637
if fingerprint in fingerprints:
raise ValueError(
"Agentic loop detected repeated tool-call fingerprint; aborting rerun"
)
if depth >= max_loops:
raise ValueError(
f"Exceeded max_agentic_loops={max_loops} for model={model}"
)

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.

P1 Safety guards swallowed by surrounding try/except

The fingerprint-repeat and max-loops checks raise ValueError inside the try/except Exception block that wraps each callback iteration (line 4697). This means the guard is caught, logged, and the for loop simply continues to the next callback — it never hard-stops the agentic chain. When no subsequent callback handles the tool calls (the typical single-callback case), _call_agentic_completion_hooks returns None, and the caller falls through to returning the original model response — which still contains the raw litellm_content_retrieve tool_use blocks — directly to the client.

The same pattern repeats in the chat-completion path at lines 4797–4804.

Move these checks before the try block, or raise after logging so the error propagates to the caller as a deliberate abort rather than an opaque exception log:

# Outside the per-callback try/except
if depth >= max_loops:
    raise ValueError(
        f"Exceeded max_agentic_loops={max_loops} for model={model}"
    )
if fingerprint in fingerprints:
    raise ValueError(
        "Agentic loop detected repeated tool-call fingerprint; aborting rerun"
    )

@krrish-berri-2
krrish-berri-2 changed the base branch from main to litellm_internal_staging April 16, 2026 15:30

@ishaan-berri ishaan-berri 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.

why not use existing call types ? vs introducing a new var to maintain input_types

compressed = litellm.compress(
messages=messages,
model="gpt-4o",
input_type="openai_chat_completions",

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.

why introduce a new input_type ? We already have call types completion, messages etc

Restores the 34 HTML files under _experimental/out/ to their pre-PR
paths (X/index.html -> X.html). All renames are R100 (content
unchanged); no other files are touched.
- Skip ``kwargs["tools"] = []`` injection when compression is a no-op —
  Anthropic Messages rejects empty tool arrays on requests that did not
  originally declare tools.
- Move agentic-loop safety guards (fingerprint cycle / max depth) out of
  the per-callback try/except so they propagate instead of being swallowed
  by the generic exception handler. Extracted _check_agentic_loop_safety.
- Gate generic ``x-<vendor>-session-id`` capture behind the
  LITELLM_CAPTURE_VENDOR_SESSION_HEADERS env var (off by default) to
  preserve backwards compatibility; explicit x-litellm-* headers are
  unaffected.
- Fix monkeypatch target in pre-call-hook test to patch the actual
  module-level binding
  (litellm.integrations.compression_interception.handler.compress).
- Add regression tests for empty-tools skip and opt-in session capture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generic x-<vendor>-session-id header capture is a new feature and only
runs *after* the explicit x-litellm-trace-id / x-litellm-session-id
checks, so it does not change behavior for any existing caller that was
already using the LiteLLM headers — no backwards-incompatibility to gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the bespoke ``CompressionInputType`` literal and use the existing
``litellm.types.utils.CallTypes`` enum instead.  ``litellm.compress()``
now takes ``call_type: Union[CallTypes, str]`` (default
``CallTypes.completion``) — no new concept to learn, and the enum is
already the way the rest of the codebase talks about request shapes.

Supported values: ``completion`` / ``acompletion`` (OpenAI chat-completions
shape) and ``anthropic_messages`` (Anthropic structured content blocks).

Updated: compress(), the compression_interception handler, tests, docs,
and the two eval scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +218 to +223
[
f"{b.get('type')}({b.get('name', '')})"
if b.get("type") == "tool_use"
else b.get("type")
for b in rebuilt.get("content", [])
]

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.

P1 Dead list comprehension — result is silently discarded

This list comprehension computes a list of content-block type strings but its result is never assigned to a variable or passed to a logger. On every agentic hook invocation the work is done and immediately garbage-collected, and the intended debug information is never surfaced.

The shape of the expression (building a human-readable block summary) strongly suggests it was meant to be passed to verbose_logger.debug(...) but the call was accidentally omitted.

Suggested change
[
f"{b.get('type')}({b.get('name', '')})"
if b.get("type") == "tool_use"
else b.get("type")
for b in rebuilt.get("content", [])
]
block_summary = [
f"{b.get('type')}({b.get('name', '')})"
if b.get("type") == "tool_use"
else b.get("type")
for b in rebuilt.get("content", [])
]
verbose_logger.debug(
"AgenticStreamingIterator: rebuilt response content blocks: %s",
block_summary,
)

@krrish-berri-2
krrish-berri-2 temporarily deployed to integration-postgres April 20, 2026 14:36 — with GitHub Actions Inactive
@krrish-berri-2
krrish-berri-2 temporarily deployed to integration-postgres April 20, 2026 14:37 — with GitHub Actions Inactive
Comment on lines +109 to +127
block = content_blocks.get(idx)
if block and block.get("type") == "tool_use":
partial = block.pop("_partial_json", "")
if partial:
try:
block["input"] = json.loads(partial)
except (json.JSONDecodeError, ValueError):
block["input"] = {"_raw": partial}


def _handle_message_delta(data: Dict, response: Dict) -> None:
delta = data.get("delta", {})
if "stop_reason" in delta:
response["stop_reason"] = delta["stop_reason"]
if "stop_sequence" in delta:
response["stop_sequence"] = delta["stop_sequence"]
usage = data.get("usage", {})
if usage.get("output_tokens") is not None:
response["usage"]["output_tokens"] = usage["output_tokens"]

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.

P1 Streaming exposes internal litellm_content_retrieve tool calls to clients

AgenticAnthropicStreamingIterator unconditionally yields Phase 1 bytes — including the stop_reason: "tool_use" event with litellm_content_retrieve content blocks — to the caller before Phase 2 even begins. Standard Anthropic SDK streaming clients (e.g. Claude Code with anthropic-sdk-python) mark the stream as complete on the first message_stop event and call get_final_message(). They never read Phase 2 bytes and receive a tool_use stop response for an unknown tool, which breaks the interaction.

The PR architecture diagram explicitly states "Final answer (no tool calls visible to client)" but the streaming implementation contradicts this: the internal retrieval tool calls are streamed verbatim to the caller in Phase 1.

A transparent streaming implementation would need to buffer Phase 1 entirely (don't forward it) until it is known whether Phase 2 is needed, and only then stream the final answer to the client — at the cost of initial latency. The current approach is only viable if every downstream client is aware of and can ignore litellm_content_retrieve tool calls.

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.

5 participants