fix(files): honor AsyncCursorPage returned by post_call_success_hook in list_files - #28958
Conversation
…in list_files The managed-files hook returns an AsyncCursorPage for GET /v1/files responses with the data list filtered to the files the calling user owns. Before this change the list_files endpoint only honored the hook return value when it was an OpenAIFileObject, so a freshly-constructed page object was silently dropped and the unfiltered raw provider listing was returned. Refs: LIT-3386, BerriAI#28294
…st_files Asserts that when the post_call_success_hook returns a fresh AsyncCursorPage filtering out files the user does not own, the list_files endpoint uses that filtered page and does not leak the raw provider listing back to the caller. Refs: LIT-3386
|
|
Greptile SummaryFixes a silent data-leak in
Confidence Score: 5/5Safe to merge — the change is minimal, targeted, and covered by a new regression test that directly demonstrates the previously-leaking file IDs are no longer returned. The two-file diff makes a single logical change: widening one isinstance tuple so the hook's filtered page is honoured instead of dropped. The previous pyright-suppression concern is also resolved by importing AsyncCursorPage directly from openai.pagination. The regression test exercises the exact failure mode described in the issue and the full existing suite remains green. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/openai_files_endpoints/files_endpoints.py | Broadens isinstance check in list_files to accept AsyncCursorPage alongside OpenAIFileObject, and imports AsyncCursorPage directly from openai.pagination (resolving the previous pyright-suppression concern). |
| tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py | Adds a regression test that stubs afile_list to return two files and the post_call_success_hook to return a filtered AsyncCursorPage, asserting only the owned file reaches the caller. |
Reviews (2): Last reviewed commit: "refactor(files): narrow pyright suppress..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…directly from openai.pagination Addresses Greptile P2: avoid suppressing future type errors on the unrelated symbols in the litellm.types.llms.openai import block. Refs: LIT-3386
|
@greptileai review Addressed P2 from previous review: narrowed the pyright suppression by importing The behavioral fix (broadened isinstance tuple) is unchanged and the regression test still passes. |
|
Closing — bulk cleanup of PRs filed by this account. |
Problem
The managed-files hook (
enterprise/litellm_enterprise/proxy/hooks/managed_files.py::async_post_call_success_hook) returns anAsyncCursorPageforGET /v1/filesresponses withdatafiltered to the files the calling user actually owns. Prior to this PR, thelist_filesendpoint only honored the hook return value when it was anOpenAIFileObject:isinstance(AsyncCursorPage_instance, OpenAIFileObject)is always False, so the hook's return value is silently discarded. The hook also mutatesresponse.datain place inside the AsyncCursorPage branch (managed_files.py:1228), which partially masks the bug today, but the type check is still wrong and would break the moment any hook returns a freshly-constructed page object — which is exactly what happens when ownership filtering produces an empty or differentdatalist and the hook chooses to allocate a new page.This is the remaining bug from #28294 — the related issue also covers Fix 1 (raw output_file_id → managed ID conversion in
CheckBatchCost), which is already in place after #27984.Fix
Broaden the
isinstancecheck to(OpenAIFileObject, AsyncCursorPage)inlitellm/proxy/openai_files_endpoints/files_endpoints.py::list_files, and addAsyncCursorPageto the imports fromlitellm.types.llms.openai. Added aNOTEcomment explaining the masking behavior so future readers do not “simplify” the tuple back.The narrow check on the file-create path (line ~524, returning a single OpenAIFileObject) is left alone — that hook contract returns OpenAIFileObject, not a page.
Evidence
Single regression test that exercises the real
/v1/filesHTTP path withTestClient, stubs the provider list response with twoOpenAIFileObjects, and stubs the post_call_success_hook to return a freshAsyncCursorPagecontaining only the owned file.BEFORE — test fails on un-patched code
The raw provider file ID (
file-leaked-raw) bypassed the hook and leaked into the response.AFTER — test passes with the fix
Only the user-owned file is returned.
Full file_endpoint suite still green
(The 2 skipped tests are pre-existing skips for
test_create_file_and_call_chat_completion_e2eandtest_create_file_for_each_modelthat require live OpenAI credentials — unrelated to this PR.)Note on PR plumbing
This PR was pushed via the GitHub Contents API (one PUT per file) because the agent's token lacks
workflowscope forgit push/update-branch. The diff is minimal: two files, +99/-2.Refs
Verification (ship-pr)
litellm/proxy/openai_files_endpoints/files_endpoints.py::list_files— broadened isinstance tuple changes runtime behavior.