diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index c271d4c9bec..286f4bda250 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -479,6 +479,24 @@ class ResponsesAPIRequestUtils: return call_id return _remove_thought_signature_from_id(call_id, THOUGHT_SIGNATURE_SEPARATOR) + @staticmethod + def _provider_uses_openai_function_call_item_ids( + custom_llm_provider: str | None, + model: str | None = None, + ) -> bool: + """True when the target Responses adapter expects OpenAI fc_ item ids.""" + if custom_llm_provider is None: + return False + + from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig + from litellm.utils import ProviderConfigManager + + config = ProviderConfigManager.get_provider_responses_api_config( + provider=custom_llm_provider, + model=model, + ) + return isinstance(config, OpenAIResponsesAPIConfig) + @staticmethod def _normalize_function_call_item_id_for_provider( item_id: str, @@ -492,7 +510,10 @@ class ResponsesAPIRequestUtils: custom_llm_provider=custom_llm_provider, ) - if custom_llm_provider != "openai": + if not ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider=custom_llm_provider, + model=model, + ): return item_id if item_id.startswith("call_"): diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index 66c414512ee..7a585014756 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -271,6 +271,57 @@ class TestResponsesAPIRequestUtils: ) assert result_tooluse == "fc_abc123" + result_vertex = ( + ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="toolu_vrtx_abc123", + model="gpt-4o", + custom_llm_provider="openai", + ) + ) + assert result_vertex == "fc_abc123" + + def test_normalize_function_call_item_id_rewrites_for_azure(self): + result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="call_abc123", + model="gpt-4o", + custom_llm_provider="azure", + ) + assert result == "fc_abc123" + + result_tooluse = ( + ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="tooluse_abc123", + model="gpt-4o", + custom_llm_provider="azure", + ) + ) + assert result_tooluse == "fc_abc123" + + def test_normalize_function_call_item_id_rewrites_for_xai(self): + result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="call_abc123", + model="grok-4", + custom_llm_provider="xai", + ) + assert result == "fc_abc123" + + result_tooluse = ( + ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="tooluse_abc123", + model="grok-4", + custom_llm_provider="xai", + ) + ) + assert result_tooluse == "fc_abc123" + + def test_normalize_function_call_item_id_rewrites_for_hosted_vllm(self): + result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="toolu_vrtx_abc123", + model="gpt-4o", + custom_llm_provider="hosted_vllm", + ) + assert result == "fc_abc123" + def test_normalize_function_call_item_id_no_rewrite_for_anthropic(self): result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( item_id="call_abc123", @@ -279,6 +330,66 @@ class TestResponsesAPIRequestUtils: ) assert result == "call_abc123" + def test_provider_uses_openai_function_call_item_ids(self): + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider="openai", + model="gpt-4o", + ) + is True + ) + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider="azure", + model="gpt-4o", + ) + is True + ) + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider="xai", + model="grok-4", + ) + is True + ) + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider="hosted_vllm", + model="gpt-4o", + ) + is True + ) + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider="anthropic", + model="claude-3-5-sonnet", + ) + is False + ) + assert ( + ResponsesAPIRequestUtils._provider_uses_openai_function_call_item_ids( + custom_llm_provider=None, + ) + is False + ) + + def test_normalize_function_call_item_id_leaves_unmatched_prefix(self): + result = ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="fc_already_normalized", + model="gpt-4o", + custom_llm_provider="openai", + ) + assert result == "fc_already_normalized" + + result_other = ( + ResponsesAPIRequestUtils._normalize_function_call_item_id_for_provider( + item_id="random_id_123", + model="gpt-4o", + custom_llm_provider="openai", + ) + ) + assert result_other == "random_id_123" + def test_normalize_function_call_ids_in_input(self): from litellm.litellm_core_utils.prompt_templates.factory import ( THOUGHT_SIGNATURE_SEPARATOR, @@ -309,6 +420,76 @@ class TestResponsesAPIRequestUtils: assert result[0]["call_id"] == "call_abc" assert result[1]["call_id"] == "call_abc" + def test_normalize_function_call_ids_in_input_for_azure(self): + from litellm.litellm_core_utils.prompt_templates.factory import ( + THOUGHT_SIGNATURE_SEPARATOR, + ) + + request_input = [ + { + "type": "function_call", + "id": "call_abc123", + "call_id": f"call_abc{THOUGHT_SIGNATURE_SEPARATOR}sig", + "name": "get_weather", + "arguments": "{}", + }, + "skip-me", + { + "type": "message", + "role": "user", + "content": "hi", + }, + ] + + result = ResponsesAPIRequestUtils._normalize_function_call_ids_in_input( + request_input=request_input, + model="gpt-4o", + custom_llm_provider="azure", + ) + + assert result[0]["id"] == "fc_abc123" + assert result[0]["call_id"] == "call_abc" + assert result[1] == "skip-me" + assert result[2]["type"] == "message" + + def test_normalize_function_call_ids_in_input_for_xai(self): + from litellm.litellm_core_utils.prompt_templates.factory import ( + THOUGHT_SIGNATURE_SEPARATOR, + ) + + request_input = [ + { + "type": "function_call", + "id": "tooluse_xyz", + "call_id": f"call_abc{THOUGHT_SIGNATURE_SEPARATOR}sig", + "name": "get_weather", + "arguments": "{}", + }, + { + "type": "function_call_output", + "call_id": f"call_abc{THOUGHT_SIGNATURE_SEPARATOR}sig", + "output": "sunny", + }, + ] + + result = ResponsesAPIRequestUtils._normalize_function_call_ids_in_input( + request_input=request_input, + model="grok-4", + custom_llm_provider="xai", + ) + + assert result[0]["id"] == "fc_xyz" + assert result[0]["call_id"] == "call_abc" + assert result[1]["call_id"] == "call_abc" + + def test_normalize_function_call_ids_in_input_non_list(self): + result = ResponsesAPIRequestUtils._normalize_function_call_ids_in_input( + request_input="just a string", + model="gpt-4o", + custom_llm_provider="openai", + ) + assert result == "just a string" + class TestResponseAPILoggingUtils: def test_is_response_api_usage_true(self):