mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Merge pull request #29832 from hclsys/fix/files-decode-encoded-id-in-chat-and-responses
fix(files): decode x-litellm-model encoded file_id in chat + responses
This commit is contained in:
commit
a28f075f45
2 changed files with 102 additions and 29 deletions
|
|
@ -470,6 +470,8 @@ def update_messages_with_model_file_ids(
|
|||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
_is_base64_encoded_unified_file_id,
|
||||
convert_b64_uid_to_unified_uid,
|
||||
get_original_file_id,
|
||||
is_model_embedded_id,
|
||||
)
|
||||
|
||||
for message in messages:
|
||||
|
|
@ -508,6 +510,11 @@ def update_messages_with_model_file_ids(
|
|||
unified_file_id = convert_b64_uid_to_unified_uid(file_id)
|
||||
if "llm_output_file_id," in unified_file_id:
|
||||
provider_file_id = unified_file_id.split("llm_output_file_id,")[1].split(";")[0]
|
||||
if not provider_file_id and is_model_embedded_id(file_id):
|
||||
# `litellm:<raw_id>;model,<m>` encoding from the
|
||||
# x-litellm-model upload path. Strip the wrapper
|
||||
# so the provider sees its own ID.
|
||||
provider_file_id = get_original_file_id(file_id)
|
||||
file_object_file_field["file_id"] = provider_file_id or file_id
|
||||
if format:
|
||||
file_object_file_field["format"] = format
|
||||
|
|
@ -535,6 +542,8 @@ def update_responses_input_with_model_file_ids(
|
|||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
_is_base64_encoded_unified_file_id,
|
||||
convert_b64_uid_to_unified_uid,
|
||||
get_original_file_id,
|
||||
is_model_embedded_id,
|
||||
)
|
||||
|
||||
if isinstance(input, str):
|
||||
|
|
@ -578,6 +587,13 @@ def update_responses_input_with_model_file_ids(
|
|||
updated_content_item = content_item.copy()
|
||||
updated_content_item["file_id"] = provider_file_id
|
||||
updated_content.append(updated_content_item)
|
||||
elif is_model_embedded_id(file_id):
|
||||
# `litellm:<raw_id>;model,<m>` encoding from the
|
||||
# x-litellm-model upload path. Strip the wrapper
|
||||
# so the provider sees its own ID.
|
||||
updated_content_item = content_item.copy()
|
||||
updated_content_item["file_id"] = get_original_file_id(file_id)
|
||||
updated_content.append(updated_content_item)
|
||||
else:
|
||||
# Not a managed file, keep as-is
|
||||
updated_content.append(content_item)
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
|
||||
|
||||
def test_get_format_from_file_id():
|
||||
unified_file_id = (
|
||||
"litellm_proxy:application/pdf;unified_id,cbbe3534-8bf8-4386-af00-f5f6b7e370bf"
|
||||
)
|
||||
unified_file_id = "litellm_proxy:application/pdf;unified_id,cbbe3534-8bf8-4386-af00-f5f6b7e370bf"
|
||||
|
||||
format = get_format_from_file_id(unified_file_id)
|
||||
|
||||
|
|
@ -48,9 +46,7 @@ def test_update_messages_with_model_file_ids():
|
|||
|
||||
model_file_id_mapping = {file_id: {"my_model_id": "provider_file_id"}}
|
||||
|
||||
updated_messages = update_messages_with_model_file_ids(
|
||||
messages, model_id, model_file_id_mapping
|
||||
)
|
||||
updated_messages = update_messages_with_model_file_ids(messages, model_id, model_file_id_mapping)
|
||||
|
||||
assert updated_messages == [
|
||||
{
|
||||
|
|
@ -143,9 +139,7 @@ def test_add_system_prompt_to_messages_merge_with_first_system():
|
|||
{"role": "system", "content": "Existing system prompt."},
|
||||
{"role": "user", "content": "Hello"},
|
||||
]
|
||||
result = add_system_prompt_to_messages(
|
||||
messages, "You are helpful.", merge_with_first_system=True
|
||||
)
|
||||
result = add_system_prompt_to_messages(messages, "You are helpful.", merge_with_first_system=True)
|
||||
assert result == [
|
||||
{"role": "system", "content": "You are helpful.\n\nExisting system prompt."},
|
||||
{"role": "user", "content": "Hello"},
|
||||
|
|
@ -155,9 +149,7 @@ def test_add_system_prompt_to_messages_merge_with_first_system():
|
|||
def test_add_system_prompt_to_messages_merge_with_first_system_adds_new_when_no_system():
|
||||
"""When merge_with_first_system=True but no system message, adds new one at start."""
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
result = add_system_prompt_to_messages(
|
||||
messages, "You are helpful.", merge_with_first_system=True
|
||||
)
|
||||
result = add_system_prompt_to_messages(messages, "You are helpful.", merge_with_first_system=True)
|
||||
assert result == [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "user", "content": "Hello"},
|
||||
|
|
@ -492,14 +484,8 @@ def test_update_messages_with_model_file_ids_tolerates_non_dict_content_items():
|
|||
messages_token_ids_batch = [{"role": "user", "content": [[15496, 995], [9906, 0]]}]
|
||||
|
||||
# Both should pass through unchanged without raising.
|
||||
assert (
|
||||
update_messages_with_model_file_ids(messages_token_ids, "model-A", {})
|
||||
== messages_token_ids
|
||||
)
|
||||
assert (
|
||||
update_messages_with_model_file_ids(messages_token_ids_batch, "model-A", {})
|
||||
== messages_token_ids_batch
|
||||
)
|
||||
assert update_messages_with_model_file_ids(messages_token_ids, "model-A", {}) == messages_token_ids
|
||||
assert update_messages_with_model_file_ids(messages_token_ids_batch, "model-A", {}) == messages_token_ids_batch
|
||||
|
||||
|
||||
class TestExtractFileDataBareStr:
|
||||
|
|
@ -645,9 +631,7 @@ class TestUnpackLegacyDefs:
|
|||
definitions = {
|
||||
f"L{i}": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
f"x{j}": {"$ref": f"#/definitions/L{i + 1}"} for j in range(fanout)
|
||||
},
|
||||
"properties": {f"x{j}": {"$ref": f"#/definitions/L{i + 1}"} for j in range(fanout)},
|
||||
}
|
||||
for i in range(depth)
|
||||
}
|
||||
|
|
@ -712,9 +696,7 @@ class TestUnpackLegacyDefs:
|
|||
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
f"r{i}": {"$ref": f"#/components/schemas/T{i}"} for i in range(50)
|
||||
},
|
||||
"properties": {f"r{i}": {"$ref": f"#/components/schemas/T{i}"} for i in range(50)},
|
||||
"components": {
|
||||
"schemas": {
|
||||
f"T{i}": {
|
||||
|
|
@ -739,9 +721,7 @@ class TestTextCompletionPromptToMessages:
|
|||
text_completion_prompt_to_messages,
|
||||
)
|
||||
|
||||
assert text_completion_prompt_to_messages("summarize this") == (
|
||||
{"role": "user", "content": "summarize this"},
|
||||
)
|
||||
assert text_completion_prompt_to_messages("summarize this") == ({"role": "user", "content": "summarize this"},)
|
||||
|
||||
def test_list_of_strings_becomes_one_message_each(self):
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
|
|
@ -970,3 +950,80 @@ class TestCustomToolFormatShapeConversion:
|
|||
for weird in ({}, {"type": "grammar"}, {"type": "future_format", "x": 1}):
|
||||
assert convert_custom_tool_format_to_chat_shape(dict(weird)) in (weird, {"type": "grammar", "grammar": {}})
|
||||
assert convert_custom_tool_format_to_responses_shape(dict(weird)) == weird
|
||||
|
||||
|
||||
# --- x-litellm-model upload-path decoding (litellm #29830) -------------------
|
||||
|
||||
|
||||
def _xlitellm_encoded(raw_id: str, model: str) -> str:
|
||||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
encode_file_id_with_model,
|
||||
)
|
||||
|
||||
return encode_file_id_with_model(raw_id, model)
|
||||
|
||||
|
||||
def test_update_messages_with_model_file_ids_decodes_xlitellm_encoded_id():
|
||||
"""x-litellm-model upload returns `file-<b64(litellm:<raw>;model,<m>)>`.
|
||||
Without decoding, the encoded id leaks to upstream OpenAI and errors as
|
||||
'Files [...] were not found'. Decode it back to raw provider id."""
|
||||
raw_id = "file-ExTuCawUqxEMjVFK6xwR9B"
|
||||
encoded_id = _xlitellm_encoded(raw_id, "gpt-5.1")
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Summarize this."},
|
||||
{"type": "file", "file": {"file_id": encoded_id}},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
updated = update_messages_with_model_file_ids(messages, "model-A", {})
|
||||
|
||||
assert updated[0]["content"][1]["file"]["file_id"] == raw_id
|
||||
|
||||
|
||||
def test_update_responses_input_with_model_file_ids_decodes_xlitellm_encoded_id():
|
||||
"""Same bug on /v1/responses path. Without decoding the encoded id (>64
|
||||
chars), OpenAI rejects with 'string too long. Expected ... maximum length
|
||||
64'."""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
raw_id = "file-ExTuCawUqxEMjVFK6xwR9B"
|
||||
encoded_id = _xlitellm_encoded(raw_id, "gpt-5.1")
|
||||
input_items = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Summarize."},
|
||||
{"type": "input_file", "file_id": encoded_id},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
updated = update_responses_input_with_model_file_ids(input_items)
|
||||
|
||||
assert updated[0]["content"][1]["file_id"] == raw_id
|
||||
|
||||
|
||||
def test_update_messages_xlitellm_decode_does_not_override_mapping():
|
||||
"""If the call-site already resolved a provider id via the mapping, that
|
||||
wins. The new decode fallback runs only when no mapping match."""
|
||||
raw_id = "file-ExTuCawUqxEMjVFK6xwR9B"
|
||||
encoded_id = _xlitellm_encoded(raw_id, "gpt-5.1")
|
||||
mapping = {encoded_id: {"model-A": "provider-explicit-id"}}
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "file", "file": {"file_id": encoded_id}},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
updated = update_messages_with_model_file_ids(messages, "model-A", mapping)
|
||||
|
||||
assert updated[0]["content"][0]["file"]["file_id"] == "provider-explicit-id"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue