diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 878f493b58e..6b9931b7df2 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -27,9 +27,10 @@ from openai.types.chat.chat_completion_named_tool_choice_param import ( ) from openai.types.responses import ResponseFunctionToolCall from openai.types.responses.response_create_params import ResponseInputParam +from openai.types.responses.tool_choice_custom_param import ToolChoiceCustomParam from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam from openai.types.responses.tool_param import FunctionToolParam -from pydantic import TypeAdapter +from pydantic import TypeAdapter, ValidationError from typing_extensions import ReadOnly, TypedDict from litellm._logging import verbose_logger @@ -272,13 +273,21 @@ class LiteLLMCompletionResponsesConfig: @staticmethod def _transform_tool_choice_for_responses_api_response(tool_choice: object) -> ToolChoice: - normalized: Final = LiteLLMCompletionResponsesConfig._transform_tool_choice(tool_choice) - match normalized: - case None: - return "auto" - case {"type": "function", "function": {"name": str(function_name)}}: + if tool_choice is None: + return "auto" + try: + return _RESPONSES_API_TOOL_CHOICE_ADAPTER.validate_python(tool_choice) + except ValidationError: + return LiteLLMCompletionResponsesConfig._chat_tool_choice_as_responses_api_tool_choice(tool_choice) + + @staticmethod + def _chat_tool_choice_as_responses_api_tool_choice(tool_choice: object) -> ToolChoice: + match tool_choice, LiteLLMCompletionResponsesConfig._transform_tool_choice(tool_choice): + case {"type": "custom"}, {"function": {"name": str(custom_name)}}: + return ToolChoiceCustomParam(type="custom", name=custom_name) + case _, {"type": "function", "function": {"name": str(function_name)}}: return ToolChoiceFunctionParam(type="function", name=function_name) - case _: + case _, normalized: return _RESPONSES_API_TOOL_CHOICE_ADAPTER.validate_python(normalized) @staticmethod diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 2aab660b5b7..8f2937cb252 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -1,4 +1,5 @@ import json +from typing import Final import pytest @@ -1426,7 +1427,9 @@ class TestToolChoiceTransformation: [ ({"type": "function", "name": "run_command"}, {"type": "function", "name": "run_command"}), ({"type": "function", "function": {"name": "run_command"}}, {"type": "function", "name": "run_command"}), - ({"type": "custom", "name": "ApplyPatch"}, {"type": "function", "name": "ApplyPatch"}), + ({"type": "custom", "name": "ApplyPatch"}, {"type": "custom", "name": "ApplyPatch"}), + ({"type": "custom", "custom": {"name": "ApplyPatch"}}, {"type": "custom", "name": "ApplyPatch"}), + ({"type": "function"}, "required"), ({"type": "tool"}, "required"), ({"type": "auto"}, "auto"), ("required", "required"), @@ -1434,14 +1437,16 @@ class TestToolChoiceTransformation: (None, "auto"), ], ) - def test_transform_tool_choice_for_responses_api_response(self, request_tool_choice, expected): - result = LiteLLMCompletionResponsesConfig._transform_tool_choice_for_responses_api_response( + def test_transform_tool_choice_for_responses_api_response( + self, request_tool_choice: object, expected: str | dict[str, str] + ) -> None: + result: Final = LiteLLMCompletionResponsesConfig._transform_tool_choice_for_responses_api_response( request_tool_choice ) assert result == expected - def test_non_streamed_response_echoes_named_tool_choice_in_responses_api_shape(self): - chat_completion_response = ModelResponse( + def test_non_streamed_response_echoes_named_tool_choice_in_responses_api_shape(self) -> None: + chat_completion_response: Final = ModelResponse( id="chatcmpl-named-tool-choice", created=1748575031, model="claude-haiku-4-5", @@ -1465,7 +1470,7 @@ class TestToolChoiceTransformation: ], ) - responses_api_response = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + responses_api_response: Final = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( request_input="Run the command pwd.", responses_api_request={"tool_choice": {"type": "function", "name": "run_command"}}, chat_completion_response=chat_completion_response, diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_streaming_iterator_transformation.py b/tests/test_litellm/responses/litellm_completion_transformation/test_streaming_iterator_transformation.py index d4b565f82a1..29061db97dc 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_streaming_iterator_transformation.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_streaming_iterator_transformation.py @@ -11,6 +11,7 @@ spend tracking stores, so a follow-up previous_response_id still finds the conve """ import json +from typing import Final from unittest.mock import AsyncMock, MagicMock import pytest @@ -657,8 +658,8 @@ def _tool_call_chunk(finish_reason: str | None = None) -> ModelResponseStream: ) -def test_streamed_named_tool_choice_is_echoed_in_responses_api_shape(): - iterator = LiteLLMCompletionStreamingIterator( +def test_streamed_named_tool_choice_is_echoed_in_responses_api_shape() -> None: + iterator: Final = LiteLLMCompletionStreamingIterator( model="claude-haiku-4-5", litellm_custom_stream_wrapper=_FakeStreamWrapper([_tool_call_chunk(finish_reason="tool_calls")]), request_input="Run the command pwd.", @@ -670,9 +671,9 @@ def test_streamed_named_tool_choice_is_echoed_in_responses_api_shape(): litellm_metadata={}, ) - events = list(iterator) + events: Final = list(iterator) - response_events = [event for event in events if getattr(event, "type", None) in RESPONSE_ID_EVENT_TYPES] + response_events: Final = [event for event in events if getattr(event, "type", None) in RESPONSE_ID_EVENT_TYPES] assert [event.type for event in response_events] == [ "response.created", "response.in_progress",