diff --git a/litellm/proxy/common_utils/responses_stream_errors.py b/litellm/proxy/common_utils/responses_stream_errors.py index 0f9df6b6985..8916c0be8a9 100644 --- a/litellm/proxy/common_utils/responses_stream_errors.py +++ b/litellm/proxy/common_utils/responses_stream_errors.py @@ -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) diff --git a/tests/unit/proxy/proxy_server/test_streaming_helpers.py b/tests/unit/proxy/proxy_server/test_streaming_helpers.py index e055d98761a..a2045995093 100644 --- a/tests/unit/proxy/proxy_server/test_streaming_helpers.py +++ b/tests/unit/proxy/proxy_server/test_streaming_helpers.py @@ -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]],