mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(responses): serialize flattened namespace tools and keep tool results adjacent to tool_calls
This commit is contained in:
parent
ae2a1f4aba
commit
397fcd0e6b
3 changed files with 111 additions and 1 deletions
|
|
@ -545,9 +545,52 @@ class LiteLLMCompletionResponsesConfig:
|
|||
messages.extend(deduped_in_place)
|
||||
continue
|
||||
|
||||
merged_assistant = LiteLLMCompletionResponsesConfig._merged_trailing_assistant_message(
|
||||
messages=messages,
|
||||
chat_completion_messages=chat_completion_messages,
|
||||
)
|
||||
if merged_assistant is not None:
|
||||
messages[-1] = merged_assistant
|
||||
continue
|
||||
|
||||
messages.extend(chat_completion_messages)
|
||||
return messages
|
||||
|
||||
@staticmethod
|
||||
def _merged_trailing_assistant_message(
|
||||
messages: Sequence[
|
||||
AllMessageValues
|
||||
| GenericChatCompletionMessage
|
||||
| ChatCompletionMessageToolCall
|
||||
| ChatCompletionResponseMessage
|
||||
],
|
||||
chat_completion_messages: Sequence[
|
||||
AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage
|
||||
],
|
||||
) -> ChatCompletionResponseMessage | None:
|
||||
"""Fold an assistant content message into a directly preceding assistant
|
||||
tool_calls message. Providers like DeepSeek and Anthropic require tool
|
||||
results immediately after the tool_calls message, so an assistant message
|
||||
between them is rejected."""
|
||||
if not messages or len(chat_completion_messages) != 1:
|
||||
return None
|
||||
last_message = messages[-1]
|
||||
new_message = chat_completion_messages[0]
|
||||
if not isinstance(last_message, dict):
|
||||
return None
|
||||
if last_message.get("role") != "assistant" or new_message.get("role") != "assistant":
|
||||
return None
|
||||
if not last_message.get("tool_calls") or last_message.get("content") or new_message.get("tool_calls"):
|
||||
return None
|
||||
new_content = new_message.get("content")
|
||||
if new_content is None:
|
||||
return None
|
||||
merged: Final = { # mutable-ok: json.dumps rejects MappingProxyType in outbound chat messages
|
||||
**last_message,
|
||||
"content": new_content,
|
||||
}
|
||||
return cast(ChatCompletionResponseMessage, merged) # cast-ok: TypedDict spread widens to dict[str, object]
|
||||
|
||||
@staticmethod
|
||||
def _deduplicate_tool_call_output_messages(
|
||||
tool_call_output_messages: list[
|
||||
|
|
@ -1373,7 +1416,9 @@ class LiteLLMCompletionResponsesConfig:
|
|||
function: Final = ChatCompletionToolParamFunctionChunk(
|
||||
name=chat_tool_name,
|
||||
description=description,
|
||||
parameters=normalized_parameters,
|
||||
parameters=dict( # mutable-ok: json.dumps rejects MappingProxyType in the outbound payload
|
||||
normalized_parameters
|
||||
),
|
||||
strict=bool(namespace_tool.get("strict", False)),
|
||||
)
|
||||
allowed_callers: Final = validated_allowed_callers(namespace_tool.get("allowed_callers"))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
@ -1816,6 +1817,32 @@ class TestToolTransformation:
|
|||
assert result_tool["function"]["parameters"] == namespace_tool["tools"][0]["parameters"]
|
||||
assert result_tool["function"]["description"] == "Multi-agent tools\n\nSpawn an agent"
|
||||
|
||||
def test_transform_namespace_tools_are_json_serializable(self):
|
||||
"""Outbound chat payloads go through json.dumps, which rejects MappingProxyType."""
|
||||
namespace_tool = {
|
||||
"type": "namespace",
|
||||
"name": "mcp__everything",
|
||||
"description": "MCP tools",
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"name": "get_sum",
|
||||
"description": "Add two numbers",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
|
||||
"required": ["a", "b"],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
result_tools, _ = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
|
||||
tools=[namespace_tool]
|
||||
)
|
||||
|
||||
assert "mcp__everything__get_sum" in json.dumps(result_tools)
|
||||
|
||||
@pytest.mark.parametrize("nested", [True, False])
|
||||
def test_transform_namespace_tools_preserves_allowed_callers(self, nested):
|
||||
function_tool = {
|
||||
|
|
|
|||
|
|
@ -75,3 +75,41 @@ def test_function_call_output_stays_adjacent_to_tool_call():
|
|||
# Tool output must be right after tool call, and before the assistant "Done." message.
|
||||
assert tool_msg_idx == tool_call_idx + 1
|
||||
assert assistant_ok_idx > tool_msg_idx
|
||||
|
||||
|
||||
def test_assistant_message_after_tool_call_is_folded_into_it():
|
||||
"""Codex echoes history as [function_call, assistant message, function_call_output].
|
||||
The assistant message must fold into the tool_calls message so the tool result
|
||||
stays immediately after it (DeepSeek and Anthropic reject it otherwise)."""
|
||||
msgs = LiteLLMCompletionResponsesConfig._transform_response_input_param_to_chat_completion_message(
|
||||
input=[
|
||||
{
|
||||
"role": "user",
|
||||
"type": "message",
|
||||
"content": [{"type": "input_text", "text": "Add 21 and 21."}],
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"name": "mcp__everything__get_sum",
|
||||
"call_id": "call_1",
|
||||
"arguments": '{"a":21,"b":21}',
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "message",
|
||||
"content": [{"type": "output_text", "text": ""}],
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_1",
|
||||
"output": "42",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
roles = [m.get("role") for m in msgs if isinstance(m, dict)]
|
||||
assert roles.count("assistant") == 1
|
||||
|
||||
tool_call_idx = next(i for i, m in enumerate(msgs) if isinstance(m, dict) and m.get("tool_calls"))
|
||||
assert msgs[tool_call_idx].get("role") == "assistant"
|
||||
assert msgs[tool_call_idx + 1].get("role") == "tool"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue