diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index cff113dc3e5..00bd7e2144b 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -585,6 +585,42 @@ class ResponsesAPIRequestUtils: decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) return decoded.get("response_id", container_id) + @staticmethod + def decode_container_ids_in_tools_for_request( + tools: Optional[Iterable[Any]], + ) -> Optional[Iterable[Any]]: + """Decode LiteLLM-managed Responses API tool container IDs. + + Returns the possibly updated tools with LiteLLM-managed container IDs + replaced by the original provider-issued container IDs. + """ + if tools is None: + return tools + + updated_tools: List[Any] = [] + changed = False + + for tool in tools: + updated_tool = tool.copy() if isinstance(tool, dict) else tool + if isinstance(updated_tool, dict): + container_id = updated_tool.get("container") + if isinstance(container_id, str): + decoded = ResponsesAPIRequestUtils._decode_container_id( + container_id + ) + original_id = decoded.get("response_id", container_id) + if original_id != container_id: + updated_tool["container"] = original_id + changed = True + updated_tools.append(updated_tool) + + if not changed: + if isinstance(tools, list): + return tools + return updated_tools + + return updated_tools + @staticmethod def _encode_container_ids_in_annotations( annotations: Any, diff --git a/litellm/router.py b/litellm/router.py index 64b90172c04..6a490d1c193 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -4445,6 +4445,8 @@ class Router: from litellm.litellm_core_utils.core_helpers import safe_deep_copy + self._decode_responses_api_tool_container_ids(kwargs) + # Snapshot the request kwargs before _ageneric_api_call_with_fallbacks # mutates them. A shallow copy alone is not enough: the primary # attempt mutates nested dicts in place — notably `litellm_metadata`, @@ -4476,6 +4478,19 @@ class Router: ) return response + @staticmethod + def _decode_responses_api_tool_container_ids(kwargs: Dict[str, Any]) -> None: + from litellm.responses.utils import ResponsesAPIRequestUtils + + # Keep authorization and deployment selection anchored to the requested + # model. Managed container IDs are supplied by the client, so only the + # provider-issued container ID is safe to recover from them here. + tools = ResponsesAPIRequestUtils.decode_container_ids_in_tools_for_request( + kwargs.get("tools") + ) + if tools is not kwargs.get("tools"): + kwargs["tools"] = tools + def _generic_api_call_with_fallbacks(self, model: str, original_function: Callable, **kwargs): """ Make a generic LLM API call through the router, this allows you to use retries/fallbacks with litellm router @@ -5514,6 +5529,8 @@ class Router: client: Optional[Any] = None, **kwargs, ): + if call_type == "responses": + self._decode_responses_api_tool_container_ids(kwargs) return self._generic_api_call_with_fallbacks(original_function=original_function, **kwargs) return sync_wrapper @@ -5617,6 +5634,8 @@ class Router: **kwargs, ) elif call_type == "aresponses": + if custom_llm_provider and "custom_llm_provider" not in kwargs: + kwargs["custom_llm_provider"] = custom_llm_provider return await self._aresponses_with_streaming_fallbacks( original_function=original_function, **kwargs, diff --git a/tests/router_unit_tests/test_router_endpoints.py b/tests/router_unit_tests/test_router_endpoints.py index 658ad4f3b5c..019bc717cd4 100644 --- a/tests/router_unit_tests/test_router_endpoints.py +++ b/tests/router_unit_tests/test_router_endpoints.py @@ -767,6 +767,44 @@ async def test_init_responses_api_endpoints(): assert second_call_kwargs["response_id"] == "resp_claude_123" +@pytest.mark.asyncio +async def test_router_aresponses_decodes_managed_tool_container_id_without_routing_override(): + from litellm.responses.utils import ResponsesAPIRequestUtils + + router = Router(model_list=[]) + router._ageneric_api_call_with_fallbacks = AsyncMock(return_value={"id": "resp_1"}) + + encoded_container_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="azure-deployment-id", + container_id="cntr_native_123", + ) + tools = [{"type": "code_interpreter", "container": encoded_container_id}] + + await router.aresponses( + model="logical-model", + input="hello", + tools=tools, + custom_llm_provider="openai", + ) + + call_kwargs = router._ageneric_api_call_with_fallbacks.call_args.kwargs + assert call_kwargs["model"] == "logical-model" + assert call_kwargs["custom_llm_provider"] == "openai" + assert call_kwargs["tools"][0]["container"] == "cntr_native_123" + assert tools[0]["container"] == encoded_container_id + + kwargs = { + "model": "logical-model", + "custom_llm_provider": "openai", + "tools": [{"type": "code_interpreter", "container": encoded_container_id}], + } + router._decode_responses_api_tool_container_ids(kwargs) + assert kwargs["model"] == "logical-model" + assert kwargs["custom_llm_provider"] == "openai" + assert kwargs["tools"][0]["container"] == "cntr_native_123" + + @pytest.mark.asyncio async def test_init_vector_store_api_endpoints(): """ diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index bd441321507..36225998ac4 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -123,6 +123,34 @@ class TestResponsesAPIRequestUtils: ) assert result_plain == plain_id + def test_decode_container_ids_in_tools_for_request(self): + encoded_container_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="azure-deployment-id", + container_id="cntr_native_123", + ) + tools = [ + {"type": "code_interpreter", "container": encoded_container_id}, + {"type": "file_search", "vector_store_ids": ["vs_123"]}, + ] + + updated_tools = ( + ResponsesAPIRequestUtils.decode_container_ids_in_tools_for_request(tools) + ) + + assert updated_tools is not tools + assert isinstance(updated_tools, list) + assert updated_tools[0]["container"] == "cntr_native_123" + assert tools[0]["container"] == encoded_container_id + + plain_tools = [{"type": "file_search", "vector_store_ids": ["vs_123"]}] + plain_result = ( + ResponsesAPIRequestUtils.decode_container_ids_in_tools_for_request( + plain_tools + ) + ) + assert plain_result is plain_tools + def test_update_responses_api_response_id_with_model_id_handles_dict(self): """Ensure _update_responses_api_response_id_with_model_id works with dict input""" responses_api_response = {"id": "resp_abc123"}