fix responses container id reuse routing

This commit is contained in:
冯基魁 2026-06-22 18:11:15 +08:00
parent 5b93ba0ada
commit 048ec5d082
4 changed files with 121 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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