litellm - ✅(Solved) Fix [Bug]: Responses→Chat lowering drops input_file in function_call_output.output (Vertex / Bedrock) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#28232Fetched 2026-05-20 03:40:48
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×2cross-referenced ×1referenced ×1

Root Cause

In `litellm/responses/litellm_completion_transformation/transformation.py`, the nested helper that walks `function_call_output.output` (`_normalize_function_call_output_to_tool_content`, called from the `function_call_output` handler) only matches text and image parts:

```python if part_type in ("input_text", "output_text", "text"): ... elif part_type in ("input_image", "image_url"): ...

no input_file branch

```

`input_file` parts fall through and are discarded. The existing helper `_transform_input_file_item_to_file_item` (around line 1215 of the same file) already knows how to convert an `input_file` to a chat-completions `{"type": "file", "file": {...}}` content part — but it's only called from `_transform_responses_api_content_to_chat_completion_content` (user/system message content path, around line 1270), not from the `function_call_output.output` path.

Fix Action

Fixed

PR fix notes

PR #28233: fix(responses): preserve input_file in function_call_output during chat lowering

Description (problem / solution / changelog)

What this PR does

Fixes #28232.

When a request hits the LiteLLM proxy at /responses and the input contains a function_call_output whose output array carries an input_file part (e.g. a PDF returned from a tool call, lowered from the OpenAI Agents SDK's ToolOutputFileContent), the Responses→ChatCompletions lowering was silently dropping the input_file part. Only sibling input_text parts survived into the resulting role: "tool" message, flattened to a plain string.

This affected every provider that lacks a native /responses endpoint and goes through the lowering path (vertex_ai/* confirmed, bedrock/* highly likely). Azure was unaffected because LiteLLM passes through to Azure OpenAI's native /openai/responses endpoint.

Root cause

In litellm/responses/litellm_completion_transformation/transformation.py, the nested helper _normalize_function_call_output_to_tool_content walks the output array but only matches text and image parts:

if part_type in ("input_text", "output_text", "text"):
    ...
elif part_type in ("input_image", "image_url"):
    ...
# no input_file branch ← BUG

So input_file parts fell through and were discarded.

Fix

Add an input_file / file branch to the same walker that reuses the existing _transform_input_file_item_to_file_item helper (already used by the user/system message content path at _transform_responses_api_content_to_chat_completion_content). Also extends the "prefer structured blocks" check so the tool message gets a list content whenever a file part is present (previously only triggered by image parts).

This is the symmetric counterpart, in the opposite direction, of #17799 (which handled input_image in Chat→Responses).

Tests

Added two mocked tests to tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py:

  • test_function_call_output_input_file_is_preserved_as_file_block — PDF via file_data survives into a structured {"type": "file", "file": {"file_data": ...}} block alongside the sibling text.
  • test_function_call_output_input_file_with_file_id_is_preserved — same for the file_id form (no inline bytes).

All four tests in the file pass locally (2 pre-existing + 2 new). The two changed files pass black --check, ruff check, and mypy --ignore-missing-imports.

Note on make format-check: there is unrelated pre-existing formatting drift in enterprise/litellm_enterprise/ that fails black --check. Verified that this drift exists on the base branch before any of my changes (git stash + make format-check at HEAD~1 still reports the same 13 files). I did not include those reformats here to keep PR scope isolated, as CONTRIBUTING.md requires.

Repro before / after

Smoke from #28232 (Agents SDK → Vertex Gemini via LiteLLM, synthetic 2-page PDF with distinctive tokens TEST-A1B2, INVOICE-7777, 123.45):

  • Before: model produces generic description, none of the distinctive tokens appear → PDF was dropped.
  • After: PDF reaches the model end-to-end via the existing chat-completions {"type": "file", ...} part; downstream Gemini adapter then lowers that to inline_data with mime_type: application/pdf.

Checklist

  • Scope isolated — one specific problem
  • At least one mocked test added (two)
  • black --check, ruff check, mypy clean on changed files
  • CLA signed (signing now)

Changed files

  • litellm/responses/litellm_completion_transformation/transformation.py (modified, +11/-2)
  • tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py (modified, +88/-0)
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

When a request to LiteLLM's /responses proxy endpoint contains a function_call_output whose output array includes an input_file content part (e.g. a PDF returned from a tool call, as produced by the OpenAI Agents SDK's ToolOutputFileContent), the Responses→ChatCompletions transformer silently drops the input_file part. Only the sibling input_text parts survive into the resulting role: "tool" message's content, flattened to a plain string.

This affects every provider that lacks a native /responses endpoint and therefore goes through the lowering path. Confirmed on vertex_ai/gemini-2.5-flash; very likely the same on bedrock/* (same lowering code path). Azure is not affected because LiteLLM passes through to Azure OpenAI's native /openai/responses endpoint and the original Responses-API body — including the input_file part — reaches the provider intact.

Expected: the file content is forwarded to the provider (as a chat-completions {"type": "file", "file": {...}} part, analogous to how input_image is forwarded as image_url).

Actual: the file part is dropped during lowering; the model only sees the input_text framing string. End-to-end, the downstream LLM never sees the file.

Steps to Reproduce

Minimal repro using the OpenAI Agents SDK pointed at a LiteLLM proxy. The PDF is rendered in-process with reportlab and carries deliberately ungoogleable strings (`TEST-A1B2`, `INVOICE-7777`, `Page 2: amount $123.45`) so the model can't pattern-match its way to a passing description.

```python import asyncio, base64, io from openai import AsyncOpenAI from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas as rl_canvas from agents import ( Agent, OpenAIResponsesModel, RunContextWrapper, Runner, ToolOutputFileContent, ToolOutputText, function_tool, )

def make_test_pdf_data_url() -> str: buf = io.BytesIO() c = rl_canvas.Canvas(buf, pagesize=letter) c.setFont("Helvetica-Bold", 28); c.drawString(72, 720, "TEST-A1B2") c.setFont("Helvetica", 18); c.drawString(72, 670, "INVOICE-7777") c.showPage() c.setFont("Helvetica-Bold", 28); c.drawString(72, 720, "Page 2: amount $123.45") c.showPage(); c.save() return f"data:application/pdf;base64,{base64.b64encode(buf.getvalue()).decode()}"

PDF = make_test_pdf_data_url()

@function_tool async def get_test_pdf(ctx: RunContextWrapper[None]): return [ ToolOutputText(text="Here is the PDF. Read every page and describe what's on it."), ToolOutputFileContent(file_data=PDF, filename="test.pdf"), ]

async def main(): client = AsyncOpenAI(api_key="sk-...", base_url="http://localhost:4000\") agent = Agent( name="PdfSmoke", instructions="Always call get_test_pdf, then describe each page.", tools=[get_test_pdf], model=OpenAIResponsesModel(model="vertex-gemini-2.5-flash", openai_client=client), ) result = await Runner.run(agent, input="Use the tool and describe each page.", max_turns=4) print(result.final_output)

asyncio.run(main()) ```

Expected output: the model echoes back at least two of `TEST-A1B2`, `INVOICE-7777`, `123.45`. Actual output: generic description with none of those tokens — the model never saw the PDF.

Relevant log output

```shell

Inbound /responses body (turn 2). PDF intact, base64 elided as <B64>:

'body': {'input': [ {'role': 'user', 'content': "Use the tool and describe each page."}, {'type': 'function_call', 'name': 'get_test_pdf', 'call_id': '...', 'arguments': '{}'}, {'type': 'function_call_output', 'call_id': '...', 'output': [ {'type': 'input_text', 'text': "Here is the PDF. Read every page and describe what's on it."}, {'type': 'input_file', 'file_data': 'data:application/pdf;base64,<B64>', 'filename': 'test.pdf'} ]} ]}

After Responses→Chat lowering (litellm.acompletion messages), Vertex path.

Note: input_file is gone; content is a plain string with only the input_text:

messages=[ {'role': 'system', 'content': "You are testing PDF tool outputs..."}, {'role': 'user', 'content': "Use the tool and describe each page."}, {'role': 'assistant', 'content': [], 'tool_calls': [{'id': '...', 'type': 'function', 'function': {'name': 'get_test_pdf', 'arguments': '{}'}}]}, {'role': 'tool', 'content': "Here is the PDF. Read every page and describe what's on it.", 'tool_call_id': '...'} ]

Same request to Azure for comparison — no lowering, PDF reaches provider intact:

api_base: https://<region>.api.cognitive.microsoft.com/openai/responses?api-version=2025-04-01-preview body.input[2] = {'type': 'function_call_output', 'output': [ {'type': 'input_text', 'text': '...'}, {'type': 'input_file', 'file_data': 'data:application/pdf;base64,<B64>', 'filename': 'test.pdf'} # ← preserved ]} ```

Root cause

In `litellm/responses/litellm_completion_transformation/transformation.py`, the nested helper that walks `function_call_output.output` (`_normalize_function_call_output_to_tool_content`, called from the `function_call_output` handler) only matches text and image parts:

```python if part_type in ("input_text", "output_text", "text"): ... elif part_type in ("input_image", "image_url"): ...

no input_file branch

```

`input_file` parts fall through and are discarded. The existing helper `_transform_input_file_item_to_file_item` (around line 1215 of the same file) already knows how to convert an `input_file` to a chat-completions `{"type": "file", "file": {...}}` content part — but it's only called from `_transform_responses_api_content_to_chat_completion_content` (user/system message content path, around line 1270), not from the `function_call_output.output` path.

Suggested fix

Mirror the approach taken in #17799 (which fixed the reverse-direction analogue for `input_image` in tool outputs).

In `_normalize_function_call_output_to_tool_content`, add an `input_file` branch that reuses the existing helper:

```python elif part_type == "input_file": chat_part = LiteLLMCompletionResponsesConfig._transform_input_file_item_to_file_item(part) parts.append(chat_part) ```

…plus whatever the surrounding code already does to make the resulting tool message use a list `content` rather than a flattened string when any non-text part is present.

Happy to send a PR if there's interest.

Related

  • #17507 / #17762 / #17799 — same bug class, opposite direction (Chat→Responses, `input_image`).
  • #24919 — adjacent open issue on user-message `input_image` with `file_id` in the same module.

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on?

v1.83.3 (bug also present on `main` per source inspection)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING