mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(responses_bridge): pass plain-string tool output through unchanged
The chat-to-Responses bridge wrapped every tool message's string content in a list of input_text items, but the Responses API spec defines function_call_output.output as a plain string. Strict OpenAI-compatible backends reject the list form for simple text output. Keep string (and None) tool output as a plain string, and only build the list form for genuinely multimodal list content. Fixes #34978
This commit is contained in:
parent
51ad1b0a57
commit
156f3663a1
2 changed files with 61 additions and 46 deletions
|
|
@ -242,24 +242,18 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
}
|
||||
)
|
||||
elif role == "tool":
|
||||
# Convert tool message to function call output format
|
||||
# The Responses API expects 'output' to be a list with input_text/input_image types
|
||||
# Using list format for consistency across text and multimodal content
|
||||
tool_output: List[Dict[str, Any]]
|
||||
tool_output: Union[str, List[Dict[str, Any]]]
|
||||
if content is None:
|
||||
tool_output = []
|
||||
tool_output = ""
|
||||
elif isinstance(content, str):
|
||||
# Convert string to list with input_text
|
||||
tool_output = [{"type": "input_text", "text": content}]
|
||||
tool_output = content
|
||||
elif isinstance(content, list):
|
||||
# Transform list content to Responses API format
|
||||
tool_output = self._convert_content_to_responses_format(
|
||||
content,
|
||||
"user", # Use "user" role to get input_* types
|
||||
)
|
||||
else:
|
||||
# Fallback: convert unexpected types to input_text
|
||||
tool_output = [{"type": "input_text", "text": str(content)}]
|
||||
tool_output = str(content)
|
||||
input_items.append(
|
||||
{
|
||||
"type": "function_call_output",
|
||||
|
|
|
|||
|
|
@ -147,17 +147,19 @@ def test_convert_chat_completion_messages_to_responses_api_tool_result_with_imag
|
|||
|
||||
def test_convert_chat_completion_messages_to_responses_api_tool_result_with_text():
|
||||
"""
|
||||
Test that tool messages with text content are correctly transformed to Responses API format.
|
||||
Test that tool messages with plain string content pass through unchanged.
|
||||
|
||||
This is a regression test for the issue where tool results were being transformed
|
||||
with type='output_text' instead of type='input_text', which caused OpenAI's Responses API
|
||||
to reject the request with "Invalid value: 'output_text'".
|
||||
The Responses API spec defines function_call_output.output as a plain string, and
|
||||
some strict OpenAI-compatible backends reject the list form for simple text output.
|
||||
This is a regression test for #34978: string tool output must stay a string rather
|
||||
than being wrapped in a list of input_text items. It also guards against the earlier
|
||||
#17507 regression where the list form used the invalid type='output_text'.
|
||||
|
||||
Chat Completion format:
|
||||
{"role": "tool", "tool_call_id": "call_abc123", "content": "15 degrees"}
|
||||
|
||||
Responses API format should use input_text, not output_text:
|
||||
{"type": "function_call_output", "call_id": "call_abc123", "output": [{"type": "input_text", "text": "15 degrees"}]}
|
||||
Responses API format keeps output as a plain string:
|
||||
{"type": "function_call_output", "call_id": "call_abc123", "output": "15 degrees"}
|
||||
"""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
|
|
@ -206,25 +208,51 @@ def test_convert_chat_completion_messages_to_responses_api_tool_result_with_text
|
|||
), "function_call_output not found in response"
|
||||
assert function_call_output["call_id"] == "call_abc123"
|
||||
|
||||
# Check that the output is correctly transformed to use input_text, not output_text
|
||||
# Plain string tool output must pass through unchanged, matching the Responses API spec
|
||||
output = function_call_output["output"]
|
||||
assert isinstance(output, list), "output should be a list"
|
||||
assert len(output) == 1, "output should have one item"
|
||||
|
||||
text_item = output[0]
|
||||
# Should be transformed to use input_text for tool results in Responses API format
|
||||
assert (
|
||||
text_item["type"] == "input_text"
|
||||
), f"Expected type 'input_text' for tool result, got '{text_item.get('type')}'"
|
||||
assert (
|
||||
text_item["text"] == "15 degrees"
|
||||
), f"Expected text '15 degrees', got '{text_item.get('text')}'"
|
||||
assert isinstance(output, str), f"output should be a plain string, got {type(output)}"
|
||||
assert output == "15 degrees", f"Expected '15 degrees', got '{output}'"
|
||||
|
||||
print(
|
||||
"✓ Tool result with text correctly transformed to use input_text for Responses API format"
|
||||
"✓ Tool result with text passed through as a plain string for Responses API format"
|
||||
)
|
||||
|
||||
|
||||
def test_convert_chat_completion_messages_to_responses_api_tool_result_with_none_content():
|
||||
"""
|
||||
Regression test for #34978: a tool message with None content produces an empty
|
||||
string output, not an empty list, keeping function_call_output.output a plain string
|
||||
"""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {"name": "noop", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_abc123", "content": None},
|
||||
]
|
||||
|
||||
response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
|
||||
|
||||
function_call_output = next(
|
||||
item for item in response if item.get("type") == "function_call_output"
|
||||
)
|
||||
assert function_call_output["call_id"] == "call_abc123"
|
||||
assert function_call_output["output"] == ""
|
||||
|
||||
|
||||
def test_openai_responses_chunk_parser_reasoning_summary():
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
OpenAiResponsesToChatCompletionStreamIterator,
|
||||
|
|
@ -1292,17 +1320,14 @@ def test_text_plus_tool_calls_sequence():
|
|||
# =============================================================================
|
||||
|
||||
|
||||
def test_tool_message_output_uses_input_text_not_output_text():
|
||||
def test_tool_message_string_output_passes_through_unchanged():
|
||||
"""
|
||||
Test that tool message content uses input_text type, not output_text.
|
||||
Test that plain string tool message content passes through as a plain string.
|
||||
|
||||
This is a regression test for a bug where tool results were transformed to:
|
||||
{"type": "function_call_output", "output": [{"type": "output_text", "text": "..."}]}
|
||||
|
||||
But the Responses API expects input_text for tool results:
|
||||
{"type": "function_call_output", "output": [{"type": "input_text", "text": "..."}]}
|
||||
|
||||
The incorrect format caused OpenAI to reject with:
|
||||
The Responses API spec defines function_call_output.output as a plain string, so
|
||||
string tool results must not be wrapped in a list (see #34978). This also guards
|
||||
against the earlier bug where the list form used the invalid type='output_text',
|
||||
which OpenAI rejected with:
|
||||
"Invalid value: 'output_text'. Supported values are: 'input_text', 'input_image', and 'input_file'."
|
||||
"""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
|
|
@ -1346,16 +1371,12 @@ def test_tool_message_output_uses_input_text_not_output_text():
|
|||
assert function_call_output is not None, "function_call_output not found"
|
||||
assert function_call_output["call_id"] == "call_abc123"
|
||||
|
||||
# The output should be a list with input_text type
|
||||
# The output should stay a plain string, matching the Responses API spec
|
||||
output = function_call_output["output"]
|
||||
assert isinstance(output, list), f"output should be a list, got {type(output)}"
|
||||
assert len(output) == 1
|
||||
assert (
|
||||
output[0]["type"] == "input_text"
|
||||
), f"Expected input_text, got {output[0].get('type')}"
|
||||
assert output[0]["text"] == '{"temperature": 15, "condition": "sunny"}'
|
||||
assert isinstance(output, str), f"output should be a plain string, got {type(output)}"
|
||||
assert output == '{"temperature": 15, "condition": "sunny"}'
|
||||
|
||||
print("✓ Tool message output correctly uses input_text type")
|
||||
print("✓ Tool message string output passes through unchanged")
|
||||
|
||||
|
||||
def test_multiple_tool_calls_in_single_choice():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue