From 7ce0bf2c7b8664e13982300964d6d7680f552f9d Mon Sep 17 00:00:00 2001 From: Ines Magessi Date: Tue, 19 May 2026 10:38:45 +0200 Subject: [PATCH] fix(responses): preserve input_file in function_call_output during chat lowering closes #28232 --- .../transformation.py | 13 +++- ...test_function_call_output_normalization.py | 67 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index e2ba8353591..4acdf4c719f 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -1085,9 +1085,18 @@ class LiteLLMCompletionResponsesConfig: "image_url": {"url": image_url_val}, } ) + elif part_type in ("input_file", "file"): + normalized_blocks.append( + LiteLLMCompletionResponsesConfig._transform_input_file_item_to_file_item( + part + ) + ) - # Prefer structured blocks if we have images; otherwise return a string. - if any(b.get("type") == "image_url" for b in normalized_blocks): + # Prefer structured blocks if we have any non-text part + # (images or files); otherwise return a string. + if any( + b.get("type") in ("image_url", "file") for b in normalized_blocks + ): # Ensure we include any accumulated text as text blocks too return normalized_blocks if text_acc: diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py b/tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py index 3af1ef51e3e..b2904960253 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_function_call_output_normalization.py @@ -40,3 +40,70 @@ def test_function_call_output_string_passthrough(): ) assert len(out) == 1 assert out[0]["content"] == '{"ok":true}' + + +def test_function_call_output_input_file_is_preserved_as_file_block(): + """A PDF returned from a tool call (input_file part inside + function_call_output.output) must survive the lowering to a chat-completions + tool message as a structured file content block. Previously the file part + was silently dropped and only the input_text sibling reached the model.""" + pdf_data_url = "data:application/pdf;base64,JVBERi0xLjQKJfb==" + + out = LiteLLMCompletionResponsesConfig._transform_responses_api_tool_call_output_to_chat_completion_message( + tool_call_output={ + "type": "function_call_output", + "call_id": "call_pdf", + "output": [ + {"type": "input_text", "text": "Here is the PDF."}, + { + "type": "input_file", + "file_data": pdf_data_url, + "filename": "test.pdf", + }, + ], + } + ) + + assert len(out) == 1 + msg = out[0] + assert msg["role"] == "tool" + assert msg["tool_call_id"] == "call_pdf" + + content = msg["content"] + assert isinstance(content, list), ( + "expected structured content list when a file part is present, " + f"got {type(content).__name__}: {content!r}" + ) + + text_blocks = [b for b in content if b.get("type") == "text"] + file_blocks = [b for b in content if b.get("type") == "file"] + + assert any(b.get("text") == "Here is the PDF." for b in text_blocks) + assert len(file_blocks) == 1 + assert file_blocks[0]["file"]["file_data"] == pdf_data_url + + +def test_function_call_output_input_file_with_file_id_is_preserved(): + """Same as above but for the file_id form (no inline bytes).""" + out = LiteLLMCompletionResponsesConfig._transform_responses_api_tool_call_output_to_chat_completion_message( + tool_call_output={ + "type": "function_call_output", + "call_id": "call_pdf_id", + "output": [ + {"type": "input_text", "text": "See attached."}, + { + "type": "input_file", + "file_id": "file-abc123", + "filename": "report.pdf", + }, + ], + } + ) + + assert len(out) == 1 + content = out[0]["content"] + assert isinstance(content, list) + + file_blocks = [b for b in content if b.get("type") == "file"] + assert len(file_blocks) == 1 + assert file_blocks[0]["file"]["file_id"] == "file-abc123"