From f2867e180cc1311bbc3667f4ff4fc9b875373d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E5=9F=BA=E9=AD=81?= <1412414664@qq.com> Date: Wed, 24 Jun 2026 12:21:16 +0800 Subject: [PATCH] fix: decode responses container tool objects --- litellm/proxy/common_request_processing.py | 4 +- litellm/responses/utils.py | 69 +++++++++++++++---- litellm/router.py | 31 +++------ .../test_router_endpoints.py | 25 +++++++ .../responses/test_responses_utils.py | 22 ++++++ 5 files changed, 114 insertions(+), 37 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 5d2dfdbae58..9b666e52de9 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -196,7 +196,7 @@ def _collect_response_file_search_vector_store_ids(data: Dict[str, Any]) -> set[ return vector_store_ids -def _collect_response_code_interpreter_container_ids(data: Dict[str, Any]) -> set[str]: +def _collect_response_code_interpreter_container_ids(data: dict[str, Any]) -> set[str]: container_ids: set[str] = set() tools = data.get("tools") if not isinstance(tools, list): @@ -232,7 +232,7 @@ async def _authorize_response_file_search_vector_stores( async def _authorize_response_code_interpreter_containers( - data: Dict[str, Any], + data: dict[str, Any], user_api_key_dict: UserAPIKeyAuth, ) -> None: container_ids = _collect_response_code_interpreter_container_ids(data) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 00bd7e2144b..f9db4bb81c0 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -1,4 +1,5 @@ import base64 +import copy import re from typing import ( Any, @@ -585,6 +586,55 @@ class ResponsesAPIRequestUtils: decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) return decoded.get("response_id", container_id) + @staticmethod + def _get_tool_value(tool: Any, key: str) -> Any: + if isinstance(tool, dict): + return tool.get(key) + return getattr(tool, key, None) + + @staticmethod + def _set_tool_value(tool: Any, key: str, value: Any) -> None: + if isinstance(tool, dict): + tool[key] = value + return + setattr(tool, key, value) + + @staticmethod + def get_decoded_container_payload_from_tool( + tool: Any, require_code_interpreter: bool = False + ) -> Optional[DecodedResponseId]: + if require_code_interpreter and ResponsesAPIRequestUtils._get_tool_value(tool, "type") != "code_interpreter": + return None + + container_id = ResponsesAPIRequestUtils._get_tool_value(tool, "container") + if not isinstance(container_id, str): + return None + + decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) + original_id = decoded.get("response_id", container_id) + if original_id == container_id: + return None + return decoded + + @staticmethod + def get_decoded_container_payload_from_tools( + tools: Any, + ) -> Optional[DecodedResponseId]: + if not isinstance(tools, list): + return None + for tool in tools: + decoded = ResponsesAPIRequestUtils.get_decoded_container_payload_from_tool( + tool, require_code_interpreter=True + ) + if decoded is not None: + return decoded + return None + + @staticmethod + def set_custom_llm_provider_if_missing(kwargs: dict[str, Any], custom_llm_provider: Optional[str]) -> None: + if custom_llm_provider and "custom_llm_provider" not in kwargs: + kwargs["custom_llm_provider"] = custom_llm_provider + @staticmethod def decode_container_ids_in_tools_for_request( tools: Optional[Iterable[Any]], @@ -597,21 +647,16 @@ class ResponsesAPIRequestUtils: if tools is None: return tools - updated_tools: List[Any] = [] + 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_tool = tool + decoded = ResponsesAPIRequestUtils.get_decoded_container_payload_from_tool(tool) + if decoded is not None: + updated_tool = tool.copy() if isinstance(tool, dict) else copy.copy(tool) + ResponsesAPIRequestUtils._set_tool_value(updated_tool, "container", decoded["response_id"]) + changed = True updated_tools.append(updated_tool) if not changed: diff --git a/litellm/router.py b/litellm/router.py index af783e9eebb..d4af3eef061 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -4479,27 +4479,15 @@ class Router: return response @staticmethod - def _decode_responses_api_tool_container_ids(kwargs: Dict[str, Any]) -> None: + def _decode_responses_api_tool_container_ids( + kwargs: dict[str, Any], custom_llm_provider: Optional[str] = None + ) -> None: from litellm.responses.utils import ResponsesAPIRequestUtils - decoded_container_payload = None + ResponsesAPIRequestUtils.set_custom_llm_provider_if_missing(kwargs, custom_llm_provider) request_tools = kwargs.get("tools") - if isinstance(request_tools, list): - for tool in request_tools: - if not isinstance(tool, dict) or tool.get("type") != "code_interpreter": - continue - container_id = tool.get("container") - if not isinstance(container_id, str): - continue - decoded = ResponsesAPIRequestUtils._decode_container_id(container_id) - original_id = decoded.get("response_id", container_id) - if original_id != container_id: - decoded_container_payload = decoded - break - - tools = ResponsesAPIRequestUtils.decode_container_ids_in_tools_for_request( - request_tools - ) + decoded_container_payload = ResponsesAPIRequestUtils.get_decoded_container_payload_from_tools(request_tools) + tools = ResponsesAPIRequestUtils.decode_container_ids_in_tools_for_request(request_tools) if tools is not request_tools: kwargs["tools"] = tools if decoded_container_payload is None: @@ -5551,9 +5539,7 @@ class Router: **kwargs, ): if call_type == "responses": - if custom_llm_provider and "custom_llm_provider" not in kwargs: - kwargs["custom_llm_provider"] = custom_llm_provider - self._decode_responses_api_tool_container_ids(kwargs) + self._decode_responses_api_tool_container_ids(kwargs, custom_llm_provider) return self._generic_api_call_with_fallbacks(original_function=original_function, **kwargs) return sync_wrapper @@ -5657,8 +5643,7 @@ 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 + self._decode_responses_api_tool_container_ids(kwargs, 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 2015a60e4b7..91404a7ca4f 100644 --- a/tests/router_unit_tests/test_router_endpoints.py +++ b/tests/router_unit_tests/test_router_endpoints.py @@ -2,6 +2,7 @@ import sys import os import json import traceback +from types import SimpleNamespace from typing import Optional from dotenv import load_dotenv from fastapi import Request @@ -833,6 +834,30 @@ def test_router_responses_routes_managed_tool_container_id_to_owner_deployment() assert tools[0]["container"] == encoded_container_id +def test_router_responses_routes_managed_tool_object_container_id_to_owner_deployment(): + from litellm.responses.utils import ResponsesAPIRequestUtils + + router = Router(model_list=[]) + encoded_container_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="azure-deployment-id", + container_id="cntr_native_123", + ) + tool = SimpleNamespace(type="code_interpreter", container=encoded_container_id) + kwargs = { + "model": "logical-model", + "custom_llm_provider": "openai", + "tools": [tool], + } + + router._decode_responses_api_tool_container_ids(kwargs) + + assert kwargs["model"] == "azure-deployment-id" + assert kwargs["custom_llm_provider"] == "azure" + assert kwargs["tools"][0].container == "cntr_native_123" + assert tool.container == encoded_container_id + + @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 a40e3da8a07..d306546191d 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -2,6 +2,7 @@ import base64 import json import os import sys +from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest @@ -164,6 +165,27 @@ class TestResponsesAPIRequestUtils: ) assert plain_tuple_result == list(plain_tuple) + def test_decode_container_ids_in_tool_objects_for_request(self): + encoded_container_id = ResponsesAPIRequestUtils._build_container_id( + custom_llm_provider="azure", + model_id="azure-deployment-id", + container_id="cntr_native_123", + ) + tool = SimpleNamespace( + type="code_interpreter", container=encoded_container_id + ) + tools = [tool] + + 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] is not tool + assert updated_tools[0].container == "cntr_native_123" + assert tool.container == encoded_container_id + 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"}