mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
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:
parent
eb4f33e920
commit
b6932a4929
2 changed files with 94 additions and 2 deletions
|
|
@ -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 ###
|
||||
|
|
|
|||
|
|
@ -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
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue