This commit is contained in:
Yassin Yasser 2026-08-27 21:05:41 -05:00 committed by GitHub
commit 52faed7183
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 163 additions and 8 deletions

View file

@ -1989,6 +1989,29 @@ class LiteLLMCompletionResponsesConfig:
namespace, restored_tool_name = mapped
return restored_tool_name, namespace
@staticmethod
def _unpack_parallel_tool_calls(
tool_id: str,
tool_arguments: str,
status: str = "completed",
) -> Sequence[ResponseFunctionToolCall] | None:
"""Unpack synthetic Azure multi_tool_use.parallel container into individual function calls."""
try:
parsed_args: Final = json.loads(tool_arguments)
return tuple(
ResponseFunctionToolCall(
name=tool_use["recipient_name"].removeprefix("functions."),
arguments=json.dumps(tool_use["parameters"]),
call_id=f"{tool_id}_{idx}",
id=f"{tool_id}_{idx}",
type="function_call",
status=status,
)
for idx, tool_use in enumerate(parsed_args["tool_uses"])
)
except (json.JSONDecodeError, KeyError, TypeError, AttributeError):
return None
@staticmethod
def transform_chat_completion_tools_to_responses_tools(
chat_completion_response: ModelResponse,
@ -2003,14 +2026,13 @@ class LiteLLMCompletionResponsesConfig:
"""
all_chat_completion_tools: Final[list[ChatCompletionMessageToolCall]] = []
for choice in chat_completion_response.choices:
if isinstance(choice, Choices):
if choice.message.tool_calls:
all_chat_completion_tools.extend(choice.message.tool_calls)
for tool_call in choice.message.tool_calls:
TOOL_CALLS_CACHE.set_cache(
key=tool_call.id,
value=tool_call,
)
if isinstance(choice, Choices) and choice.message.tool_calls:
all_chat_completion_tools.extend(choice.message.tool_calls)
for tool_call in choice.message.tool_calls:
TOOL_CALLS_CACHE.set_cache(
key=tool_call.id,
value=tool_call,
)
request_tools: Final = responses_api_request.get("tools") if responses_api_request is not None else None
custom_tool_names: Final = extract_custom_tool_names(request_tools)
@ -2042,6 +2064,16 @@ class LiteLLMCompletionResponsesConfig:
restore_name = LiteLLMCompletionResponsesConfig._restore_namespace_tool_name
tool_name, namespace = restore_name(tool_name, namespace_tool_names)
if tool_name == "multi_tool_use.parallel":
unpacked = LiteLLMCompletionResponsesConfig._unpack_parallel_tool_calls(
tool_id=tool_id,
tool_arguments=tool_arguments,
status=function_definition.get("status") or "completed",
)
if unpacked is not None:
responses_tools.extend(unpacked)
continue
provider_specific_fields: dict | None = None
if hasattr(tool, "provider_specific_fields") and getattr(tool, "provider_specific_fields", None):
provider_specific_fields = getattr(tool, "provider_specific_fields")

View file

@ -6,9 +6,21 @@ as tool/function response parts; if the tool output is passed as a list of input
we normalize it to text/image blocks or a string.
"""
import json
from typing import Final
from openai.types.responses import ResponseFunctionToolCall
from litellm.responses.litellm_completion_transformation.transformation import (
LiteLLMCompletionResponsesConfig,
)
from litellm.types.utils import (
ChatCompletionMessageToolCall,
Choices,
Function,
Message,
ModelResponse,
)
def test_function_call_output_list_input_text_is_converted_to_tool_string_content():
@ -40,3 +52,114 @@ def test_function_call_output_string_passthrough():
)
assert len(out) == 1
assert out[0]["content"] == '{"ok":true}'
def test_multi_tool_use_parallel_expanded_in_responses_tools():
tool_call: Final = ChatCompletionMessageToolCall(
id="call_azure_123",
type="function",
function=Function(
name="multi_tool_use.parallel",
arguments=json.dumps(
{ # mutable-ok: test payload
"tool_uses": [
{
"recipient_name": "functions.zoekt_search",
"parameters": {"query": "litellm"},
},
{
"recipient_name": "functions.file_lookup",
"parameters": {"path": "README.md"},
},
]
}
),
),
)
response: Final = ModelResponse(
id="test_resp",
choices=[
Choices(index=0, message=Message(content=None, role="assistant", tool_calls=[tool_call]))
], # mutable-ok: test payload
created=1234567890,
model="azure/gpt-4o",
object="chat.completion",
)
result: Final = LiteLLMCompletionResponsesConfig.transform_chat_completion_tools_to_responses_tools(response)
assert len(result) == 2
assert isinstance(result[0], ResponseFunctionToolCall)
assert result[0].name == "zoekt_search"
assert result[0].id == "call_azure_123_0"
assert result[0].call_id == "call_azure_123_0"
assert json.loads(result[0].arguments) == {"query": "litellm"} # mutable-ok: comparison
assert isinstance(result[1], ResponseFunctionToolCall)
assert result[1].name == "file_lookup"
assert result[1].id == "call_azure_123_1"
assert result[1].call_id == "call_azure_123_1"
assert json.loads(result[1].arguments) == {"path": "README.md"} # mutable-ok: comparison
def test_multi_tool_use_parallel_invalid_json_fallback():
tool_call: Final = ChatCompletionMessageToolCall(
id="call_azure_123",
type="function",
function=Function(
name="multi_tool_use.parallel",
arguments="invalid-json-content",
),
)
response: Final = ModelResponse(
id="test_resp",
choices=[
Choices(index=0, message=Message(content=None, role="assistant", tool_calls=[tool_call]))
], # mutable-ok: test payload
created=1234567890,
model="azure/gpt-4o",
object="chat.completion",
)
result: Final = LiteLLMCompletionResponsesConfig.transform_chat_completion_tools_to_responses_tools(response)
assert len(result) == 1
assert isinstance(result[0], ResponseFunctionToolCall)
assert result[0].name == "multi_tool_use.parallel"
assert result[0].id == "call_azure_123"
def test_multi_tool_use_parallel_malformed_recipient_fallback():
tool_call: Final = ChatCompletionMessageToolCall(
id="call_azure_123",
type="function",
function=Function(
name="multi_tool_use.parallel",
arguments=json.dumps(
{ # mutable-ok: test payload
"tool_uses": [
{
"recipient_name": None,
"parameters": {"query": "litellm"},
}
]
}
),
),
)
response: Final = ModelResponse(
id="test_resp",
choices=[
Choices(index=0, message=Message(content=None, role="assistant", tool_calls=[tool_call]))
], # mutable-ok: test payload
created=1234567890,
model="azure/gpt-4o",
object="chat.completion",
)
result: Final = LiteLLMCompletionResponsesConfig.transform_chat_completion_tools_to_responses_tools(response)
assert len(result) == 1
assert isinstance(result[0], ResponseFunctionToolCall)
assert result[0].name == "multi_tool_use.parallel"
assert result[0].id == "call_azure_123"