mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
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:
parent
fa81017e12
commit
40cd8427b5
2 changed files with 99 additions and 4 deletions
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue