fix(proxy): guard against malformed SSE chunks in Anthropic passthrough logging#24343
Conversation
…gh logging Fixes BerriAI#24281 When proxying models like anthropic/deepseek-reasoner, certain SSE stream events contain JSON structures that the Anthropic chunk parser cannot handle (e.g. thinking blocks, extended metadata), producing: streaming_handler.py:197 - Error in _route_streaming_logging_to_handler: Expecting value: line 1 column 3 (char 2) Previously _build_complete_streaming_response only caught StopIteration / StopAsyncIteration from convert_str_chunk_to_generic_chunk. Any other exception (json.JSONDecodeError, KeyError, …) propagated all the way up and was silently swallowed by the broad except in _route_streaming_logging_to_handler — but not before emitting a noisy ERROR-level log on every request. Fix: add an except-Exception guard around the per-event conversion loop so that individual unparseable events are skipped with a DEBUG-level log while all valid events in the same stream are still processed and logged correctly. The overall logging call is no longer aborted by a single bad event.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
NIK-TIGER-BILL seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Greptile SummaryThis PR adds a defensive
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py | Adds a broad except Exception guard in _build_complete_streaming_response to skip unparseable SSE events at DEBUG level rather than propagating errors through the logging stack; logical fix but no automated test is included. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[all_chunks loop] --> B[_split_sse_chunk_into_events]
B --> C[individual_events loop]
C --> D[convert_str_chunk_to_generic_chunk]
D -->|Success| E[append to all_openai_chunks]
E --> C
D -->|StopIteration / StopAsyncIteration| F[break inner loop]
F --> A
D -->|Exception - pre-fix| G[propagates to _route_streaming_logging_to_handler]
G --> H[ERROR log + logging call aborted]
D -->|Exception - post-fix NEW| I[DEBUG log, skip event]
I --> C
A -->|done| J[stream_chunk_builder all_openai_chunks]
J --> K[complete_streaming_response]
Last reviewed commit: "fix(proxy): guard ag..."
| except Exception as e: | ||
| # Some models (e.g. anthropic/deepseek-reasoner) may emit SSE | ||
| # events with non-standard JSON or extra fields that the | ||
| # Anthropic chunk parser cannot handle. Silently skip | ||
| # individual malformed events so that valid events in the same | ||
| # stream are still logged rather than dropping the entire | ||
| # logging call with an unhandled JSONDecodeError. | ||
| verbose_proxy_logger.debug( | ||
| "Skipping unparseable Anthropic SSE event during passthrough " | ||
| "logging. Error: %s. Event (truncated): %.200s", | ||
| str(e), | ||
| event_str, | ||
| ) | ||
| continue |
There was a problem hiding this comment.
Consider narrowing the exception type
The bare except Exception will silently swallow any unexpected error that occurs inside convert_str_chunk_to_generic_chunk — including AttributeError, TypeError, ValueError, and similar programming errors that are unrelated to malformed SSE data. For a logging path this is low-risk, but it does make future bugs harder to detect.
The PR description specifically calls out json.JSONDecodeError and KeyError as the known failure modes. Catching only those (plus perhaps ValueError for other parse errors) would let genuine coding mistakes surface while still suppressing the known noisy failures:
except (json.JSONDecodeError, KeyError, ValueError) as e:
verbose_proxy_logger.debug(
"Skipping unparseable Anthropic SSE event during passthrough "
"logging. Error: %s. Event (truncated): %.200s",
str(e),
event_str,
)
continueIf the intent is truly "skip everything no matter what," a short inline comment explaining that reasoning would help future readers understand it's intentional.
| except (StopIteration, StopAsyncIteration): | ||
| break |
There was a problem hiding this comment.
break only exits the inner loop
When StopIteration or StopAsyncIteration is raised by convert_str_chunk_to_generic_chunk, the existing break only exits the inner for event_str in individual_events: loop. The outer for _chunk_str in all_chunks: loop then proceeds to the next chunk, which is unlikely to be correct — a StopIteration from the iterator signals that the stream is done, so processing should stop entirely.
This is pre-existing behavior (not introduced by this PR), but the new except Exception block directly follows it and the addition of continue makes the control-flow choices here more visible. It would be worth auditing whether the outer loop should also be exited when StopIteration is caught, for example:
except (StopIteration, StopAsyncIteration):
break # exits inner loop; consider `goto` outer loop or a flagA simple fix with a sentinel flag:
stream_exhausted = False
for _chunk_str in all_chunks:
if stream_exhausted:
break
individual_events = ...
for event_str in individual_events:
try:
...
except (StopIteration, StopAsyncIteration):
stream_exhausted = True
break
except Exception as e:
...
continue
Problem
Closes #24281
When proxying models like
anthropic/deepseek-reasonervia the LiteLLM proxy, certain SSE stream events contain JSON structures (e.g. thinking-block events) that the Anthropic chunk parser cannot handle. This produces a noisy ERROR-level log on every streaming request:Root Cause
In
_build_complete_streaming_response, the per-event loop insideAnthropicPassthroughLoggingHandleronly caughtStopIteration/StopAsyncIteration. Any other exception (json.JSONDecodeError,KeyError, …) fromconvert_str_chunk_to_generic_chunkpropagated all the way up to the broadexcept Exceptionin_route_streaming_logging_to_handler— which swallowed it silently but not before logging an ERROR and aborting the entire logging call for that request.Solution
Add an
except Exceptionguard around the per-event conversion in_build_complete_streaming_response:DEBUG-level logTesting
deepseek-reasonerresponse that includes thinking-block SSE events