diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index a9c7818b577..281f8b8c503 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -459,6 +459,15 @@ def _dropped_cache_control(_: object) -> None: return None +def _replayed_web_search_call(input_item: Mapping[str, object]) -> ResponseFunctionWebSearch: + try: + return ResponseFunctionWebSearch.model_validate(input_item) + except ValidationError: + # Azure and Codex replay web_search_call items without the SDK-required `action`; + # the item only becomes replay text, so keep it as sent instead of failing the turn. + return ResponseFunctionWebSearch.model_construct(None, **input_item) + + def _without_cache_control_key(item: object) -> object: if not isinstance(item, dict): return item @@ -2062,7 +2071,7 @@ class LiteLLMCompletionResponsesConfig: - ItemReference """ if input_item.get("type") == "web_search_call": - search: Final = ResponseFunctionWebSearch.model_validate(input_item) + search: Final = _replayed_web_search_call(input_item) 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 023d0ea26fb..c89cbfcb021 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 @@ -5062,6 +5062,22 @@ class TestHostedWebSearchReplay: assert replayed == search assert input_item == original + def test_replays_search_without_action(self) -> None: + # Azure and Codex replay web_search_call items without the SDK-required `action`. + input_item: Final = {"type": "web_search_call", "id": "ws_no_action", "status": "completed"} + + messages: Final = ( + LiteLLMCompletionResponsesConfig._transform_responses_api_input_item_to_chat_completion_message( + input_item=input_item + ) + ) + + assert len(messages) == 1 + assert messages[0]["role"] == "assistant" + content: Final = messages[0].get("content") + assert isinstance(content, str) + assert json.loads(content[content.index("{") :]) == input_item + @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))