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:
Sameer Kankute
2025-11-17 18:41:26 -08:00
committed by GitHub
parent 20ba3fce70
commit 7cebc151b0
6 changed files with 262 additions and 1 deletions
@@ -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.
+11
View File
@@ -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
#########################################################
+2
View File
@@ -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"