From 1c85934231fb0e5d202cd320ec594ea1ef04fa1e Mon Sep 17 00:00:00 2001 From: moe-berri Date: Tue, 27 Oct 2026 11:31:32 -0700 Subject: [PATCH] fix(responses): validate replay fields when action is missing --- .../transformation.py | 18 ++++++++--- .../test_litellm_completion_responses.py | 31 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index cf9642f2a85..3850654daf9 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -2062,11 +2062,19 @@ class LiteLLMCompletionResponsesConfig: - ItemReference """ if input_item.get("type") == "web_search_call": - search: Final = ( - ResponseFunctionWebSearch.model_validate(input_item) - if "action" in input_item - else ResponseFunctionWebSearch.model_construct(None, **input_item) - ) + try: + search: Final = ResponseFunctionWebSearch.model_validate(input_item) + except ValidationError as exc: + errors: Final = exc.errors(include_url=False, include_input=False) + if len(errors) != 1 or errors[0]["type"] != "missing" or errors[0]["loc"] != ("action",): + raise + replay: Final = ResponseFunctionWebSearch.model_construct(None, **input_item) + return [ + GenericChatCompletionMessage( + role="assistant", + content="Hosted web search: " + replay.model_dump_json(exclude_none=True), + ) + ] return [ GenericChatCompletionMessage( role="assistant", diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 4f3b7e45825..63c98b571cb 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -14,7 +14,7 @@ from openai.types.responses.response_function_web_search import ( ActionSearchSource, ResponseFunctionWebSearch, ) -from pydantic import TypeAdapter +from pydantic import TypeAdapter, ValidationError import litellm from litellm.constants import CALLER_SESSION_ID_METADATA_KEY @@ -5077,6 +5077,35 @@ class TestHostedWebSearchReplay: assert isinstance(content, str) assert json.loads(content[content.index("{") :]) == input_item + @pytest.mark.parametrize( + "fields", + ( + {"status": "completed"}, + {"id": "ws_invalid"}, + {"id": 42, "status": "completed"}, + {"id": "ws_invalid", "status": "invalid"}, + {"id": "ws_invalid", "status": "completed", "action": None}, + {"id": "ws_invalid", "status": "completed", "action": 42}, + {"id": "ws_invalid", "status": "completed", "action": {"type": "invalid"}}, + {"id": "ws_invalid", "status": "completed", "action": {"type": "find_in_page"}}, + ), + ids=( + "missing-id", + "missing-status", + "invalid-id", + "invalid-status", + "null-action", + "invalid-action-type", + "unknown-action", + "incomplete-action", + ), + ) + def test_rejects_search_with_other_invalid_fields(self, fields: Mapping[str, object]) -> None: + with pytest.raises(ValidationError): + LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages( + input=[{"type": "web_search_call", **fields}], responses_api_request={} + ) + @pytest.mark.parametrize("order", ((0, 1, 2, 3), (1, 0, 3, 2), (1, 3, 0, 2))) @pytest.mark.parametrize("modify_params", (False, True)) @pytest.mark.parametrize("structured_content", (False, True))