diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index da3b9184edb..4895322d140 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -783,15 +783,29 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): f"Chat provider: image -> {converted}" ) elif item_type == "file": - # Map Chat Completion file to Responses API input_file - # {"type": "file", "file": {"file_data": "...", "filename": "..."}} - # -> {"type": "input_file", "file_data": "...", "filename": "..."} + # Map Chat Completion file to Responses API input_file. + # {"type": "file", "file": {"file_id" | "file_data" | "filename" | "file_url": ...}} + # -> {"type": "input_file", "file_id" | "file_data" | "filename" | "file_url": ...} + # If `file_id` is an http(s) URL, route it to `file_url` instead — providers + # require `file_id` to be an uploaded file identifier (e.g. "file-abc123") and + # reject URLs there. file_data = item.get("file", {}) converted = {"type": "input_file"} if isinstance(file_data, dict): - for key in ["file_id", "file_data", "filename"]: + for key in ( + "file_id", + "file_data", + "filename", + "file_url", + ): if key in file_data: converted[key] = file_data[key] + file_id = converted.get("file_id") + if isinstance(file_id, str) and file_id.startswith( + ("https://", "http://") + ): + converted.setdefault("file_url", file_id) + converted.pop("file_id", None) result.append(converted) verbose_logger.debug( f"Chat provider: file -> {converted}" diff --git a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index e40543e01a0..20afbe0035f 100644 --- a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -2188,6 +2188,87 @@ def test_convert_chat_completion_file_type_with_file_id(): assert "file_data" not in content[1] +@pytest.mark.parametrize( + "url", + [ + "https://pdfobject.com/pdf/sample.pdf", + "http://example.com/report.pdf", + ], +) +def test_convert_chat_completion_file_type_with_url_file_id_routes_to_file_url(url): + """ + Chat Completion clients sometimes pass a URL in `file_id`. The Responses API + requires `file_id` to be an uploaded file identifier (e.g. "file-abc123") and + rejects URLs there, but it natively accepts `file_url`. The transformation + should route URL-shaped `file_id` values to `file_url` and drop `file_id`. + """ + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + + handler = LiteLLMResponsesTransformationHandler() + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Summarize this PDF."}, + { + "type": "file", + "file": {"file_id": url}, + }, + ], + } + ] + + ( + input_items, + _, + ) = handler.convert_chat_completion_messages_to_responses_api(messages) + + file_item = input_items[0]["content"][1] + assert file_item["type"] == "input_file" + assert file_item["file_url"] == url + assert "file_id" not in file_item + + +def test_convert_chat_completion_file_type_passes_through_file_url(): + """ + When the caller already provides `file_url` directly (Responses-API-shaped + payload nested inside a Chat Completion `file` block), the transformation + should pass it through unchanged. + """ + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + + handler = LiteLLMResponsesTransformationHandler() + + url = "https://example.com/report.pdf" + messages = [ + { + "role": "user", + "content": [ + { + "type": "file", + "file": {"file_url": url, "filename": "report.pdf"}, + }, + ], + } + ] + + ( + input_items, + _, + ) = handler.convert_chat_completion_messages_to_responses_api(messages) + + file_item = input_items[0]["content"][0] + assert file_item["type"] == "input_file" + assert file_item["file_url"] == url + assert file_item["filename"] == "report.pdf" + assert "file_id" not in file_item + + # ============================================================================= # Tests for reasoning_items round-trip (encrypted_content preservation) # =============================================================================