fix(responses): route URL-shaped file_id to file_url in input_file

When a Chat Completion request carries a `file` block whose `file_id` is
actually an http(s) URL, the Responses API rejects it: providers (OpenAI,
Azure) require `file_id` to be an uploaded file identifier such as
`file-abc123`. The Responses API does, however, natively accept a `file_url`
field on `input_file` for URL inputs.

Detect URL-shaped `file_id` values during the
`/chat/completions -> /responses` transformation and route them to
`file_url`, dropping the original `file_id`. Also include `file_url` in the
set of keys forwarded from the Chat Completion `file` block so callers can
pass it through directly.

Adds tests for URL-shaped `file_id` -> `file_url` mapping and for native
`file_url` passthrough alongside the existing file_data / file_id tests.
This commit is contained in:
xander 2026-05-09 16:14:50 +08:00
parent fa81017e12
commit 40cd8427b5
2 changed files with 99 additions and 4 deletions

View file

@ -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}"

View file

@ -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)
# =============================================================================