fix: decode responses container tool objects

This commit is contained in:
冯基魁 2026-06-24 12:21:16 +08:00
parent 43ba0857b5
commit f2867e180c
5 changed files with 114 additions and 37 deletions

View file

@ -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)

View file

@ -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:

View file

@ -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,

View file

@ -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():
"""

View file

@ -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"}