mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge a45cd4f3e6 into 2306816d40
This commit is contained in:
commit
52faed7183
2 changed files with 163 additions and 8 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue