Skip to content

Handle OpenRouter SSE control noise on Anthropic /v1/messages - #27666

Merged
oss-pr-review-agent-shin[bot] merged 3 commits into
BerriAI:litellm_agent_oss_staging_05_11_2026from
DmitriyAlergant:pr/anthropic-openrouter-sse-noise
May 11, 2026
Merged

Handle OpenRouter SSE control noise on Anthropic /v1/messages#27666
oss-pr-review-agent-shin[bot] merged 3 commits into
BerriAI:litellm_agent_oss_staging_05_11_2026from
DmitriyAlergant:pr/anthropic-openrouter-sse-noise

Conversation

@DmitriyAlergant

@DmitriyAlergant DmitriyAlergant commented May 11, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

None filed.

Pre-Submission checklist

  • Added tests in tests/test_litellm/ (and updated one existing test in tests/pass_through_unit_tests/ whose invariant the new filter changes).
  • make test-unit passes for the touched suites (test_anthropic_sse_filter, test_unit_test_streaming::test_chunk_processor_yields_raw_bytes, test_sse_wrapper).
  • PR is isolated to one problem.

Type

🐛 Bug Fix

Changes

Certain providers (e.g. OpenRouter) implementing "Anthropic-compatible Messages API" inject non-spec SSE control events into a /v1/messages stream, such as:

  • : OPENROUTER PROCESSING comment keep-alives
  • An OpenAI-style data: [DONE] terminator
  • data: lines whose payload is empty or itself a :-prefixed comment

These cause two failure modes in LiteLLM

  1. Logging path crashModelResponseIterator.convert_str_chunk_to_generic_chunk (used by the proxy's pass-through spend-logging reassembly) tries json.loads on every data: payload, blowing up mid-stream. Result: request processed but not logged to SpendLogs
  2. Egress contract break — strict Anthropic SDK clients downstream of the proxy, e.g. yet another LiteLLM instance, treat the noise as malformed input and similarly fail on their end

Two-Pronged Fix

  1. Logging-path tolerance (litellm/llms/anthropic/chat/handler.py) — short-circuit convert_str_chunk_to_generic_chunk on the noise shapes before json.loads.
  2. Egress filter (litellm/proxy/pass_through_endpoints/anthropic_sse_filter.py) — new AnthropicSSENoiseFilter is a per-request, chunk-boundary-safe filter wired into PassThroughStreamingHandler.chunk_processor for EndpointType.ANTHROPIC. It strips the noise from client-bound bytes while leaving raw_bytes (the internal logging path) unfiltered.

The noise predicate is intentionally duplicated at the two sites (proxy and llms/anthropic) rather than imported across the provider/proxy boundary; the in-code comment flags this so the two stay in sync.

Tests

  • tests/test_litellm/proxy/pass_through_endpoints/test_anthropic_sse_filter.py — 13 unit tests covering: clean streams, each noise shape, mid-event and mid-separator chunk boundaries, CRLF separators, ping preservation, flush behavior, and split UTF-8 multibyte sequences across chunk boundaries.
  • tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py — added a logging-path test that feeds a realistic OpenRouter-style stream and asserts _build_complete_streaming_response reassembles usage correctly.
  • tests/pass_through_unit_tests/test_unit_test_streaming.py::test_chunk_processor_yields_raw_bytes — the Anthropic parametrization is updated to use proper SSE-bounded chunks; the filter (correctly) re-bundles at event boundaries, so the per-chunk identity invariant required real SSE data.

yuneng-berri and others added 3 commits May 7, 2026 18:05
Two-layer fix for Anthropic-format streaming through OpenRouter, which
injects non-spec SSE control events (`: OPENROUTER PROCESSING`,
`data: [DONE]`, empty / `:`-prefixed `data:` payloads):

1. Logging-path tolerance — `ModelResponseIterator.convert_str_chunk_to_generic_chunk`
   short-circuits on the noise shapes before `json.loads`, so the proxy's
   spend-logging reassembly no longer crashes mid-stream.

2. Egress filtering — new `AnthropicSSENoiseFilter` strips the same noise
   from client-bound bytes in `PassThroughStreamingHandler.chunk_processor`,
   while leaving `raw_bytes` (used by the internal logging path) unfiltered.
   Filter is per-request and only active for `EndpointType.ANTHROPIC`.

Noise predicate is identical in both sites and must be kept in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing DmitriyAlergant:pr/anthropic-openrouter-sse-noise (06e40a1) with main (e182a5e)

Open in CodSpeed

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.55224% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../proxy/pass_through_endpoints/streaming_handler.py 40.00% 6 Missing ⚠️
...oxy/pass_through_endpoints/anthropic_sse_filter.py 98.11% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a two-layer defence against non-Anthropic SSE control noise (OpenRouter keep-alives, [DONE] terminators, empty/comment data: payloads) that previously caused JSON-parse crashes in the logging path and broke strict Anthropic SDK clients downstream.

  • AnthropicSSENoiseFilter \u2014 a new stateful, chunk-boundary-safe filter wired into PassThroughStreamingHandler.chunk_processor for EndpointType.ANTHROPIC. It strips noise events from the egress byte stream while leaving the unfiltered raw_bytes intact for spend logging.
  • convert_str_chunk_to_generic_chunk gains early-exit guards in handler.py for empty data: payloads, [DONE], and colon-prefixed comment payloads before the json.loads call.
  • Tests cover all noise shapes, chunk-boundary splits (including mid-separator and split UTF-8 sequences), CRLF streams, and the logging-path reassembly scenario against a realistic OpenRouter stream.

Confidence Score: 4/5

Safe to merge; the filter and logging-path guard are well-isolated and thoroughly tested, and raw bytes continue to reach the logging path unchanged.

The core filtering logic in _is_noise_event silently drops any SSE event with no data: lines, relying on a spec invariant that is true today but undocumented in the code. The per-chunk identity test for the Anthropic endpoint was updated in a way that constrains the test data rather than exercising split-chunk rebundling at the integration layer. Neither is a current defect, but both are worth addressing before the pattern expands.

anthropic_sse_filter.py (_is_noise_event spec assumption) and test_unit_test_streaming.py (weakened per-chunk identity assertion for Anthropic)

Important Files Changed

Filename Overview
litellm/proxy/pass_through_endpoints/anthropic_sse_filter.py New AnthropicSSENoiseFilter class — chunk-boundary-safe SSE filter. Logic is sound with good test coverage; one semantic edge case in _is_noise_event noted.
litellm/proxy/pass_through_endpoints/streaming_handler.py Wires AnthropicSSENoiseFilter into chunk_processor for ANTHROPIC endpoint; raw_bytes are captured before filtering, correctly preserving the logging path.
litellm/llms/anthropic/chat/handler.py Adds early-exit guards in convert_str_chunk_to_generic_chunk for empty data, [DONE], and comment payloads before json.loads — straightforward defensive fix.
tests/pass_through_unit_tests/test_unit_test_streaming.py Modified existing test to use per-endpoint raw_chunks; the Anthropic case now uses valid SSE events because the filter re-bundles at event boundaries. The change is justified but the per-chunk identity invariant was weakened.
tests/test_litellm/proxy/pass_through_endpoints/test_anthropic_sse_filter.py 13 new unit tests for AnthropicSSENoiseFilter covering clean streams, each noise shape, chunk-boundary splits, CRLF, flush, and UTF-8 multibyte edge cases.
tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py New test verifies logging path correctly reassembles usage from a realistic OpenRouter-style stream with injected noise events.

Reviews (1): Last reviewed commit: "Handle OpenRouter SSE control noise on A..." | Re-trigger Greptile

Comment on lines +79 to +88
@staticmethod
def _is_noise_event(event: str) -> bool:
for ln in event.splitlines():
stripped = ln.strip()
if not stripped.startswith("data:"):
continue
payload = stripped[5:].strip()
if payload and payload != "[DONE]" and not payload.startswith(":"):
return False
return True

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 Silent drop for data-less events

_is_noise_event returns True for any SSE event that contains no data: lines at all — including event:-only blocks like a bare event: ping\n or any future Anthropic event type that only carries event: and id: fields with no accompanying data: line. For the current Anthropic SSE spec every real event includes data:, so this is unlikely to bite today, but a future spec extension without a data: line would be silently dropped with no log or error. At minimum, a short comment at the end of the method spelling out the spec invariant it relies on (# Relies on Anthropic SSE spec invariant: every meaningful event carries a data: line) would make this assumption explicit and easier to catch in a future review.

Comment on lines +46 to 58
),
# Anthropic pass-through enables an SSE noise filter that re-bundles
# bytes at event boundaries, so each input chunk must be a complete
# SSE event for the per-chunk identity assertion to hold.
(
EndpointType.ANTHROPIC,
"/v1/messages",
[
b'event: message_start\ndata: {"type":"message_start","message":{"id":"m_1"}}\n\n',
b'event: content_block_delta\ndata: {"type":"content_block_delta","delta":{"text":"hi"}}\n\n',
b'event: message_stop\ndata: {"type":"message_stop"}\n\n',
],
),

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 Per-chunk identity invariant weakened for Anthropic

The original test asserted that every chunk yielded by chunk_processor is present in the original raw_chunks list (chunk in raw_chunks). This held because the old ANTHROPIC parameter used the same non-SSE byte blobs as VERTEX_AI. The change is justified — the noise filter re-bundles at event boundaries, so non-SSE inputs produce different output. However, the updated test now requires the author to supply chunks that are already complete SSE events, which implicitly constrains the test data rather than the production code. A complementary test that feeds mid-event split inputs and confirms the reassembled bytes match the total stream (like test_buffers_across_chunk_boundary_mid_event in test_anthropic_sse_filter.py) would close the gap for the chunk_processor integration layer.

Rule Used: What: Flag any modifications to existing tests and... (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!

@oss-pr-review-agent-shin
oss-pr-review-agent-shin Bot changed the base branch from main to litellm_agent_oss_staging_05_11_2026 May 11, 2026 19:19
@oss-pr-review-agent-shin
oss-pr-review-agent-shin Bot merged commit 7471728 into BerriAI:litellm_agent_oss_staging_05_11_2026 May 11, 2026
50 of 51 checks passed
@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: Squash-merged into staging branch litellm_agent_oss_staging_05_11_2026. Staging PR: #27664


Triage Summary
Gathered PR data only — the triage LLM step did not produce a valid report, so failing-check classification and prior-signal reconciliation were skipped. 281 line(s) across 6 file(s) (+272/-9).

281 lines across 6 files (+272 / -9)

Merge Confidence: 5/5 ✅ READY
Ready to ship.

All checks green. Greptile 4/5, no blocking pattern findings, no CircleCI runs (OSS-typical).

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