litellm - ✅(Solved) Fix [Bug]: FallbackStreamWrapper / SyncFallbackStreamWrapper hide attributes from third-party instrumentation (e.g. ddtrace) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#23357Fetched 2026-04-08 00:37:13
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Participants
Timeline (top)
labeled ×2cross-referenced ×1

SyncFallbackStreamWrapper and FallbackStreamWrapper (introduced in PR #22375) subclass CustomStreamWrapper but hide attributes that third-party instrumentation libraries add to the original stream object. This causes AttributeError at runtime when those libraries access their injected attributes on the returned wrapper.

Concrete case: ddtrace (Datadog's APM library) wraps CustomStreamWrapper in a TracedStream (wrapt.ObjectProxy) that adds a .handler attribute. When the Router wraps this in SyncFallbackStreamWrapper, .handler is no longer accessible.

Error Message

import litellm

router = litellm.Router( model_list=[ {"model_name": "gemini", "litellm_params": {"model": "vertex_ai/gemini-2.0-flash", "vertex_project": "...", "vertex_location": "..."}}, ], fallbacks=[{"gemini": ["gemini"]}], )

With ddtrace LLM Observability enabled (DD_LLMOBS_ENABLED=1, ddtrace-run):

response = router.completion(model="gemini", messages=[{"role": "user", "content": "hi"}], stream=True)

→ AttributeError: 'SyncFallbackStreamWrapper' object has no attribute 'handler'

Root Cause

The call chain with ddtrace enabled:

  1. Router.completion() calls internal litellm.completion() → returns CustomStreamWrapper
  2. ddtrace patches litellm.completion and wraps the result in TracedStream(wrapt.ObjectProxy) — this adds .handler
  3. Router wraps this in SyncFallbackStreamWrapper(CustomStreamWrapper) — the new outer wrapper
  4. traced_router_completion (ddtrace's Router.completion patch) does resp.handlerAttributeError

Step 4 fails because SyncFallbackStreamWrapper inherits from CustomStreamWrapper which has its own __init__ that doesn't know about .handler. The TracedStream is buried inside _wrapped_response / model_response and its attributes are hidden.

Same issue exists for the async FallbackStreamWrapper (unreachable in current ddtrace since Router.acompletion is not yet patched, but will break when it is).

Fix Action

Fix / Workaround

  1. Router.completion() calls internal litellm.completion() → returns CustomStreamWrapper
  2. ddtrace patches litellm.completion and wraps the result in TracedStream(wrapt.ObjectProxy) — this adds .handler
  3. Router wraps this in SyncFallbackStreamWrapper(CustomStreamWrapper) — the new outer wrapper
  4. traced_router_completion (ddtrace's Router.completion patch) does resp.handlerAttributeError

Same issue exists for the async FallbackStreamWrapper (unreachable in current ddtrace since Router.acompletion is not yet patched, but will break when it is).

PR fix notes

PR #23358: fix: add getattr proxy to FallbackStreamWrapper and SyncFallbackStreamWrapper

Description (problem / solution / changelog)

Summary

SyncFallbackStreamWrapper and FallbackStreamWrapper (PR #22375) subclass CustomStreamWrapper but hide attributes that third-party instrumentation libraries add to the original stream. This causes AttributeError when those libraries access their injected attributes on the returned wrapper.

Concrete case: ddtrace wraps the inner CustomStreamWrapper in a TracedStream (wrapt.ObjectProxy) that adds a .handler attribute. The Router then wraps this in SyncFallbackStreamWrapper, hiding .handler:

Router.completion()
  → litellm.completion() → CustomStreamWrapper
  → ddtrace wraps in TracedStream (has .handler)
  → Router wraps in SyncFallbackStreamWrapper (hides .handler)
  → ddtrace does resp.handler → AttributeError

Changes

litellm/router.py

  • FallbackStreamWrapper: Store model_response as _wrapped_response before super().__init__(). Add __getattr__ that proxies to _wrapped_response.
  • SyncFallbackStreamWrapper: Same.

__getattr__ is only called when normal attribute lookup fails, so it doesn't interfere with CustomStreamWrapper's own attributes.

tests/test_litellm/test_router.py

  • test_acompletion_streaming_iterator_proxies_unknown_attrs — async wrapper proxies .handler
  • test_completion_streaming_iterator_proxies_unknown_attrs — sync wrapper proxies .handler

Fixes #23357

Changed files

  • litellm/router.py (modified, +12/-1)
  • tests/test_litellm/test_router.py (modified, +151/-0)

Code Example

import litellm

router = litellm.Router(
    model_list=[
        {"model_name": "gemini", "litellm_params": {"model": "vertex_ai/gemini-2.0-flash", "vertex_project": "...", "vertex_location": "..."}},
    ],
    fallbacks=[{"gemini": ["gemini"]}],
)

# With ddtrace LLM Observability enabled (DD_LLMOBS_ENABLED=1, ddtrace-run):
response = router.completion(model="gemini", messages=[{"role": "user", "content": "hi"}], stream=True)
# → AttributeError: 'SyncFallbackStreamWrapper' object has no attribute 'handler'

---

class SyncFallbackStreamWrapper(CustomStreamWrapper):
    def __init__(self, sync_generator: Generator):
        self._wrapped_response = model_response
        super().__init__(...)
        ...

    def __getattr__(self, name: str):
        return getattr(self._wrapped_response, name)
RAW_BUFFERClick to expand / collapse

Summary

SyncFallbackStreamWrapper and FallbackStreamWrapper (introduced in PR #22375) subclass CustomStreamWrapper but hide attributes that third-party instrumentation libraries add to the original stream object. This causes AttributeError at runtime when those libraries access their injected attributes on the returned wrapper.

Concrete case: ddtrace (Datadog's APM library) wraps CustomStreamWrapper in a TracedStream (wrapt.ObjectProxy) that adds a .handler attribute. When the Router wraps this in SyncFallbackStreamWrapper, .handler is no longer accessible.

Steps to Reproduce

import litellm

router = litellm.Router(
    model_list=[
        {"model_name": "gemini", "litellm_params": {"model": "vertex_ai/gemini-2.0-flash", "vertex_project": "...", "vertex_location": "..."}},
    ],
    fallbacks=[{"gemini": ["gemini"]}],
)

# With ddtrace LLM Observability enabled (DD_LLMOBS_ENABLED=1, ddtrace-run):
response = router.completion(model="gemini", messages=[{"role": "user", "content": "hi"}], stream=True)
# → AttributeError: 'SyncFallbackStreamWrapper' object has no attribute 'handler'

Expected: Streaming works — attributes from the original stream are accessible through the wrapper. Actual: AttributeError on first access of .handler by ddtrace's traced_router_completion.

Root Cause

The call chain with ddtrace enabled:

  1. Router.completion() calls internal litellm.completion() → returns CustomStreamWrapper
  2. ddtrace patches litellm.completion and wraps the result in TracedStream(wrapt.ObjectProxy) — this adds .handler
  3. Router wraps this in SyncFallbackStreamWrapper(CustomStreamWrapper) — the new outer wrapper
  4. traced_router_completion (ddtrace's Router.completion patch) does resp.handlerAttributeError

Step 4 fails because SyncFallbackStreamWrapper inherits from CustomStreamWrapper which has its own __init__ that doesn't know about .handler. The TracedStream is buried inside _wrapped_response / model_response and its attributes are hidden.

Same issue exists for the async FallbackStreamWrapper (unreachable in current ddtrace since Router.acompletion is not yet patched, but will break when it is).

Suggested Fix

Add _wrapped_response + __getattr__ to both wrapper classes to proxy unknown attribute lookups to the original stream:

class SyncFallbackStreamWrapper(CustomStreamWrapper):
    def __init__(self, sync_generator: Generator):
        self._wrapped_response = model_response
        super().__init__(...)
        ...

    def __getattr__(self, name: str):
        return getattr(self._wrapped_response, name)

This is transparent to existing behavior — __getattr__ is only called when normal attribute lookup fails, so it doesn't interfere with CustomStreamWrapper's own attributes.

Impact

Any user running LiteLLM Router with streaming + ddtrace LLM Observability enabled (DD_LLMOBS_ENABLED=1) hits this on every streaming call. Non-streaming and non-ddtrace usage is unaffected.

What part of LiteLLM is this about?

SDK (litellm Python package)

What LiteLLM version are you on?

1.82.1+ (any version with PR #22375 merged)

extent analysis

Fix Plan

To resolve the issue, we need to modify the SyncFallbackStreamWrapper and FallbackStreamWrapper classes to proxy unknown attribute lookups to the original stream. Here are the steps:

  • Modify the __init__ method of SyncFallbackStreamWrapper and FallbackStreamWrapper to store the _wrapped_response attribute.
  • Implement the __getattr__ method in both classes to proxy unknown attribute lookups.

Example code:

class SyncFallbackStreamWrapper(CustomStreamWrapper):
    def __init__(self, sync_generator: Generator):
        self._wrapped_response = model_response
        super().__init__(...)
        ...

    def __getattr__(self, name: str):
        return getattr(self._wrapped_response, name)

class FallbackStreamWrapper(CustomStreamWrapper):
    def __init__(self, async_generator: AsyncGenerator):
        self._wrapped_response = model_response
        super().__init__(...)
        ...

    def __getattr__(self, name: str):
        return getattr(self._wrapped_response, name)

Verification

To verify the fix, you can run the following test:

router = litellm.Router(
    model_list=[
        {"model_name": "gemini", "litellm_params": {"model": "vertex_ai/gemini-2.0-flash", "vertex_project": "...", "vertex_location": "..."}},
    ],
    fallbacks=[{"gemini": ["gemini"]}],
)

response = router.completion(model="gemini", messages=[{"role": "user", "content": "hi"}], stream=True)
print(response.handler)  # Should not raise AttributeError

Extra Tips

  • Make sure to update both SyncFallbackStreamWrapper and FallbackStreamWrapper classes to ensure the fix works for both synchronous and asynchronous streaming.
  • The __getattr__ method is only called when normal attribute lookup fails, so it doesn't interfere with CustomStreamWrapper's own attributes.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: FallbackStreamWrapper / SyncFallbackStreamWrapper hide attributes from third-party instrumentation (e.g. ddtrace) [1 pull requests, 1 participants]