mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(responses-api): fix tool calls transformation in completion bridge
Fixes two bugs in the openai/responses/... completion bridge:
1. function_call_output.output must be a string, not a list
- When sending tool results back to the model, the content was being
transformed to [{type: "output_text", text: "..."}] instead of a plain string
- This caused OpenAI to reject with "Invalid value: 'output_text'"
2. Multiple tool calls must be in a single choice, not separate choices
- When the model returned multiple tool calls, each was put in its own
Choice with index 0, 1, 2... instead of all together in one Choice
- This broke the standard Chat Completions API format where all tool_calls
belong in a single message
Fixes #18201
This commit is contained in:
parent
9faee8bba6
commit
c60b2dd987
1 changed files with 36 additions and 25 deletions
|
|
@ -167,24 +167,28 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
)
|
||||
elif role == "tool":
|
||||
# Convert tool message to function call output format
|
||||
# Transform content to responses format (handles str, list, and other types)
|
||||
# _convert_content_to_responses_format always returns List[Dict[str, Any]]
|
||||
# The Responses API expects 'output' to be a string, not a list
|
||||
if content is None:
|
||||
transformed_output: list[dict[str, Any]] = []
|
||||
elif isinstance(content, (str, list)):
|
||||
transformed_output = self._convert_content_to_responses_format(
|
||||
content, "tool"
|
||||
)
|
||||
output_str = ""
|
||||
elif isinstance(content, str):
|
||||
output_str = content
|
||||
elif isinstance(content, list):
|
||||
# If content is a list, extract text parts and join them
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
elif isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
output_str = " ".join(text_parts) if text_parts else str(content)
|
||||
else:
|
||||
# Fallback: convert unexpected types to string first
|
||||
transformed_output = self._convert_content_to_responses_format(
|
||||
str(content), "tool"
|
||||
)
|
||||
# Fallback: convert unexpected types to string
|
||||
output_str = str(content)
|
||||
input_items.append(
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": tool_call_id,
|
||||
"output": transformed_output,
|
||||
"output": output_str,
|
||||
}
|
||||
)
|
||||
elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
|
||||
|
|
@ -345,6 +349,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
index = 0
|
||||
reasoning_content: Optional[str] = None
|
||||
|
||||
# Collect all tool calls to put them in a single choice
|
||||
# (Chat Completions API expects all tool calls in one message)
|
||||
accumulated_tool_calls: List[Dict[str, Any]] = []
|
||||
tool_call_index = 0
|
||||
|
||||
for item in output_items:
|
||||
if isinstance(item, ResponseReasoningItem):
|
||||
for summary_item in item.summary:
|
||||
|
|
@ -378,20 +387,10 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
|
||||
tool_call_dict = LiteLLMCompletionResponsesConfig.convert_response_function_tool_call_to_chat_completion_tool_call(
|
||||
tool_call_item=item,
|
||||
index=index,
|
||||
index=tool_call_index,
|
||||
)
|
||||
|
||||
msg = Message(
|
||||
content=None,
|
||||
tool_calls=[tool_call_dict],
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
|
||||
choices.append(
|
||||
Choices(message=msg, finish_reason="tool_calls", index=index)
|
||||
)
|
||||
reasoning_content = None # flush reasoning content
|
||||
index += 1
|
||||
accumulated_tool_calls.append(tool_call_dict)
|
||||
tool_call_index += 1
|
||||
|
||||
elif isinstance(item, dict) and handle_raw_dict_callback is not None:
|
||||
# Handle raw dict responses (e.g., from GPT-5 Codex)
|
||||
|
|
@ -401,6 +400,18 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
else:
|
||||
pass # don't fail request if item in list is not supported
|
||||
|
||||
# If we accumulated tool calls, create a single choice with all of them
|
||||
if accumulated_tool_calls:
|
||||
msg = Message(
|
||||
content=None,
|
||||
tool_calls=accumulated_tool_calls,
|
||||
reasoning_content=reasoning_content,
|
||||
)
|
||||
choices.append(
|
||||
Choices(message=msg, finish_reason="tool_calls", index=index)
|
||||
)
|
||||
reasoning_content = None
|
||||
|
||||
return choices
|
||||
|
||||
def transform_response( # noqa: PLR0915
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue