mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(proxy): send string and number subclasses whose repr shows their value as that value in Responses stream frames
This commit is contained in:
parent
4180859208
commit
6b851e9371
2 changed files with 60 additions and 7 deletions
|
|
@ -107,14 +107,27 @@ def _str_skips_its_repr(model: BaseModel) -> bool:
|
|||
return kind.__str__ is BaseModel.__str__ and kind.__repr__ is not BaseModel.__repr__
|
||||
|
||||
|
||||
def _plain_scalar(value: str | float) -> str | float:
|
||||
if isinstance(value, str):
|
||||
return str.__str__(value)
|
||||
if isinstance(value, int):
|
||||
return int.__int__(value)
|
||||
return float.__float__(value)
|
||||
|
||||
|
||||
def _repr_shows_its_value(value: str | float) -> bool:
|
||||
plain: Final = _plain_scalar(value)
|
||||
if isinstance(value, Enum):
|
||||
return repr(value) == f"<{type(value).__name__}.{value.name}: {plain!r}>"
|
||||
return repr(value) == repr(plain)
|
||||
|
||||
|
||||
def _text(value: object, form: _BaseForm) -> object:
|
||||
try:
|
||||
if isinstance(value, (str, int, float)) and (form is _BaseForm.DUMPED or _repr_shows_its_value(value)):
|
||||
return _plain_scalar(value)
|
||||
if form is _BaseForm.PRINTED:
|
||||
return repr(value)
|
||||
if isinstance(value, int):
|
||||
return int(value)
|
||||
if isinstance(value, float):
|
||||
return float(value)
|
||||
if isinstance(value, BaseModel) and _str_skips_its_repr(value):
|
||||
return repr(value)
|
||||
return str(value)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ from litellm.types.llms.openai import (
|
|||
ResponseFailedEvent,
|
||||
ResponseIncompleteEvent,
|
||||
ResponsesAPIResponse,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
from litellm.types.proxy.litellm_pre_call_utils import RedactedDict
|
||||
from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices, Usage
|
||||
|
|
@ -340,6 +341,11 @@ class _Score(float):
|
|||
pass
|
||||
|
||||
|
||||
class _MaskedCount(int):
|
||||
def __repr__(self) -> str:
|
||||
return "_MaskedCount([secure])"
|
||||
|
||||
|
||||
_WHEN: Final = datetime(2026, 10, 30, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
|
|
@ -348,10 +354,9 @@ _WHEN: Final = datetime(2026, 10, 30, 12, 0, tzinfo=timezone.utc)
|
|||
[
|
||||
(_WHEN, repr(_WHEN), str(_WHEN)),
|
||||
(_RigColor.RED, repr(_RigColor.RED), str(_RigColor.RED)),
|
||||
(HTTPStatus.BAD_REQUEST, repr(HTTPStatus.BAD_REQUEST), 400),
|
||||
(_Score(0.5), repr(_Score(0.5)), 0.5),
|
||||
(_MaskedCount(7), repr(_MaskedCount(7)), 7),
|
||||
],
|
||||
ids=["datetime", "enum", "int_enum", "float_subclass"],
|
||||
ids=["datetime", "enum", "number_subclass_with_its_own_repr"],
|
||||
)
|
||||
def test_responses_stream_frames_print_values_json_cannot_take_as_base_printed_them(
|
||||
nested: object, printed: str, dumped: object
|
||||
|
|
@ -366,6 +371,41 @@ def test_responses_stream_frames_print_values_json_cannot_take_as_base_printed_t
|
|||
assert json.loads(error_frame.split("data: ", 1)[1])["error"]["nested"] == dumped
|
||||
|
||||
|
||||
class _RigCode(str, Enum):
|
||||
BLOCKED = "blocked_by_rig"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("nested", "plain"),
|
||||
[(_RigCode.BLOCKED, "blocked_by_rig"), (HTTPStatus.BAD_REQUEST, 400), (_Score(0.5), 0.5)],
|
||||
ids=["str_enum", "int_enum", "float_subclass"],
|
||||
)
|
||||
def test_responses_stream_frames_send_string_and_number_subclasses_as_their_plain_value(
|
||||
nested: object, plain: object
|
||||
) -> None:
|
||||
assert _responses_frame_payload(nested)["nested"] == plain
|
||||
|
||||
|
||||
_DUMPED_DELTA: Final = OutputTextDeltaEvent(
|
||||
type=ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA,
|
||||
item_id="msg_1",
|
||||
output_index=0,
|
||||
content_index=0,
|
||||
delta="hi",
|
||||
sequence_number=3,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mapping", [dict, MappingProxyType], ids=["dict", "mapping_proxy"])
|
||||
def test_responses_sse_chunk_a_hook_dumped_from_an_event_streams_as_that_event(
|
||||
mapping: Callable[[Mapping[str, object]], Mapping[str, object]],
|
||||
) -> None:
|
||||
frame: Final = _format_responses_sse_chunk(chunk=mapping(_DUMPED_DELTA.model_dump()))
|
||||
|
||||
assert isinstance(frame, str)
|
||||
assert json.loads(frame.removeprefix("data: ")) == json.loads(_DUMPED_DELTA.model_dump_json())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mapping", [dict, MappingProxyType], ids=["dict", "mapping_proxy"])
|
||||
def test_responses_stream_frames_turn_mapping_keys_json_cannot_take_into_strings(
|
||||
mapping: Callable[[Mapping[object, object]], Mapping[object, object]],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue