mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 06:22:48 +00:00
Add managed files support for responses API (#16733)
* Fix responses api with managed files * fix litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py mypy * fix litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py mypy * fix mypy errors
This commit is contained in:
@@ -21,7 +21,7 @@ Available via the `litellm[proxy]` package or any `litellm` docker image.
|
||||
| Proxy | ✅ | |
|
||||
| SDK | ❌ | Requires postgres DB for storing file ids. |
|
||||
| Available across all providers | ✅ | |
|
||||
| Supported endpoints | `/chat/completions`, `/batch`, `/fine_tuning` | |
|
||||
| Supported endpoints | `/chat/completions`, `/batch`, `/fine_tuning`, `/responses` | |
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -296,6 +296,16 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
file_ids, user_api_key_dict.parent_otel_span
|
||||
)
|
||||
|
||||
data["model_file_id_mapping"] = model_file_id_mapping
|
||||
elif call_type == CallTypes.aresponses.value or call_type == CallTypes.responses.value:
|
||||
# Handle managed files in responses API input
|
||||
input_data = data.get("input")
|
||||
if input_data:
|
||||
file_ids = self.get_file_ids_from_responses_input(input_data)
|
||||
if file_ids:
|
||||
model_file_id_mapping = await self.get_model_file_id_mapping(
|
||||
file_ids, user_api_key_dict.parent_otel_span
|
||||
)
|
||||
data["model_file_id_mapping"] = model_file_id_mapping
|
||||
elif call_type == CallTypes.afile_content.value:
|
||||
retrieve_file_id = cast(Optional[str], data.get("file_id"))
|
||||
@@ -453,6 +463,47 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
||||
file_ids.append(file_id)
|
||||
return file_ids
|
||||
|
||||
def get_file_ids_from_responses_input(
|
||||
self, input: Union[str, List[Dict[str, Any]]]
|
||||
) -> List[str]:
|
||||
"""
|
||||
Gets file ids from responses API input.
|
||||
|
||||
The input can be:
|
||||
- A string (no files)
|
||||
- A list of input items, where each item can have:
|
||||
- type: "input_file" with file_id
|
||||
- content: a list that can contain items with type: "input_file" and file_id
|
||||
"""
|
||||
file_ids: List[str] = []
|
||||
|
||||
if isinstance(input, str):
|
||||
return file_ids
|
||||
|
||||
if not isinstance(input, list):
|
||||
return file_ids
|
||||
|
||||
for item in input:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
# Check for direct input_file type
|
||||
if item.get("type") == "input_file":
|
||||
file_id = item.get("file_id")
|
||||
if file_id:
|
||||
file_ids.append(file_id)
|
||||
|
||||
# Check for input_file in content array
|
||||
content = item.get("content")
|
||||
if isinstance(content, list):
|
||||
for content_item in content:
|
||||
if isinstance(content_item, dict) and content_item.get("type") == "input_file":
|
||||
file_id = content_item.get("file_id")
|
||||
if file_id:
|
||||
file_ids.append(file_id)
|
||||
|
||||
return file_ids
|
||||
|
||||
async def get_model_file_id_mapping(
|
||||
self, file_ids: List[str], litellm_parent_otel_span: Span
|
||||
) -> dict:
|
||||
|
||||
@@ -437,6 +437,66 @@ def update_messages_with_model_file_ids(
|
||||
return messages
|
||||
|
||||
|
||||
def update_responses_input_with_model_file_ids(
|
||||
input: Any,
|
||||
) -> Union[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Updates responses API input with provider-specific file IDs.
|
||||
File IDs are always inside the content array, not as direct input_file items.
|
||||
|
||||
For managed files (unified file IDs), decodes the base64-encoded unified file ID
|
||||
and extracts the llm_output_file_id directly.
|
||||
"""
|
||||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
_is_base64_encoded_unified_file_id,
|
||||
convert_b64_uid_to_unified_uid,
|
||||
)
|
||||
|
||||
if isinstance(input, str):
|
||||
return input
|
||||
|
||||
if not isinstance(input, list):
|
||||
return input
|
||||
|
||||
updated_input = []
|
||||
for item in input:
|
||||
if not isinstance(item, dict):
|
||||
updated_input.append(item)
|
||||
continue
|
||||
|
||||
updated_item = item.copy()
|
||||
content = item.get("content")
|
||||
if isinstance(content, list):
|
||||
updated_content = []
|
||||
for content_item in content:
|
||||
if isinstance(content_item, dict) and content_item.get("type") == "input_file":
|
||||
file_id = content_item.get("file_id")
|
||||
if file_id:
|
||||
# Check if this is a managed file ID (base64-encoded unified file ID)
|
||||
is_unified_file_id = _is_base64_encoded_unified_file_id(file_id)
|
||||
if is_unified_file_id:
|
||||
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]
|
||||
else:
|
||||
# Fallback: keep original if we can't extract
|
||||
provider_file_id = file_id
|
||||
updated_content_item = content_item.copy()
|
||||
updated_content_item["file_id"] = provider_file_id
|
||||
updated_content.append(updated_content_item)
|
||||
else:
|
||||
updated_content.append(content_item)
|
||||
else:
|
||||
updated_content.append(content_item)
|
||||
else:
|
||||
updated_content.append(content_item)
|
||||
updated_item["content"] = updated_content
|
||||
|
||||
updated_input.append(updated_item)
|
||||
|
||||
return updated_input
|
||||
|
||||
|
||||
def extract_file_data(file_data: FileTypes) -> ExtractedFileData:
|
||||
"""
|
||||
Extracts and processes file data from various input formats.
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import (
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
import httpx
|
||||
@@ -37,6 +38,9 @@ from litellm.types.llms.openai import (
|
||||
ToolChoice,
|
||||
ToolParam,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
# Handle ResponseText import with fallback
|
||||
if TYPE_CHECKING:
|
||||
@@ -555,6 +559,13 @@ def responses(
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
)
|
||||
|
||||
#########################################################
|
||||
# Update input with provider-specific file IDs if managed files are used
|
||||
#########################################################
|
||||
input = cast(Union[str, ResponseInputParam], update_responses_input_with_model_file_ids(input=input))
|
||||
local_vars["input"] = input
|
||||
|
||||
#########################################################
|
||||
# Native MCP Responses API
|
||||
#########################################################
|
||||
|
||||
@@ -390,6 +390,8 @@ CallTypesLiteral = Literal[
|
||||
"vector_store_file_delete",
|
||||
"avector_store_file_delete",
|
||||
"call_mcp_tool",
|
||||
"aresponses",
|
||||
"responses",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -453,3 +453,140 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation():
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for task in tasks:
|
||||
assert task.exception() is None, f"Error: {task.exception()}"
|
||||
|
||||
|
||||
def test_update_responses_input_with_unified_file_id():
|
||||
"""
|
||||
Test that update_responses_input_with_model_file_ids correctly decodes
|
||||
unified file IDs and extracts llm_output_file_id from responses API input.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
# Create a base64-encoded unified file ID
|
||||
# This decodes to: litellm_proxy:application/pdf;unified_id,6c0b5890-8914-48e0-b8f4-0ae5ed3c14a5;target_model_names,gpt-4o;llm_output_file_id,file-ECBPW7ML9g7XHdwGgUPZaM;llm_output_file_model_id,e26453f9e76e7993680d0068d98c1f4cc205bbad0967a33c664893568ca743c2
|
||||
unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My"
|
||||
|
||||
# Test input with unified file ID in content array
|
||||
input_data = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_id": unified_file_id,
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "What is the first dragon in the book?",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
# Update the input
|
||||
updated_input = update_responses_input_with_model_file_ids(input=input_data)
|
||||
|
||||
# Verify the file_id was updated to the provider-specific file ID
|
||||
assert updated_input[0]["content"][0]["type"] == "input_file"
|
||||
assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM"
|
||||
assert updated_input[0]["content"][1]["type"] == "input_text"
|
||||
assert updated_input[0]["content"][1]["text"] == "What is the first dragon in the book?"
|
||||
|
||||
|
||||
def test_update_responses_input_with_regular_file_id():
|
||||
"""
|
||||
Test that update_responses_input_with_model_file_ids keeps regular
|
||||
OpenAI file IDs unchanged.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
# Regular OpenAI file ID (not a unified file ID)
|
||||
regular_file_id = "file-abc123xyz"
|
||||
|
||||
input_data = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_id": regular_file_id,
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "What is this file?",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
# Update the input
|
||||
updated_input = update_responses_input_with_model_file_ids(input=input_data)
|
||||
|
||||
# Verify the file_id was kept unchanged (regular OpenAI file ID)
|
||||
assert updated_input[0]["content"][0]["type"] == "input_file"
|
||||
assert updated_input[0]["content"][0]["file_id"] == regular_file_id
|
||||
assert updated_input[0]["content"][1]["type"] == "input_text"
|
||||
|
||||
|
||||
def test_update_responses_input_with_string_input():
|
||||
"""
|
||||
Test that update_responses_input_with_model_file_ids returns string input unchanged.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
input_data = "What is AI?"
|
||||
|
||||
updated_input = update_responses_input_with_model_file_ids(input=input_data)
|
||||
|
||||
assert updated_input == input_data
|
||||
assert isinstance(updated_input, str)
|
||||
|
||||
|
||||
def test_update_responses_input_with_multiple_file_ids():
|
||||
"""
|
||||
Test that update_responses_input_with_model_file_ids handles multiple file IDs
|
||||
(both unified and regular) in the same input.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
update_responses_input_with_model_file_ids,
|
||||
)
|
||||
|
||||
# Unified file ID
|
||||
unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My"
|
||||
# Regular OpenAI file ID
|
||||
regular_file_id = "file-regular123"
|
||||
|
||||
input_data = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_id": unified_file_id,
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Compare these files",
|
||||
},
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_id": regular_file_id,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
updated_input = update_responses_input_with_model_file_ids(input=input_data)
|
||||
|
||||
# Verify unified file ID was updated
|
||||
assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM"
|
||||
# Verify regular file ID was kept unchanged
|
||||
assert updated_input[0]["content"][2]["file_id"] == regular_file_id
|
||||
# Verify text content was preserved
|
||||
assert updated_input[0]["content"][1]["text"] == "Compare these files"
|
||||
|
||||
Reference in New Issue
Block a user