Merge pull request #33767 from BerriAI/litellm_lit_4561_bedrock_passthrough_content_type

fix(proxy): forward Bedrock event-stream content-type on unbuffered passthrough
This commit is contained in:
Mateo Wang 2026-08-18 13:43:47 -07:00 • committed by GitHub
commit 035bd76669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 4 deletions

View file

@ -2039,6 +2039,7 @@ class ProxyBaseLLMRequestProcessing:
return StreamingResponse(
content=generator,
status_code=status.HTTP_200_OK,
media_type=self._passthrough_event_stream_media_type(),
headers=custom_headers,
)
else:
@ -2494,10 +2495,16 @@ class ProxyBaseLLMRequestProcessing:
def _passthrough_event_stream_media_type(self) -> str | None:
"""
Content-type for a buffered passthrough event-stream response, resolved
from the provider handler so the proxy stays provider-agnostic. Mirrors
the upstream content-type the non-streaming path forwards, since the
buffered streaming generator carries no headers of its own.
Content-type for a passthrough event-stream response, resolved from the
provider handler so the proxy stays provider-agnostic. Mirrors the
upstream content-type the non-streaming path forwards, since the
streaming generator carries no headers of its own. Used for both the
buffered (guardrail-rewritten) and the unbuffered relay paths so
clients that enforce the event-stream content-type (e.g. Claude Code on
Bedrock invoke-with-response-stream) see the correct header instead of
no content-type at all, which they fall back to reading as
application/octet-stream. Returns None for providers with no
event-stream media type, leaving the response headers unchanged.
"""
from litellm.llms.pass_through.guardrail_translation.handler import (
LlmPassthroughRouteHandler,

View file

@ -4511,6 +4511,61 @@ class TestAllmPassthroughStreamingProviderGate:
assert streamed == chunks
mock_handler.assert_not_awaited()
@pytest.mark.asyncio
async def test_bedrock_invoke_stream_sets_event_stream_content_type(self, monkeypatch):
"""
Regression for LIT-4561. The unbuffered Bedrock event-stream relay
(invoke-with-response-stream, no post-call guardrail rewriting) must set
content-type: application/vnd.amazon.eventstream instead of emitting no
content-type header at all, which trips Claude Code's content-type guard
added in 2.1.208
"""
processing_obj = self._build_processing_obj(
"bedrock", "model/us.anthropic.claude-sonnet-4-20250514-v1:0/invoke-with-response-stream"
)
chunks = [b"raw-1", b"raw-2"]
with patch.object(
ProxyBaseLLMRequestProcessing,
"_has_post_call_guardrails",
return_value=False,
), patch.object(
ProxyBaseLLMRequestProcessing,
"_has_post_call_guardrails_for_passthrough",
return_value=False,
):
result = await self._run(processing_obj, monkeypatch, chunks)
assert isinstance(result, StreamingResponse)
assert result.media_type == "application/vnd.amazon.eventstream"
assert result.headers["content-type"] == "application/vnd.amazon.eventstream"
streamed = [chunk async for chunk in result.body_iterator]
assert streamed == chunks
@pytest.mark.asyncio
async def test_non_bedrock_stream_keeps_default_content_type(self, monkeypatch):
"""
A provider with no registered event-stream media type must not have one
forced onto its unbuffered stream, so the response default is unchanged
"""
processing_obj = self._build_processing_obj("anthropic")
chunks = [b"chunk-1", b"chunk-2"]
with patch.object(
ProxyBaseLLMRequestProcessing,
"_has_post_call_guardrails",
return_value=False,
), patch.object(
ProxyBaseLLMRequestProcessing,
"_has_post_call_guardrails_for_passthrough",
return_value=False,
):
result = await self._run(processing_obj, monkeypatch, chunks)
assert isinstance(result, StreamingResponse)
assert result.media_type is None
assert "content-type" not in result.headers
class TestResponseCostHeaderForTypedDictResponses:
"""