perf(streaming): cut per-chunk pydantic round trips on openai-compatible streams (#47030)

* perf(streaming): skip full-chunk dump and delta rebuild on openai-compatible chunks

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* perf(streaming): reuse the built choice and delta on plain openai-compatible chunks

Co-authored-by: yassin <yassin@berri.ai>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(streaming): copy upstream chunk attributes in upstream key order

The scalar fast path iterated a frozenset of keys, so the SSE key order of chunk extras like service_tier and obfuscation changed with each worker's hash seed

* test(streaming): count chunk_creator calls with a profiler instead of patching classes

* test(streaming): check service_tier still passes through chunk_creator

* fix(streaming): reuse the built choice for chunks with upstream choice extras

* test(streaming): cover ModelResponseStream chunks on the choice reuse path

* fix(streaming): keep main's delta rebuild for deltas with nested values

Clearing role in place on a delta that holds tool calls left its nested
fields unset, so chunk.model_dump() dropped keys like a custom tool call's
name on later chunks. Deltas holding only scalars keep the in-place path.

* perf(streaming): build each plain OpenAI chunk's delta once and skip pydantic attribute misses

Later plain ChatCompletionChunks now build one Delta and one StreamingChoices, with no full-chunk model_dump, and read stored values instead of attribute probes that raise and catch AttributeError. Nested-value chunks no longer pay the dict(model) misses that made them slower than the merge base.

* fix(streaming): read hidden params a chunk carries as a pydantic extra

* test(streaming): pin logprobs, Azure filter extras, and audio and image deltas on later OpenAI chunks

* test(streaming): cover role, reasoning, choice extras and billing on plain OpenAI SDK chunks

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
Yassin Kortam 2026-10-19 19:42:32 -07:00 • committed by GitHub
parent edf4097baf
commit ce4a6cbf5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 871 additions and 39 deletions

View file

@ -612,10 +612,30 @@ def preserve_upstream_non_openai_attributes(
Preserve non-OpenAI attributes from the original chunk.
"""
# Access model_fields on the class, not the instance, to avoid Pydantic 2.11+ deprecation warnings
expected_keys: Final = set(type(model_response).model_fields.keys()).union({"usage"})
for key, value in original_chunk.model_dump().items():
if key not in expected_keys:
expected_keys: Final = frozenset(type(model_response).model_fields) | frozenset({"usage"})
candidate_keys: Final = tuple(
key
for key in (*type(original_chunk).model_fields, *(original_chunk.__pydantic_extra__ or {}))
if key not in expected_keys
)
if not candidate_keys:
return
candidate_values: Final[tuple[tuple[str, object], ...]] = tuple(
(
key,
cast( # cast-ok: Pydantic extras are scalar-checked before assignment
object,
getattr(original_chunk, key),
),
)
for key in candidate_keys
)
if all(value is None or isinstance(value, (str, int, float, bool)) for _, value in candidate_values):
for key, value in candidate_values:
setattr(model_response, key, value)
return
for key, value in original_chunk.model_dump(include=frozenset(candidate_keys)).items():
setattr(model_response, key, value)
def safe_deep_copy(data):

View file

@ -1,6 +1,8 @@
from collections.abc import Iterator, MutableMapping
from collections.abc import Iterator, Mapping, MutableMapping
from typing import Final, Protocol, cast
from pydantic import BaseModel
from litellm.types.llms.base import HiddenParams
_HIDDEN_PARAMS_ATTR: Final = "_hidden_params"
@ -47,8 +49,22 @@ class HiddenParamsModelView(MutableMapping[str, object]):
return frozenset(self._hidden_params.model_fields_set) | frozenset(self._hidden_params.model_extra or {})
def stored_attribute(obj: object, name: str) -> object:
if not isinstance(obj, BaseModel):
return getattr(obj, name, None)
values: Final[Mapping[str, object]] = vars(obj)
if name in values:
return values[name]
extras: Final[Mapping[str, object]] = obj.__pydantic_extra__ or {}
return extras.get(name)
def _get_hidden_params_storage(obj: object) -> object | None:
return obj.get(_HIDDEN_PARAMS_ATTR) if isinstance(obj, dict) else getattr(obj, _HIDDEN_PARAMS_ATTR, None)
if isinstance(obj, dict):
return obj.get(_HIDDEN_PARAMS_ATTR)
if isinstance(obj, BaseModel) and _HIDDEN_PARAMS_ATTR in type(obj).__private_attributes__:
return getattr(obj, _HIDDEN_PARAMS_ATTR, None)
return stored_attribute(obj, _HIDDEN_PARAMS_ATTR)
def get_hidden_params_storage(obj: object) -> dict[str, object] | HiddenParams | None:

View file

@ -15,6 +15,8 @@ from typing import Any, Final, NoReturn, Protocol, TypeVar, cast
import anyio
import httpx
from openai.types.chat import ChatCompletionChunk
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from openai.types.chat.chat_completion_chunk import ChoiceDelta
from pydantic import BaseModel, ValidationError
from typing_extensions import NotRequired, TypedDict
@ -22,7 +24,7 @@ import litellm
from litellm import verbose_logger
from litellm._uuid import uuid
from litellm.litellm_core_utils.asyncify import asyncify
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, get_hidden_params
from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, get_hidden_params, stored_attribute
from litellm.litellm_core_utils.model_response_utils import (
is_model_response_stream_empty,
)
@ -88,6 +90,30 @@ def _apply_provider_fields(response: ModelResponseStream, fields: Mapping[str, o
setattr(response, key, value)
def _prepend_upstream_choice_extras(
choice: StreamingChoices, upstream_choice: ChunkChoice, extra_keys: frozenset[str]
) -> None:
if not extra_keys:
return
upstream_extras: Final[Mapping[str, object]] = upstream_choice.model_dump(include=set(extra_keys))
choice.__pydantic_extra__ = {**upstream_extras, **(choice.__pydantic_extra__ or {})} # rebind-ok: reuse choice
choice.__pydantic_fields_set__.update(upstream_extras)
def _holds_only_scalars(model: BaseModel) -> bool:
fields: Final[Iterator[tuple[str, object]]] = iter(model)
return all(value is None or isinstance(value, (str, int, float, bool)) for _, value in fields)
def _delta_from_upstream(upstream_delta: BaseModel | None) -> Delta | None:
if upstream_delta is None:
return Delta()
try:
return Delta(**dict(iter(upstream_delta)))
except Exception:
return None
def _is_transport_timeout(error: BaseException) -> bool:
return isinstance(error, httpx.TimeoutException) or (
isinstance(error, Timeout) and isinstance(error.__cause__, httpx.TimeoutException)
@ -252,6 +278,7 @@ class CustomStreamWrapper:
self.completion_stream = completion_stream
self.sent_first_chunk = False
self.sent_last_chunk = False
self._openai_handler_receives_chat_chunks = False
self._failure_logging: asyncio.Task[None] | None = None
self._closed = False
self._stream_created_time: float = time.time()
@ -789,7 +816,10 @@ class CustomStreamWrapper:
raise e
def model_response_creator(
self, chunk: dict | None = None, hidden_params: Mapping[str, object] | None = None
self,
chunk: dict | None = None,
hidden_params: Mapping[str, object] | None = None,
delta: Delta | None = None,
) -> ModelResponseStream:
_model: Final = self._cached_model_name
_logging_obj_llm_provider: Final = self._cached_logging_llm_provider
@ -802,7 +832,11 @@ class CustomStreamWrapper:
if chunk:
args.update({k: v for k, v in chunk.items() if k != "stream"})
model_response: Final = ModelResponseStream(**args)
model_response: Final = (
ModelResponseStream(**args)
if delta is None
else ModelResponseStream(**args, choices=[StreamingChoices(delta=delta)])
)
if self.response_id is not None:
model_response.id = self.response_id
elif model_response.id:
@ -943,6 +977,39 @@ class CustomStreamWrapper:
model_response.choices[0].delta = Delta(**_initial_delta)
return model_response
def _prebuilt_plain_delta(self, chunk: object) -> Delta | None:
if (
not self._openai_handler_receives_chat_chunks
or not isinstance(chunk, ChatCompletionChunk)
or not chunk.choices
):
return None
upstream_delta: Final = cast( # cast-ok: [LIT006] runtime delta may be absent
ChoiceDelta | None,
chunk.choices[0].delta,
)
if (
upstream_delta is None
or upstream_delta.function_call is not None
or upstream_delta.tool_calls is not None
or self._has_any_special_delta_attributes(upstream_delta)
):
return None
return _delta_from_upstream(upstream_delta)
def _set_role_on_fresh_delta(self, model_response: ModelResponseStream) -> None:
delta: Final = model_response.choices[0].delta
if self.sent_first_chunk is False:
delta.role = "assistant"
self.sent_first_chunk = True
return
if not _holds_only_scalars(delta):
self.strip_role_from_delta(model_response)
return
delta.role = None
if "provider_specific_fields" not in delta.model_fields_set:
delta.provider_specific_fields = None
def _has_special_delta_content(self, model_response: ModelResponseStream) -> bool:
"""
Check if the delta contains special content types (tool_calls, function_call, audio, or image).
@ -975,11 +1042,11 @@ class CustomStreamWrapper:
"""
return self.strip_role_from_delta(model_response)
def _has_special_delta_attribute(self, delta, attribute_name: str) -> bool:
def _has_special_delta_attribute(self, delta: object, attribute_name: str) -> bool:
"""
Check if delta has a specific attribute and it's not None.
"""
return delta is not None and getattr(delta, attribute_name, None) is not None
return delta is not None and stored_attribute(delta, attribute_name) is not None
def _copy_delta_attribute(self, source_delta, target_delta, attribute_name: str) -> None:
"""
@ -987,7 +1054,7 @@ class CustomStreamWrapper:
"""
setattr(target_delta, attribute_name, getattr(source_delta, attribute_name))
def _has_any_special_delta_attributes(self, delta) -> bool:
def _has_any_special_delta_attributes(self, delta: object) -> bool:
"""
Check if delta has any special attributes (audio, image).
"""
@ -1011,6 +1078,7 @@ class CustomStreamWrapper:
completion_obj: dict[str, Any],
model_response: ModelResponseStream,
response_obj: dict[str, Any],
plain_delta_created: bool = False,
):
from litellm.litellm_core_utils.core_helpers import (
preserve_upstream_non_openai_attributes,
@ -1030,32 +1098,66 @@ class CustomStreamWrapper:
original_chunk: Final = response_obj.get("original_chunk", None)
if original_chunk:
if len(original_chunk.choices) > 0:
choices: Final = []
for choice in original_chunk.choices:
try:
if isinstance(choice, BaseModel):
choice_json = choice.model_dump()
choice_json.pop(
"finish_reason", None
) # for mistral etc. which return a value in their last chunk (not-openai compatible).
choices.append(StreamingChoices(**choice_json))
except Exception:
choices.append(StreamingChoices())
setattr(model_response, "choices", choices)
original_chunk_for_reuse: Final[object] = cast( # cast-ok: runtime type-check before access
object,
original_chunk,
)
upstream_choice: Final = (
original_chunk_for_reuse.choices[0]
if plain_delta_created
and isinstance(original_chunk_for_reuse, ChatCompletionChunk)
and len(original_chunk_for_reuse.choices) == 1
else None
)
upstream_delta: Final = cast( # cast-ok: [LIT006] runtime delta may be absent
ChoiceDelta | None,
getattr(upstream_choice, "delta", None),
)
upstream_choice_extra_keys: Final = frozenset(
(upstream_choice.__pydantic_extra__ or {}) if upstream_choice is not None else ()
)
can_reuse_choice: Final = (
upstream_choice is not None
and upstream_delta is not None
and "enhancements" not in upstream_choice_extra_keys
and upstream_choice.logprobs is None
and _holds_only_scalars(upstream_delta)
)
if can_reuse_choice and upstream_choice is not None:
model_response.choices[0].finish_reason = None # rebind-ok: keep reused choice
model_response.choices[0].index = upstream_choice.index # rebind-ok: keep reused choice
_prepend_upstream_choice_extras(
model_response.choices[0],
upstream_choice,
upstream_choice_extra_keys - {"message"},
)
else:
choices: Final = []
for choice in original_chunk.choices:
try:
if isinstance(choice, BaseModel):
choice_json = choice.model_dump()
choice_json.pop(
"finish_reason", None
) # for mistral etc. which return a value in their last chunk (not-openai compatible).
choices.append(StreamingChoices(**choice_json))
except Exception:
choices.append(StreamingChoices())
setattr(model_response, "choices", choices)
else:
return
model_response.system_fingerprint = original_chunk.system_fingerprint
setattr(
model_response,
"citations",
getattr(original_chunk, "citations", None),
stored_attribute(original_chunk, "citations"),
)
preserve_upstream_non_openai_attributes(
model_response=model_response,
original_chunk=original_chunk,
)
model_response = self.strip_role_from_delta(model_response)
self._set_role_on_fresh_delta(model_response)
if verbose_logger.isEnabledFor(logging.DEBUG):
verbose_logger.debug(
"model_response.choices[0].delta: %s",
@ -1404,6 +1506,8 @@ class CustomStreamWrapper:
if response_obj["is_finished"]:
self.received_finish_reason = response_obj["finish_reason"]
else: # openai / azure chat model
if isinstance(chunk, ChatCompletionChunk):
self._openai_handler_receives_chat_chunks = True
if self.custom_llm_provider in [
LlmProviders.AZURE.value,
LlmProviders.AZURE_AI.value,
@ -1468,8 +1572,11 @@ class CustomStreamWrapper:
provider_response_model: Final = _provider_response_model(chunk)
if provider_response_model is not None:
self._provider_response_model = provider_response_model
prebuilt_delta: Final = self._prebuilt_plain_delta(chunk)
plain_delta_created: bool = prebuilt_delta is not None # rebind-ok: the first chunk builds its delta below
model_response = self.model_response_creator(
hidden_params=_provider_hidden_params(chunk, self._provider_response_model)
hidden_params=_provider_hidden_params(chunk, self._provider_response_model),
delta=prebuilt_delta,
)
response_obj: dict[str, Any] = {}
try:
@ -1493,7 +1600,7 @@ class CustomStreamWrapper:
# enter this branch when no content has been passed in response
if hasattr(original_chunk, "id"):
model_response = self.set_model_id(original_chunk.id, model_response)
if hasattr(original_chunk, "provider_specific_fields"):
if stored_attribute(original_chunk, "provider_specific_fields") is not None:
model_response = self.copy_model_response_level_provider_specific_fields(
original_chunk, model_response
)
@ -1545,16 +1652,10 @@ class CustomStreamWrapper:
model_response.choices[0].delta = Delta()
elif self._has_any_special_delta_attributes(delta):
self._handle_special_delta_attributes(delta, model_response)
else:
try:
delta = (
dict()
if original_chunk.choices[0].delta is None
else dict(original_chunk.choices[0].delta)
)
model_response.choices[0].delta = Delta(**delta)
except Exception:
model_response.choices[0].delta = Delta()
elif prebuilt_delta is None:
plain_delta: Final = _delta_from_upstream(delta)
model_response.choices[0].delta = plain_delta if plain_delta is not None else Delta()
plain_delta_created = plain_delta is not None # rebind-ok: the first chunk builds its delta
else:
if self.send_stream_usage is True:
model_response.choices = []
@ -1581,6 +1682,7 @@ class CustomStreamWrapper:
completion_obj=completion_obj,
model_response=model_response,
response_obj=response_obj,
plain_delta_created=plain_delta_created,
)
return result

View file

@ -0,0 +1,142 @@
import json
import uuid
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
import pytest
from integration._support.client import Gateway, eventually, list_value, object_value
from integration._support.database import read_rows
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue, TypeAdapter
JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
PROVIDER_FIELDS: Final = MappingProxyType({"tier": "gold"})
def _frame(identity: str, choice: Mapping[str, JsonValue] | None, **chunk_fields: JsonValue) -> bytes:
value: Final = {
"id": identity,
"object": "chat.completion.chunk",
"created": 1,
"model": "gpt-4o-mini",
"choices": [] if choice is None else [{"index": 0, "finish_reason": None, **choice}],
**chunk_fields,
}
return b"data: " + json.dumps(value).encode() + b"\n\n"
def _upstream_stream(identity: str) -> tuple[bytes, ...]:
return (
_frame(identity, {"delta": {"role": "assistant", "content": ""}}, system_fingerprint="fp_audit"),
_frame(
identity,
{
"delta": {"content": "Hel", "reasoning_content": "plan"},
"content_filter_results": {"hate": {"filtered": False, "severity": "safe"}},
},
),
_frame(identity, {"delta": {"role": "assistant", "content": "lo"}}),
_frame(identity, {"delta": {"content": " there"}}),
_frame(identity, {"delta": {"content": "!", "provider_specific_fields": dict(PROVIDER_FIELDS)}}),
_frame(identity, {"delta": {}, "finish_reason": "stop"}),
_frame(identity, None, usage={"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15}),
b"data: [DONE]\n\n",
)
def _deployment(provider: str, api_base: str) -> dict[str, JsonValue]:
if provider == "azure":
return {
"model": "azure/gpt-4.1-mini",
"api_base": api_base,
"api_key": "synthetic-azure-key",
"api_version": "2024-10-21",
}
return {"model": "openai/gpt-4o-mini", "api_base": f"{api_base}/v1"}
def _stream_events(gateway: Gateway, model: str, prompt: str) -> tuple[dict[str, JsonValue], ...]:
with gateway.client.stream(
"POST",
"/v1/chat/completions",
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": True,
"stream_options": {"include_usage": True},
},
headers={"Authorization": f"Bearer {gateway.key}"},
) as response:
assert response.status_code == 200, response.read().decode()
return tuple(
JSON_OBJECT.validate_json(line.removeprefix("data: "))
for line in response.iter_lines()
if line.startswith("data: ") and line != "data: [DONE]"
)
def _deltas(events: tuple[dict[str, JsonValue], ...]) -> tuple[dict[str, JsonValue], ...]:
return tuple(
object_value(object_value(list_value(event["choices"])[0])["delta"]) for event in events if event["choices"]
)
def _event_with_content(events: tuple[dict[str, JsonValue], ...], content: str) -> dict[str, JsonValue]:
matches: Final = tuple(
event
for event in events
if event["choices"]
and object_value(object_value(list_value(event["choices"])[0])["delta"]).get("content") == content
)
assert len(matches) == 1, f"{content!r} arrived in {len(matches)} events"
return matches[0]
@pytest.mark.parametrize("provider", ["openai", "azure"])
def test_plain_chunks_keep_text_role_and_provider_fields(gateway: Gateway, provider: str) -> None:
identity: Final = f"plain-chunks-{uuid.uuid4().hex}"
def upstream(request: Request) -> Reply:
return Reply(content_type="text/event-stream", chunks=_upstream_stream(identity))
with wire_server(upstream) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
**_deployment(provider, wire.url), input_cost_per_token=0.001, output_cost_per_token=0.002
)
events: Final = _stream_events(gateway, model, identity)
received: Final = wire.drain()
assert len(received) == 1
assert json.loads(received[0].body)["stream"] is True
deltas: Final = _deltas(events)
assert "".join(str(delta.get("content") or "") for delta in deltas) == "Hello there!"
assert tuple(delta.get("role") for delta in deltas if delta.get("role") is not None) == ("assistant",)
assert deltas[0].get("role") == "assistant"
assert {event["id"] for event in events} == {identity}
assert events[0].get("system_fingerprint") == "fp_audit"
hel_choice: Final = object_value(list_value(_event_with_content(events, "Hel")["choices"])[0])
assert object_value(hel_choice["delta"]).get("reasoning_content") == "plan"
assert hel_choice.get("content_filter_results") == {"hate": {"filtered": False, "severity": "safe"}}
bang_delta: Final = object_value(object_value(list_value(_event_with_content(events, "!")["choices"])[0])["delta"])
assert bang_delta.get("provider_specific_fields") == dict(PROVIDER_FIELDS)
assert tuple(
object_value(list_value(event["choices"])[0]).get("finish_reason")
for event in events
if event["choices"] and object_value(list_value(event["choices"])[0]).get("finish_reason") is not None
) == ("stop",)
usages: Final = tuple(object_value(event["usage"]) for event in events if event.get("usage"))
assert tuple((usage["prompt_tokens"], usage["completion_tokens"], usage["total_tokens"]) for usage in usages) == (
(11, 4, 15),
)
rows: Final = eventually(
lambda: read_rows(
'SELECT spend, prompt_tokens, completion_tokens FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
(identity,),
database_url=gateway.database_url,
),
lambda values: len(values) == 1,
seconds=70,
)
assert (rows[0]["prompt_tokens"], rows[0]["completion_tokens"]) == (11, 4)
assert float(str(rows[0]["spend"])) == pytest.approx(11 * 0.001 + 4 * 0.002)

View file

@ -1,5 +1,6 @@
"""Tests for litellm_core_utils.core_helpers module."""
import json
import logging
import threading
from collections.abc import Iterator, Mapping
@ -7,6 +8,7 @@ from typing import Final
import httpx
import pytest
from openai.types.chat import ChatCompletionChunk
from litellm.litellm_core_utils.core_helpers import (
_FINISH_REASON_MAP,
@ -22,13 +24,14 @@ from litellm.litellm_core_utils.core_helpers import (
map_finish_reason,
normalize_drop_params,
process_response_headers,
preserve_upstream_non_openai_attributes,
reconstruct_model_name,
redact_nested_match_and_regex_keys,
set_provider_response_headers_in_hidden_params,
unbind_budget_reservation_from_callbacks,
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.utils import ImageResponse, TranscriptionResponse
from litellm.types.utils import Delta, ImageResponse, ModelResponseStream, StreamingChoices, TranscriptionResponse
def test_parent_otel_span_reads_dict_like_metadata_by_index():
@ -57,6 +60,46 @@ def test_native_request_id_survives_stream_header_processing(header: str) -> Non
assert processed[header if header.startswith("llm_provider-") else "llm_provider-" + header] == "req_native"
def test_preserve_upstream_non_openai_attributes_keeps_scalar_and_structured_values() -> None:
original_chunk: Final = ModelResponseStream(
choices=[StreamingChoices(index=0, delta=Delta(content="hello"))],
scalar_attribute="scalar",
structured_attribute={"items": ["value"]},
)
model_response: Final = ModelResponseStream(
choices=[StreamingChoices(index=0, delta=Delta(content="hello"))]
)
preserve_upstream_non_openai_attributes(model_response, original_chunk)
assert model_response.scalar_attribute == "scalar"
assert model_response.structured_attribute == {"items": ["value"]}
@pytest.mark.parametrize("last_extra_value", [9, {"nested": 9}], ids=["all_scalar", "one_structured"])
def test_preserve_upstream_non_openai_attributes_keeps_upstream_key_order(last_extra_value: object) -> None:
extra_names: Final = ("zeta", "alpha", "mid", "beta", "omega", "delta", "kappa", "gamma", "theta", "iota")
extras: Final = {
**{f"extra_{name}": index for index, name in enumerate(extra_names[:-1])},
f"extra_{extra_names[-1]}": last_extra_value,
}
original_chunk: Final = ChatCompletionChunk(
id="chatcmpl-1",
created=0,
model="gpt-x",
object="chat.completion.chunk",
choices=[],
service_tier="default",
**extras,
)
model_response: Final = ModelResponseStream(choices=[StreamingChoices(index=0, delta=Delta(content="hello"))])
preserve_upstream_non_openai_attributes(model_response, original_chunk)
serialized_keys: Final = tuple(json.loads(model_response.model_dump_json(exclude_none=True, exclude_unset=True)))
assert serialized_keys[-len(extras) - 1 :] == ("service_tier", *extras)
class TestBudgetReservationBinding:
"""The request-end release skips a reservation a cost callback has claimed, so the claim
must land on the one dict auth stamped, through whichever metadata field or auth object

View file

@ -1,6 +1,7 @@
from typing import Final
import pytest
from openai.types.chat import ChatCompletionChunk
from litellm.litellm_core_utils.hidden_params import (
HIDDEN_PARAMS_ATTR,
@ -61,6 +62,42 @@ def test_get_hidden_params_storage_preserves_supported_storage_identity() -> Non
assert get_hidden_params_storage(model_response) is model_storage
_UPSTREAM_CHUNK_FIELDS: Final = {
"id": "chunk",
"choices": [],
"created": 1,
"model": "m",
"object": "chat.completion.chunk",
}
def _chunk_with_assigned_hidden_params(hidden_params: dict[str, object]) -> ChatCompletionChunk:
chunk: Final = ChatCompletionChunk(**_UPSTREAM_CHUNK_FIELDS)
setattr(chunk, HIDDEN_PARAMS_ATTR, hidden_params)
return chunk
@pytest.mark.parametrize(
"build_chunk",
[
_chunk_with_assigned_hidden_params,
lambda hidden_params: ChatCompletionChunk(**_UPSTREAM_CHUNK_FIELDS, _hidden_params=hidden_params),
lambda hidden_params: ChatCompletionChunk.model_construct(
**_UPSTREAM_CHUNK_FIELDS, _hidden_params=hidden_params
),
],
ids=["assigned", "constructor_extra", "model_construct_extra"],
)
def test_get_hidden_params_storage_reads_upstream_chunk_hidden_params(build_chunk) -> None:
hidden_params: Final[dict[str, object]] = {"is_finished": True}
assert get_hidden_params_storage(build_chunk(hidden_params)) is hidden_params
def test_get_hidden_params_storage_is_none_for_upstream_chunk_without_hidden_params() -> None:
assert get_hidden_params_storage(ChatCompletionChunk(**_UPSTREAM_CHUNK_FIELDS)) is None
def test_get_or_create_hidden_params_sets_empty_dict_on_plain_object() -> None:
class PlainResponse:
pass

View file

@ -1,12 +1,18 @@
import asyncio
import json
import sys
import threading
import time
from typing import AsyncIterator, Callable, Final, Iterator, Literal, NoReturn, Optional
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from types import CodeType, FrameType, MappingProxyType
from typing import AsyncIterator, Callable, Final, Iterator, Literal, NoReturn, Optional, cast
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import httpx
import pytest
from openai.types.chat import ChatCompletionChunk
from pydantic import BaseModel
import litellm
from litellm import verbose_logger
@ -5066,6 +5072,472 @@ def test_handle_stream_fallback_error_restores_context_only_after_exception_mapp
session_id_var.set("")
@dataclass(frozen=True, slots=True)
class _ProfiledCall:
code: CodeType
arguments: Mapping[str, object]
def _chunk_creator_calls(
wrapper: CustomStreamWrapper,
chunks: Sequence[ChatCompletionChunk | ModelResponseStream],
codes: frozenset[CodeType],
) -> tuple[tuple[ModelResponseStream | None, ...], tuple[_ProfiledCall, ...]]:
calls: Final[list[_ProfiledCall]] = []
def record(frame: FrameType, event: str, _arg: object) -> None:
if event == "call" and frame.f_code in codes:
arguments: Final = dict(cast(Mapping[str, object], frame.f_locals))
calls.append(_ProfiledCall(code=frame.f_code, arguments=MappingProxyType(arguments)))
previous_profiler: Final = sys.getprofile()
sys.setprofile(record)
try:
outputs: Final = tuple(wrapper.chunk_creator(chunk=chunk) for chunk in chunks)
finally:
sys.setprofile(previous_profiler)
return outputs, tuple(calls)
def test_openai_chunk_creator_skips_full_chunk_dump_and_delta_rebuild(
logging_obj: Logging,
) -> None:
chunks: Final[list[ChatCompletionChunk]] = [
ChatCompletionChunk(
id="chunk-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": {"role": "assistant", "content": "hello"}, "finish_reason": None}],
),
ChatCompletionChunk(
id="chunk-2",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": {"content": " world"}, "finish_reason": None}],
service_tier="default",
citations=["https://a"],
custom_chunk_attribute="custom value",
),
]
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="openai",
logging_obj=logging_obj,
)
chunk_model_dump: Final = ChatCompletionChunk.model_dump.__code__
delta_init: Final = Delta.__init__.__code__
streaming_choices_init: Final = StreamingChoices.__init__.__code__
outputs, calls = _chunk_creator_calls(
wrapper, chunks, frozenset({chunk_model_dump, delta_init, streaming_choices_init})
)
emitted_chunks: Final = tuple(output for output in outputs if output is not None)
choice_dumps: Final = tuple(
call
for call in calls
if call.code is chunk_model_dump and not isinstance(call.arguments["self"], ChatCompletionChunk)
)
full_chunk_dumps: Final = tuple(
call
for call in calls
if call.code is chunk_model_dump
and isinstance(call.arguments["self"], ChatCompletionChunk)
and call.arguments["include"] is None
)
delta_constructions: Final = tuple(call for call in calls if call.code is delta_init)
streaming_choice_constructions: Final = tuple(call for call in calls if call.code is streaming_choices_init)
assert len(emitted_chunks) == 2
assert full_chunk_dumps == ()
assert choice_dumps == ()
assert len(delta_constructions) <= len(emitted_chunks) * 2
assert len(streaming_choice_constructions) <= len(emitted_chunks)
assert [chunk.choices[0].delta.content for chunk in emitted_chunks] == ["hello", " world"]
assert emitted_chunks[0].choices[0].delta.role == "assistant"
assert emitted_chunks[1].choices[0].delta.role is None
assert getattr(emitted_chunks[1], "citations") == ["https://a"]
assert getattr(emitted_chunks[1], "custom_chunk_attribute") == "custom value"
assert getattr(emitted_chunks[1], "service_tier") == "default"
chunk2_delta: Final = emitted_chunks[1].model_dump()["choices"][0]["delta"]
assert "provider_specific_fields" in chunk2_delta
assert chunk2_delta["provider_specific_fields"] is None
finish_output: Final = wrapper.chunk_creator(
chunk=ChatCompletionChunk(
id="chunk-3",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": {}, "finish_reason": "stop"}],
)
)
assert finish_output is not None
assert finish_output.choices[0].finish_reason == "stop"
assert finish_output.choices[0].delta.content is None
def _openai_chunk(chunk_id: str, delta: Mapping[str, object]) -> ChatCompletionChunk:
return ChatCompletionChunk(
id=chunk_id,
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": delta, "finish_reason": None}],
)
def _missed_attributes(wrapper: CustomStreamWrapper, chunks: Sequence[ChatCompletionChunk]) -> tuple[str, ...]:
attribute_lookup: Final = BaseModel.__getattr__.__code__
misses: Final[list[str]] = []
def record_miss(frame: FrameType, event: str, arg: object) -> Callable[[FrameType, str, object], object]:
if event == "exception" and isinstance(arg, tuple) and isinstance(arg[1], AttributeError):
misses.append(str(frame.f_locals["item"]))
return record_miss
def trace_lookups(frame: FrameType, event: str, _arg: object) -> Callable[[FrameType, str, object], object] | None:
return record_miss if event == "call" and frame.f_code is attribute_lookup else None
previous_tracer: Final = sys.gettrace()
sys.settrace(trace_lookups)
try:
tuple(wrapper.chunk_creator(chunk=chunk) for chunk in chunks)
finally:
sys.settrace(previous_tracer)
return tuple(misses)
def test_openai_chunk_creator_builds_one_delta_and_misses_no_attribute_per_later_chunk(
logging_obj: Logging,
) -> None:
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="openai",
logging_obj=logging_obj,
)
wrapper.chunk_creator(chunk=_openai_chunk("chunk-1", {"role": "assistant", "content": ""}))
plain_chunks: Final = (
_openai_chunk("chunk-2", {"content": "hello"}),
_openai_chunk("chunk-3", {"content": " world"}),
)
nested_chunk: Final = _openai_chunk(
"chunk-4", {"content": "!", "reasoning_details": [{"type": "reasoning.text", "text": "thought"}]}
)
delta_init: Final = Delta.__init__.__code__
streaming_choices_init: Final = StreamingChoices.__init__.__code__
outputs, calls = _chunk_creator_calls(wrapper, plain_chunks, frozenset({delta_init, streaming_choices_init}))
assert sum(call.code is delta_init for call in calls) == len(plain_chunks)
assert sum(call.code is streaming_choices_init for call in calls) == len(plain_chunks)
assert [output.choices[0].delta.content if output else None for output in outputs] == ["hello", " world"]
assert _missed_attributes(wrapper, (*plain_chunks, nested_chunk)) == ()
def test_chunk_creator_skips_full_dump_of_model_response_stream_chunks(
logging_obj: Logging,
) -> None:
chunks: Final = (
ModelResponseStream(
id="chunk-1",
created=1,
model="claude-model",
choices=[StreamingChoices(index=0, delta=Delta(role="assistant", content="hello"))],
),
ModelResponseStream(
id="chunk-2",
created=1,
model="claude-model",
choices=[StreamingChoices(index=0, delta=Delta(content=" world"))],
custom_chunk_attribute="custom value",
),
)
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="claude-model",
custom_llm_provider="anthropic",
logging_obj=logging_obj,
)
model_dump: Final = BaseModel.model_dump.__code__
outputs, calls = _chunk_creator_calls(wrapper, chunks, frozenset({model_dump}))
emitted_chunks: Final = tuple(output for output in outputs if output is not None)
full_upstream_dumps: Final = tuple(
call
for call in calls
if any(call.arguments["self"] is chunk for chunk in chunks) and call.arguments["include"] is None
)
assert full_upstream_dumps == ()
assert [chunk.choices[0].delta.content for chunk in emitted_chunks] == ["hello", " world"]
assert emitted_chunks[1].choices[0].delta.role is None
assert getattr(emitted_chunks[1], "custom_chunk_attribute") == "custom value"
def test_openai_chunk_creator_keeps_multiple_choice_logprobs_and_extras(
logging_obj: Logging,
) -> None:
chunk: Final = ChatCompletionChunk(
id="chunk-with-multiple-choices",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
citations=["https://a"],
choices=[
{
"index": 3,
"delta": {"content": "first"},
"finish_reason": None,
"logprobs": {"content": []},
},
{
"index": 7,
"delta": {"content": "second"},
"finish_reason": None,
"custom_choice_attribute": "choice extra",
},
],
)
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="openai",
logging_obj=logging_obj,
)
result: Final = wrapper.chunk_creator(chunk=chunk)
assert result is not None
assert [choice.index for choice in result.choices] == [3, 7]
result_logprobs = result.choices[0].logprobs
chunk_logprobs = chunk.choices[0].logprobs
assert result_logprobs is not None
assert chunk_logprobs is not None
assert result_logprobs.model_dump() == chunk_logprobs.model_dump()
assert getattr(result.choices[1], "custom_choice_attribute") == "choice extra"
assert getattr(result, "citations") == ["https://a"]
@pytest.mark.parametrize(
("choice_extras", "expected_choice_keys", "expected_choice_constructions"),
[
pytest.param(
{
"content_filter_results": {"hate": {"filtered": False, "severity": "safe"}},
"message": {"role": "assistant", "content": "hello"},
},
["content_filter_results", "index", "delta"],
1,
id="azure-content-filter-results",
),
pytest.param(
{"enhancements": {"grounding": {"lines": []}}},
["index", "delta", "enhancements"],
2,
id="enhancements",
),
],
)
def test_chunk_creator_keeps_upstream_choice_extras_in_upstream_order(
logging_obj: Logging,
choice_extras: dict[str, object],
expected_choice_keys: list[str],
expected_choice_constructions: int,
) -> None:
chunk: Final = ChatCompletionChunk(
id="chunk-with-choice-extras",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": {"content": "hello"}, "finish_reason": None, **choice_extras}],
)
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="azure",
logging_obj=logging_obj,
)
streaming_choices_init: Final = StreamingChoices.__init__.__code__
outputs, calls = _chunk_creator_calls(wrapper, [chunk], frozenset({streaming_choices_init}))
result: Final = outputs[0]
assert result is not None
choice_dump: Final = result.choices[0].model_dump(exclude_none=True)
assert list(choice_dump) == expected_choice_keys
assert choice_dump["delta"]["content"] == "hello"
assert {key: choice_dump[key] for key in choice_extras if key != "message"} == {
key: value for key, value in choice_extras.items() if key != "message"
}
assert len(calls) == expected_choice_constructions
def test_chunk_creator_keeps_upstream_index_on_single_choice_chunk(logging_obj: Logging) -> None:
chunk: Final = ChatCompletionChunk(
id="chunk-with-index-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 1, "delta": {"content": "hello"}, "finish_reason": None}],
)
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="openai",
logging_obj=logging_obj,
)
result: Final = wrapper.chunk_creator(chunk=chunk)
assert result is not None
assert result.choices[0].index == 1
assert result.choices[0].delta.content == "hello"
def _later_chunk_outputs(
custom_llm_provider: str, later_chunks: Sequence[ChatCompletionChunk], logging_obj: Logging
) -> tuple[ModelResponseStream, ...]:
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider=custom_llm_provider,
logging_obj=logging_obj,
)
wrapper.chunk_creator(chunk=_openai_chunk("chunk-0", {"role": "assistant", "content": ""}))
outputs: Final = tuple(wrapper.chunk_creator(chunk=chunk) for chunk in later_chunks)
return tuple(output for output in outputs if output is not None)
def _wire_dump(chunk: ModelResponseStream) -> dict[str, object]:
return chunk.model_dump(exclude_none=True, exclude_unset=True)
def test_openai_chunk_creator_keeps_single_choice_logprobs_on_later_chunks(logging_obj: Logging) -> None:
logprobs: Final = {"content": [{"token": "hello", "logprob": -0.25, "bytes": [104], "top_logprobs": []}]}
chunk: Final = ChatCompletionChunk(
id="chunk-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": {"content": "hello"}, "finish_reason": None, "logprobs": logprobs}],
)
outputs: Final = _later_chunk_outputs("openai", [chunk], logging_obj)
assert [_wire_dump(output)["choices"][0]["logprobs"] for output in outputs] == [logprobs]
def test_azure_chunk_creator_streams_choice_content_filter_results_on_later_chunks(logging_obj: Logging) -> None:
filter_results: Final = {"hate": {"filtered": False, "severity": "safe"}}
chunk: Final = ChatCompletionChunk(
id="chunk-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[
{"index": 0, "delta": {"content": "hello"}, "finish_reason": None, "content_filter_results": filter_results}
],
)
outputs: Final = _later_chunk_outputs("azure", [chunk], logging_obj)
assert [_wire_dump(output)["choices"][0]["content_filter_results"] for output in outputs] == [filter_results]
assert ["content_filter_results" in output.choices[0].model_fields_set for output in outputs] == [True]
@pytest.mark.parametrize(
"special_delta",
[
pytest.param({"audio": {"id": "audio_1", "data": "AAAA", "transcript": "hi"}}, id="audio"),
pytest.param({"images": [{"type": "image_url", "image_url": {"url": "data:,"}, "index": 0}]}, id="images"),
],
)
def test_openai_chunk_creator_dumps_special_delta_attributes_like_other_deltas(
logging_obj: Logging, special_delta: dict[str, object]
) -> None:
chunk: Final = ChatCompletionChunk.model_construct(
id="chunk-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[{"index": 0, "delta": special_delta, "finish_reason": None}],
)
outputs: Final = _later_chunk_outputs("openai", [chunk], logging_obj)
assert [output.model_dump()["choices"][0]["delta"] for output in outputs] == [
{
"audio": None,
"content": None,
"function_call": None,
"provider_specific_fields": None,
"role": None,
"tool_calls": None,
**special_delta,
}
]
def test_openai_chunk_creator_keeps_custom_tool_name_key_on_later_chunks(
logging_obj: Logging,
) -> None:
chunks: Final = (
ChatCompletionChunk.model_construct(
id="chunk-1",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[
{
"index": 0,
"delta": {
"role": "assistant",
"tool_calls": [
{"index": 0, "id": "call_1", "type": "custom", "custom": {"name": "run_python", "input": ""}}
],
},
"finish_reason": None,
}
],
),
ChatCompletionChunk.model_construct(
id="chunk-2",
created=1,
model="gpt-4o",
object="chat.completion.chunk",
choices=[
{
"index": 0,
"delta": {"tool_calls": [{"index": 0, "custom": {"input": "print"}}]},
"finish_reason": None,
}
],
),
)
wrapper: Final = CustomStreamWrapper(
completion_stream=iter(()),
model="gpt-4o",
custom_llm_provider="openai",
logging_obj=logging_obj,
)
emitted_chunks: Final = tuple(
output for output in (wrapper.chunk_creator(chunk=chunk) for chunk in chunks) if output is not None
)
assert [chunk.model_dump()["choices"][0]["delta"]["tool_calls"][0]["custom"] for chunk in emitted_chunks] == [
{"name": "run_python", "input": ""},
{"name": None, "input": "print"},
]
def test_chunk_creator_preserves_hidden_provider_specific_fields_from_parsed_chunk():
wrapper = CustomStreamWrapper(
completion_stream=None,