fix(files): list unscoped managed files

Read owner-scoped managed rows directly when no provider or model is supplied, avoiding an unauthenticated OpenAI fallback.

Refs #35362
This commit is contained in:
Rithvik Mysore Suresh 2026-08-21 11:47:04 -04:00
parent ff02d5cfc0
commit f9f8320972
5 changed files with 130 additions and 23 deletions

View file

@ -1365,12 +1365,23 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
async def afile_list(
self,
purpose: Optional[OpenAIFilesPurpose],
purpose: str | None,
litellm_parent_otel_span: Optional[Span],
user_api_key_dict: UserAPIKeyAuth,
**data: Dict,
) -> List[OpenAIFileObject]:
"""Handled in files_endpoints.py"""
return []
) -> Dict[str, object]:
owner_filter: Final = build_owner_filter(user_api_key_dict)
if owner_filter is None:
return build_list_page([])
rows: Final = await _managed_file_table(self.prisma_client).find_many(where=owner_filter)
files: Final = [
parsed_file_object.model_copy(update={"id": row.unified_file_id})
for row in rows
if (parsed_file_object := _parse_managed_file_object(row.file_object, row.unified_file_id)) is not None
and (purpose is None or parsed_file_object.purpose == purpose)
]
return build_list_page(files)
def _is_batch_polling_enabled(self) -> bool:
"""

View file

@ -13,7 +13,6 @@ from litellm.types.llms.openai import (
FileContentRequest,
OpenAICreateFileRequestOptionalParams,
OpenAIFileObject,
OpenAIFilesPurpose,
)
from litellm.types.utils import LlmProviders, ModelResponse
@ -240,10 +239,11 @@ class BaseFileEndpoints(ABC):
@abstractmethod
async def afile_list(
self,
purpose: OpenAIFilesPurpose | None,
purpose: str | None,
litellm_parent_otel_span: Span | None,
user_api_key_dict: UserAPIKeyAuth,
**data: dict,
) -> list[OpenAIFileObject]:
) -> dict[str, object]:
pass
@abstractmethod

View file

@ -1488,24 +1488,28 @@ async def list_files(
or get_custom_llm_provider_from_request_headers(request=request)
or get_custom_llm_provider_from_request_query(request=request)
or await get_custom_llm_provider_from_request_body(request=request)
or "openai"
)
managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files")
if custom_llm_provider is None and isinstance(managed_files_obj, BaseFileEndpoints):
response = await managed_files_obj.afile_list(
purpose=purpose,
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
user_api_key_dict=user_api_key_dict,
)
else:
resolved_custom_llm_provider: Final = custom_llm_provider or "openai"
apply_team_provider_credentials(
data=data,
llm_router=llm_router,
user_api_key_dict=user_api_key_dict,
custom_llm_provider=resolved_custom_llm_provider,
)
# No model/target_model_names pinned: resolve upstream credentials from
# the team's deployment for this provider so the call is authenticated
# against the team's own account (e.g. the team's openai deployment).
apply_team_provider_credentials(
data=data,
llm_router=llm_router,
user_api_key_dict=user_api_key_dict,
custom_llm_provider=custom_llm_provider,
)
response = await litellm.afile_list(
custom_llm_provider=custom_llm_provider,
purpose=purpose,
**data,
)
response = await litellm.afile_list(
custom_llm_provider=resolved_custom_llm_provider,
purpose=purpose,
**data,
)
if response is None:
raise HTTPException(

View file

@ -190,6 +190,39 @@ async def test_get_user_created_file_ids_remaps_stored_raw_provider_id_to_unifie
assert files[0].purpose == raw_provider_object.purpose
@pytest.mark.asyncio
async def test_afile_list_returns_owner_scoped_managed_files():
managed_files = _make_managed_files_instance()
managed_files.prisma_client.db.litellm_managedfiletable.find_many = AsyncMock(
return_value=[
MagicMock(
file_object=_make_file_object("file-provider-id").model_dump(),
unified_file_id="unified-file-id",
),
MagicMock(
file_object=_make_file_object("file-other-purpose").model_copy(
update={"purpose": "batch"}
).model_dump(),
unified_file_id="unified-other-purpose",
),
]
)
response = await managed_files.afile_list(
purpose="batch_output",
litellm_parent_otel_span=None,
user_api_key_dict=_make_user_api_key_dict(),
)
managed_files.prisma_client.db.litellm_managedfiletable.find_many.assert_awaited_once_with(
where={"created_by": "test-user"}
)
assert [file.id for file in response["data"]] == ["unified-file-id"]
assert response["first_id"] == "unified-file-id"
assert response["last_id"] == "unified-file-id"
assert response["has_more"] is False
@pytest.mark.asyncio
async def test_parse_managed_file_object_warning_omits_rejected_values(caplog):
from litellm_enterprise.proxy.hooks.managed_files import (

View file

@ -2468,6 +2468,65 @@ def test_list_files_without_target_model_names_uses_team_openai_deployment(
proxy_logging_obj.post_call_failure_hook.assert_not_called()
def test_unscoped_list_files_uses_managed_file_store(
mocker: MockerFixture, monkeypatch, llm_router: Router
):
import litellm.proxy.proxy_server as ps
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.proxy._types import LitellmUserRoles
managed_file = OpenAIFileObject(
id="unified-file-id",
object="file",
bytes=100,
created_at=1700000000,
filename="output.jsonl",
purpose="batch_output",
status="processed",
)
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
managed_files = mocker.MagicMock(spec=BaseFileEndpoints)
managed_files.afile_list = mocker.AsyncMock(
return_value={
"object": "list",
"data": [managed_file],
"first_id": managed_file.id,
"last_id": managed_file.id,
"has_more": False,
}
)
proxy_logging_obj.proxy_hook_mapping["managed_files"] = managed_files
proxy_logging_obj.update_request_status = mocker.AsyncMock()
proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=None)
proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
provider_list = mocker.patch.object(litellm, "afile_list", new=mocker.AsyncMock())
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
api_key="test-key",
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="test-user",
)
try:
response = client.get(
"/v1/files",
headers={"Authorization": "Bearer test-key"},
)
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
assert response.status_code == 200, response.text
assert response.json()["data"][0]["id"] == "unified-file-id"
managed_files.afile_list.assert_awaited_once()
assert managed_files.afile_list.await_args.kwargs["user_api_key_dict"].user_id == "test-user"
provider_list.assert_not_awaited()
proxy_logging_obj.post_call_failure_hook.assert_not_called()
def test_list_files_restricted_team_does_not_leak_global_openai_credentials(
mocker: MockerFixture, monkeypatch
):