This commit is contained in:
devin-ai-integration[bot] 2026-08-27 17:54:29 -05:00 committed by GitHub
commit a508d02e4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 3 deletions

View file

@ -0,0 +1,32 @@
"""
Streaming handler for Amazon Bedrock Mantle (OpenAI-compatible API).
Bedrock Mantle emits a fresh ``id`` on every SSE chunk, which violates the
OpenAI streaming contract that all chunks in one response share a single
``id``. Clients that validate this (e.g. the openai-go SDK's
``ChatCompletionAccumulator``) drop every chunk after the first, losing
content and tool-call arguments. We pin the id from the first chunk that
carries one and reuse it for the rest of the stream.
"""
from typing import Optional
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIChatCompletionStreamingHandler,
)
from litellm.types.utils import ModelResponseStream
class BedrockMantleChatCompletionStreamingHandler(OpenAIChatCompletionStreamingHandler):
def __init__(self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False):
super().__init__(streaming_response=streaming_response, sync_stream=sync_stream, json_mode=json_mode)
self._response_id: Optional[str] = None
def chunk_parser(self, chunk: dict) -> ModelResponseStream:
parsed = super().chunk_parser(chunk)
raw_id = chunk.get("id")
if self._response_id is None and raw_id:
self._response_id = raw_id
if self._response_id is not None:
parsed.id = self._response_id
return parsed

View file

@ -111,11 +111,11 @@ class BedrockMantleChatConfig(BedrockMantleAuthMixin, OpenAILikeChatConfig):
sync_stream: bool,
json_mode: bool | None = False,
) -> Any:
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIChatCompletionStreamingHandler,
from litellm.llms.bedrock_mantle.chat.handler import (
BedrockMantleChatCompletionStreamingHandler,
)
return OpenAIChatCompletionStreamingHandler(
return BedrockMantleChatCompletionStreamingHandler(
streaming_response=streaming_response,
sync_stream=sync_stream,
json_mode=json_mode,

View file

@ -666,6 +666,56 @@ def test_gemma_4_bedrock_mantle_model_metadata(
)
def test_streaming_handler_pins_first_chunk_id():
"""Bedrock Mantle emits a fresh id on every SSE chunk; the OpenAI contract
requires all chunks in a response to share one id, so the handler must pin
the first chunk's id onto every subsequent chunk."""
from litellm.llms.bedrock_mantle.chat.handler import (
BedrockMantleChatCompletionStreamingHandler,
)
handler = BedrockMantleChatCompletionStreamingHandler(
streaming_response=iter([]), sync_stream=True
)
raw_chunks = [
{
"id": f"chatcmpl-{i}",
"created": 1,
"model": "openai.gpt-5.5",
"choices": [{"index": 0, "delta": {"content": token}}],
}
for i, token in enumerate(["Hel", "lo", "!"])
]
parsed_ids = [handler.chunk_parser(chunk).id for chunk in raw_chunks]
assert parsed_ids == ["chatcmpl-0", "chatcmpl-0", "chatcmpl-0"]
def test_streaming_handler_pins_id_when_first_chunk_lacks_one():
"""Some backends omit id on an initial role-only chunk; the handler should
pin the first id it actually sees and keep it stable afterwards."""
from litellm.llms.bedrock_mantle.chat.handler import (
BedrockMantleChatCompletionStreamingHandler,
)
handler = BedrockMantleChatCompletionStreamingHandler(
streaming_response=iter([]), sync_stream=True
)
raw_chunks = [
{"created": 1, "model": "openai.gpt-5.5", "choices": [{"index": 0, "delta": {"role": "assistant"}}]},
{"id": "chatcmpl-real", "created": 1, "model": "openai.gpt-5.5", "choices": [{"index": 0, "delta": {"content": "Hi"}}]},
{"id": "chatcmpl-other", "created": 1, "model": "openai.gpt-5.5", "choices": [{"index": 0, "delta": {"content": "!"}}]},
]
parsed_ids = [handler.chunk_parser(chunk).id for chunk in raw_chunks]
assert parsed_ids[1] == "chatcmpl-real"
assert parsed_ids[2] == "chatcmpl-real"
@pytest.mark.parametrize(
"model_id",
[