fix(files): return hook-filtered page from GET /v1/files

The list_files endpoint narrowed the managed-files post_call_success_hook
return to OpenAIFileObject. For list responses that hook returns an
AsyncCursorPage filtered to the caller's own files, so the isinstance guard
rejected it and the unfiltered provider response went back to the caller,
making per-user file filtering a silent no-op.

Widen the guard to also accept AsyncCursorPage so the filtered page is
returned. Add a regression asserting the endpoint returns the hook's
filtered page and another user's file is absent.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-01 00:18:04 +00:00
parent eb4f33e920
commit b6932a4929
2 changed files with 94 additions and 2 deletions

View file

@ -56,6 +56,7 @@ from litellm.repositories.table_repositories import ManagedFileRepository
from litellm.router import Router
from litellm.types.llms.openai import (
CREATE_FILE_REQUESTS_PURPOSE,
AsyncCursorPage,
FileExpiresAfter,
OpenAIFileObject,
OpenAIFilesPurpose,
@ -1403,7 +1404,9 @@ async def list_files(
_response = await proxy_logging_obj.post_call_success_hook(
data=data, user_api_key_dict=user_api_key_dict, response=response
)
if _response is not None and isinstance(_response, OpenAIFileObject):
if _response is not None and isinstance(
_response, (OpenAIFileObject, AsyncCursorPage)
):
response = _response
### ALERTING ###

View file

@ -24,7 +24,11 @@ from litellm.proxy.openai_files_endpoints.file_content_streaming_handler import
FileContentStreamingHandler,
)
from litellm.proxy.proxy_server import app
from litellm.types.llms.openai import HttpxBinaryResponseContent, OpenAIFileObject
from litellm.types.llms.openai import (
AsyncCursorPage,
HttpxBinaryResponseContent,
OpenAIFileObject,
)
client = TestClient(app)
from litellm.caching.caching import DualCache
@ -2545,6 +2549,91 @@ def test_list_files_prefers_team_byok_over_global_openai_deployment(
proxy_logging_obj.post_call_failure_hook.assert_not_called()
def test_list_files_returns_hook_filtered_page_not_unfiltered_provider_response(
mocker: MockerFixture, monkeypatch
):
"""
GET /v1/files must return the managed-files hook's per-user filtered
AsyncCursorPage, not the provider's unfiltered response. Regression for
LIT-4850 (GH #28294): the endpoint narrowed the hook return to
OpenAIFileObject, so the filtered page was discarded and another user's
files leaked back to the caller.
"""
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
router = Router(
model_list=[
{
"model_name": "openai/*",
"litellm_params": {
"model": "openai/*",
"api_key": "team-openai-key",
},
},
]
)
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, router)
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", router)
proxy_logging_obj.update_request_status = mocker.AsyncMock()
proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock()
def _file(file_id: str) -> OpenAIFileObject:
return OpenAIFileObject(
id=file_id,
bytes=1,
created_at=1,
filename=f"{file_id}.jsonl",
object="file",
purpose="batch",
status="processed",
)
caller_file = _file("file-caller")
other_user_file = _file("file-other-user")
unfiltered_page: AsyncCursorPage[OpenAIFileObject] = AsyncCursorPage(
data=[caller_file, other_user_file]
)
filtered_page: AsyncCursorPage[OpenAIFileObject] = AsyncCursorPage(
data=[caller_file]
)
proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(
return_value=filtered_page
)
async def _mock_afile_list(**kwargs):
return unfiltered_page
monkeypatch.setattr(litellm, "afile_list", _mock_afile_list)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
api_key="test-key",
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="caller-user",
team_id="test-team",
team_models=["openai/*"],
)
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
returned_ids = [f["id"] for f in response.json()["data"]]
assert returned_ids == ["file-caller"]
assert "file-other-user" not in returned_ids
proxy_logging_obj.post_call_failure_hook.assert_not_called()
def test_list_files_with_all_proxy_models_team_uses_openai_deployment(
mocker: MockerFixture, monkeypatch
):