Skip to content

fix(responses): join function_call_output text parts with a newline in the chat-completions bridge - #45338

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
litellm_responses_bridge_tool_output_parts_separator
Open

devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
litellm_responses_bridge_tool_output_parts_separator

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /v1/responses through the chat-completions bridge glued a tool result's text parts together
  • Paris: 22 degrees plus clear skies reached the model as Paris: 22 degreesclear skies
  • Every provider served through chat completions saw it (Anthropic, Gemini, Bedrock, ...)

How it solves it:

User Flow

Before: a developer whose agent app runs on the OpenAI Agents SDK against a model served through chat completions gets a two-part tool result read as one word

  1. Their agent sends POST https://litellm-domain/v1/responses with "model": "sonnet", a get_weather tool, and "What is the weather in Paris?"; the model answers with a function_call
  2. Their tool returns two text parts, so the SDK sends the same POST again with a function_call_output whose output is [{"type": "input_text", "text": "Paris: 22 degrees"}, {"type": "input_text", "text": "clear skies"}]
  3. The model reads the tool result as Paris: 22 degreesclear skies, so an agent told to repeat the result prints Paris: 22 degreesclear skies; "model": "gemini" does the same

After: the same tool result reaches the model as two lines

  1. Their agent sends POST https://litellm-domain/v1/responses with "model": "sonnet", a get_weather tool, and "What is the weather in Paris?"; the model answers with a function_call
  2. Their tool returns two text parts, so the SDK sends the same POST again with a function_call_output whose output is [{"type": "input_text", "text": "Paris: 22 degrees"}, {"type": "input_text", "text": "clear skies"}]
  3. The model reads Paris: 22 degrees and clear skies on separate lines, so the agent prints them on two lines; "model": "gemini" does the same

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Last updated: 574b96a. QA and /live-pr-risk ran on 4264079, the last commit with a product diff

Setup, shared by both legs: a proxy booted from a worktree at the named commit with --num_workers 2 --detailed_debug, drop_params: true, and two deployments, sonnet = anthropic/claude-sonnet-5-5 and gemini = gemini/gemini-3.8-flash. The before leg ran on port 26357, the after leg on port 41667, same config and the same .env. The client is the OpenAI Agents SDK 0.23.1 (openai 3.26.1) over the Responses API, pointed at the proxy. Its get_weather tool returns [ToolOutputText("Paris: 22 degrees"), ToolOutputText("clear skies")], which the SDK sends as a function_call_output whose output is two input_text parts, and the agent's instructions are to repeat the tool output exactly, inside a code block, so what the agent prints is what the model read

# agent_client.py <proxy url> <key> <model>
import asyncio
import sys

from agents import Agent, ModelSettings, Runner, ToolOutputText, function_tool, set_default_openai_api, set_default_openai_client, set_tracing_disabled
from openai import AsyncOpenAI

proxy_url, api_key, model_name = sys.argv[1], sys.argv[2], sys.argv[3]

set_default_openai_client(AsyncOpenAI(base_url=proxy_url, api_key=api_key), use_for_tracing=False)
set_default_openai_api("responses")
set_tracing_disabled(True)


@function_tool
def get_weather(city: str) -> list[ToolOutputText]:
    return [ToolOutputText(type="text", text=f"{city}: 22 degrees"), ToolOutputText(type="text", text="clear skies")]


agent = Agent(
    name="weather",
    instructions=(
        "Call get_weather for the city asked. After the tool result arrives, reply with the tool output exactly as you "
        "received it, character for character, inside a fenced code block, and nothing else."
    ),
    tools=[get_weather],
    model=model_name,
    model_settings=ModelSettings(temperature=0),
)


async def main() -> None:
    result = await Runner.run(agent, "What is the weather in Paris?")
    print(result.final_output)


asyncio.run(main())

Before (7c7b0ea)

Anthropic, a provider whose tool result takes a list

  1. python agent_client.py http://127.0.0.1:26357/v1 $KEY sonnet
  2. The SDK sends POST http://127.0.0.1:26357/v1/responses with "output": [{"type": "input_text", "text": "Paris: 22 degrees"}, {"type": "input_text", "text": "clear skies"}] on the function_call_output
  3. The agent prints
    Paris: 22 degreesclear skies
    

Gemini, a provider whose tool result is one string

  1. python agent_client.py http://127.0.0.1:26357/v1 $KEY gemini
  2. The SDK sends the same two-part function_call_output
  3. The agent prints
    Paris: 22 degreesclear skies
    

What each upstream received, from the proxy's POST Request Sent from LiteLLM debug line: Anthropic got {'type': 'tool_result', 'tool_use_id': 'toolu_01FLAxzSfN6PxyJxrhHdYy3v', 'content': 'Paris: 22 degreesclear skies'} and Gemini got 'function_response': {'name': 'get_weather', 'response': {'content': 'Paris: 22 degreesclear skies'}, 'id': 'call_134314'}

After (4264079)

Anthropic, a provider whose tool result takes a list

  1. python agent_client.py http://127.0.0.1:41667/v1 $KEY sonnet
  2. The SDK sends POST http://127.0.0.1:41667/v1/responses with "output": [{"type": "input_text", "text": "Paris: 22 degrees"}, {"type": "input_text", "text": "clear skies"}] on the function_call_output
  3. The agent prints
    Paris: 22 degrees
    clear skies
    

Gemini, a provider whose tool result is one string

  1. python agent_client.py http://127.0.0.1:41667/v1 $KEY gemini
  2. The SDK sends the same two-part function_call_output
  3. The agent prints
    Paris: 22 degrees
    clear skies
    

What each upstream received, from the same debug line: Anthropic got {'type': 'tool_result', 'tool_use_id': 'toolu_01VAUtNW4R56xZhymCeUojKb', 'content': 'Paris: 22 degrees\nclear skies'} and Gemini got 'function_response': {'name': 'get_weather', 'response': {'content': 'Paris: 22 degrees\nclear skies'}, 'id': 'call_250842'}

Observations from the run:

  • Gemini's own chat translation glues a tool message's text parts; this PR leaves it alone
  • Gemini call ids carry a __thought__<signature> suffix on the wire; unrelated, unchanged

Type

🐛 Bug Fix

Caveats (if any)

Low

  • A single input_text part and a plain string output are unchanged; only lists of two or more text parts change. A client that splits one string across several input_text parts now gets a newline at each seam. No known client does that (the Agents SDK sends one part per ToolOutputText), and no rule can tell a seam from two results, so this stays as is
  • Images in the list still go through as text and image blocks, as before. Providers that keep blocks (Anthropic, Bedrock Converse) get the text parts apart; the string-only chat translations (Gemini and Vertex, Cohere, the generic content flattener) glue them. That is pre-existing on the chat route and shared by every chat caller, so it is left for its own PR
  • A chat-completions caller sending two text parts on a tool message to Gemini or Vertex, Cohere, or a provider on the generic flattener still gets them glued. Those three translations are not touched here, since widening one bridge line into shared translations needs its own proof; a follow-up is filed
  • Providers that keep blocks now receive one text block with a newline instead of two text blocks. The bridge has no provider in hand, so a per-provider split is not done here, as the TLDR says

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@devin-ai-integration

devin-ai-integration Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Changes how tool call outputs join multiple text parts.

This PR appears safe to merge, with the previous coverage issue fixed

Summary

The bridge joins text-only tool output parts with a newline instead of gluing them together

  • The latest change restores the leading-space regression test
  • The previous finding is fully addressed; no new actionable issues were found

Reviews (2) · Last reviewed commit: "test(responses): pin that a text part's ..." · Reviewed by Greptile

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_responses_bridge_tool_output_parts_separator (574b96a) with main (7c7b0ea)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (3a3b8ca) during the generation of this report, so 7c7b0ea was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant