Merge pull request #42795 from BerriAI/litellm_/patch-gang-backport-6da70a
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
LiteLLM Rust / release wheel (push) Has been cancelled

chore(release): backport #42607 to stable/1.101.x and cut 1.101.2
This commit is contained in:
yuneng-jiang 2026-09-23 15:54:05 -07:00 • committed by GitHub
commit ccb327f032
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 104 additions and 16 deletions

View file

@ -780,9 +780,7 @@ class AmazonAnthropicClaudeMessagesConfig(
aws_decoder: Final = AmazonAnthropicClaudeMessagesStreamDecoder(
model=model,
)
completion_stream: Final = aws_decoder.aiter_bytes(
httpx_response.aiter_bytes(chunk_size=aws_decoder.DEFAULT_CHUNK_SIZE)
)
completion_stream: Final = aws_decoder.aiter_bytes(httpx_response.aiter_bytes())
# Convert decoded Bedrock events to Server-Sent Events expected by Anthropic clients.
return self.bedrock_sse_wrapper(
completion_stream=completion_stream,
@ -929,16 +927,6 @@ class AmazonAnthropicClaudeMessagesConfig(
class AmazonAnthropicClaudeMessagesStreamDecoder(AWSEventStreamDecoder):
def __init__(
self,
model: str,
) -> None:
"""
Iterator to return Bedrock invoke response in anthropic /messages format
"""
super().__init__(model=model)
self.DEFAULT_CHUNK_SIZE = 1024
def _chunk_parser(self, chunk_data: dict) -> GChunk | ModelResponseStream | dict:
"""
Parse the chunk data into anthropic /messages format

View file

@ -1,6 +1,6 @@
[project]
name = "litellm"
version = "1.101.1"
version = "1.101.2"
description = "Library to easily interface with LLM API providers"
readme = "README.md"
requires-python = ">=3.10, <3.15"
@ -328,7 +328,7 @@ members = ["enterprise", "litellm-proxy-extras"]
profile = "black"
[tool.commitizen]
version = "1.101.1"
version = "1.101.2"
version_files = [
"pyproject.toml:^version",
]

View file

@ -1,11 +1,17 @@
import asyncio
import base64
import copy
import json
import os
import struct
import zlib
from datetime import datetime
from types import SimpleNamespace
from collections.abc import AsyncIterator, Mapping, Sequence
from typing import Final
from unittest.mock import Mock
import httpx
import pytest
# Ensure the project root is on the import path so `litellm` can be imported when
@ -3336,3 +3342,97 @@ def test_bedrock_messages_strips_effort_but_keeps_format_for_sonnet_4_5(local_mo
)
assert result.get("output_config") == {"format": schema_format}
def _bedrock_event_frame(payload: Mapping[str, object]) -> bytes:
def _header(name: str, value: str) -> bytes:
return (
bytes([len(name)])
+ name.encode()
+ bytes([7])
+ struct.pack(">H", len(value))
+ value.encode()
)
headers: Final = (
_header(":message-type", "event")
+ _header(":event-type", "chunk")
+ _header(":content-type", "application/json")
)
body: Final = json.dumps(
{"bytes": base64.b64encode(json.dumps(payload).encode()).decode()}
).encode()
prelude: Final = struct.pack(">II", 12 + len(headers) + len(body) + 4, len(headers))
prelude_crc: Final = struct.pack(">I", zlib.crc32(prelude))
message_crc: Final = struct.pack(">I", zlib.crc32(prelude + prelude_crc + headers + body))
return prelude + prelude_crc + headers + body + message_crc
class _GatedAsyncByteStream(httpx.AsyncByteStream):
def __init__(self, chunks: Sequence[bytes], gate: asyncio.Event) -> None:
self._chunks = chunks
self._gate = gate
async def __aiter__(self) -> AsyncIterator[bytes]:
yield self._chunks[0]
await self._gate.wait()
for chunk in self._chunks[1:]:
yield chunk
async def aclose(self) -> None:
return None
@pytest.mark.asyncio
async def test_get_async_streaming_response_iterator_yields_small_frame_before_upstream_pauses():
gate: Final = asyncio.Event()
response: Final = httpx.Response(
200,
stream=_GatedAsyncByteStream(
chunks=(
_bedrock_event_frame(
{
"type": "message_start",
"message": {
"id": "msg_test",
"type": "message",
"role": "assistant",
"content": [],
"model": "us.anthropic.claude-sonnet-4-6",
"usage": {"input_tokens": 3, "output_tokens": 1},
},
}
),
_bedrock_event_frame(
{
"type": "message_stop",
"usage": {"input_tokens": 3, "output_tokens": 9},
}
),
),
gate=gate,
),
)
iterator: Final = AmazonAnthropicClaudeMessagesConfig().get_async_streaming_response_iterator(
model="us.anthropic.claude-sonnet-4-6",
httpx_response=response,
request_body={"model": "us.anthropic.claude-sonnet-4-6"},
litellm_logging_obj=LiteLLMLoggingObj(
model="bedrock/us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
call_type="chat",
start_time=datetime.now(),
litellm_call_id="test_small_frame_before_upstream_pauses",
function_id="test_small_frame_before_upstream_pauses",
),
)
first: Final = await asyncio.wait_for(anext(iterator), timeout=10)
assert first.startswith(b"event: message_start\n"), first
gate.set()
remaining: Final = tuple([chunk async for chunk in iterator])
assert any(chunk.startswith(b"event: message_stop\n") for chunk in remaining), remaining
await iterator.aclose()

2
uv.lock generated
View file

@ -4358,7 +4358,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.101.1"
version = "1.101.2"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },