Skip to content

fix(proxy): use e.request_data for logging_obj in ModifyResponseException streaming passthrough - #30701

Closed
seph-barker wants to merge 2 commits into
BerriAI:litellm_oss_stagingfrom
predibase:joseph/fix-streaming-block-logging-obj
Closed

fix(proxy): use e.request_data for logging_obj in ModifyResponseException streaming passthrough#30701
seph-barker wants to merge 2 commits into
BerriAI:litellm_oss_stagingfrom
predibase:joseph/fix-streaming-block-logging-obj

Conversation

@seph-barker

Copy link
Copy Markdown
Contributor

Relevant issues

A guardrail that blocks a streaming request pre-call (by raising ModifyResponseException or RejectedRequestError) 500s the request instead of streaming the violation message back as a 200.

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

The bug

When a guardrail blocks a streaming request pre-call, chat_completion's except ModifyResponseException / except RejectedRequestError handlers stream the violation message back as a 200 by building a CustomStreamWrapper:

except ModifyResponseException as e:
    _data = e.request_data
    ...
    _streaming_response = litellm.CustomStreamWrapper(
        completion_stream=_iterator,
        model=e.model,
        custom_llm_provider="cached_response",
        logging_obj=data.get("litellm_logging_obj", None),   # <-- outer body
    )

data here is the outer request body returned by _read_request_body. It never carries litellm_logging_obj: the processor's data dict diverges from the outer body at function_setup (which returns a fresh kwargs dict), and litellm_logging_obj is attached only to that processor copy. So data.get("litellm_logging_obj") is None, and CustomStreamWrapper.__init__ immediately dereferences it:

litellm_params: GenericLiteLLMParams = GenericLiteLLMParams(
    **self.logging_obj.model_call_details.get("litellm_params", {})
)
AttributeError: 'NoneType' object has no attribute 'model_call_details'

The non-streaming path is unaffected (it returns a ModelResponse directly), and the anthropic / responses-API passthrough paths are unaffected (they stream via a plain SSE generator, not CustomStreamWrapper). Only streaming + pre-call block hits this.

The fix

_data = e.request_data is already bound two lines above and is the processor's data dict, which does carry litellm_logging_obj. Read logging_obj from _data in both streaming passthrough handlers (ModifyResponseException and RejectedRequestError).

Added a regression test (tests/test_litellm/proxy/test_modify_response_streaming_passthrough.py) that drives chat_completion's ModifyResponseException streaming branch with an outer body lacking litellm_logging_obj and asserts the CustomStreamWrapper receives the logging object from e.request_data (it would be None without the fix).

@codspeed-hq

codspeed-hq Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Congrats! CodSpeed is installed 🎉

🆕 16 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a NoneType AttributeError that caused streaming requests blocked pre-call by a guardrail to 500 instead of streaming the violation message back as a 200. The root cause was CustomStreamWrapper receiving logging_obj=None because both handlers read litellm_logging_obj from the outer request body (data), which never carries it — only the processor's data dict (_data = e.request_data) does.

  • proxy_server.py: In the except ModifyResponseException and except RejectedRequestError streaming branches, data.get(\"litellm_logging_obj\", None) is replaced with _data.get(\"litellm_logging_obj\", None). _data is already bound from e.request_data two lines above in each handler.
  • New test file: A shared _run_streaming_block_and_get_wrapper helper drives both exception paths with an outer body that intentionally lacks litellm_logging_obj, then asserts CustomStreamWrapper receives the sentinel object from e.request_data. Both ModifyResponseException and RejectedRequestError are covered, closing the regression gap flagged in the previous review.

Confidence Score: 5/5

Safe to merge — the change is two surgical one-line substitutions in existing exception handlers, each using a variable that is already bound and validated in context, with no impact on the non-streaming or non-blocked-request paths.

Both changed lines read _data (the processor's data dict) instead of data (the outer request body) for litellm_logging_obj. _data = e.request_data is bound and non-null before each change site, and the key is the one that actually carries the logging object. The new test file exercises both exception-type branches with an outer body that lacks the key, confirming the fix and guarding against future regressions on both paths.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py One-line fix in each of the ModifyResponseException and RejectedRequestError streaming handlers: reads litellm_logging_obj from _data (the processor's data dict, which carries the object) instead of data (the outer request body, which never does). Both context variables are confirmed to be bound correctly before the change.
tests/test_litellm/proxy/test_modify_response_streaming_passthrough.py New regression tests for both streaming passthrough handlers; uses a shared _run_streaming_block_and_get_wrapper helper, patches all network I/O, and asserts that CustomStreamWrapper receives the sentinel logging object from e.request_data rather than None. Both ModifyResponseException and RejectedRequestError paths are covered.

Reviews (3): Last reviewed commit: "test(proxy): cover RejectedRequestError ..." | Re-trigger Greptile

Comment thread tests/test_litellm/proxy/test_modify_response_streaming_passthrough.py Outdated
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@seph-barker
seph-barker changed the base branch from main to litellm_oss_branch June 18, 2026 00:16
@seph-barker
seph-barker force-pushed the joseph/fix-streaming-block-logging-obj branch from 6906a6b to 7004638 Compare June 18, 2026 00:19
@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the PR! Triggering Greptile for a code review:

@greptileai

@Sameerlite
Sameerlite changed the base branch from litellm_oss_branch to litellm_oss_staging June 18, 2026 12:36
@Sameerlite

Copy link
Copy Markdown
Contributor

@seph-barker please resolve the conflicts

…tion streaming passthrough

When a guardrail blocks a streaming request pre-call by raising
ModifyResponseException (or RejectedRequestError), chat_completion streams the
violation message back as a 200 by building a CustomStreamWrapper. It read the
logging object from the outer request body (`data.get("litellm_logging_obj")`),
but that dict never carries litellm_logging_obj -- it diverges from the
processor's data at function_setup, and only the processor copy (exposed as
e.request_data, already bound to `_data` here) gets the logging object
attached. CustomStreamWrapper.__init__ then dereferences
`logging_obj.model_call_details` on None and 500s the request with
"AttributeError: 'NoneType' object has no attribute 'model_call_details'".

Read logging_obj from `_data` (= e.request_data) in both streaming
passthrough handlers so the refusal streams correctly. The non-streaming and
the anthropic/responses passthrough paths were unaffected.

Adds a regression test asserting the wrapper receives the logging object from
e.request_data rather than None.
The streaming logging_obj fix was applied to both the ModifyResponseException
and RejectedRequestError handlers, but only the former had a regression test.
Extract a shared helper and add a parallel test for the RejectedRequestError
streaming path so both handlers stay guarded against the None-logging_obj crash.
@seph-barker
seph-barker force-pushed the joseph/fix-streaming-block-logging-obj branch from 7004638 to 5838387 Compare June 18, 2026 16:37
@seph-barker

Copy link
Copy Markdown
Contributor Author

@seph-barker please resolve the conflicts

Done, thanks for looking

@ishaan-berri
ishaan-berri changed the base branch from litellm_oss_staging to litellm_internal_staging June 18, 2026 17:04
@ishaan-berri
ishaan-berri changed the base branch from litellm_internal_staging to litellm_oss_branch June 18, 2026 17:05
@ishaan-berri
ishaan-berri changed the base branch from litellm_oss_branch to litellm_oss_staging June 18, 2026 17:05
@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for the fix, @seph-barker! The root cause explanation is thorough and the regression test covers both exception handlers. Triggering a fresh Greptile review on the latest commit since the current review covers an earlier SHA.

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

Thanks for the contribution! This has been merged (we have to make a copy of your branch to run e2e tests)

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