Skip to content

fix(proxy): list_files honors AsyncCursorPage from post-call hook (LIT-3386)#28957

Closed
oss-agent-shin wants to merge 6 commits into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:shin/lit-3386-broaden-list-files-hook-type-check
Closed

fix(proxy): list_files honors AsyncCursorPage from post-call hook (LIT-3386)#28957
oss-agent-shin wants to merge 6 commits into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:shin/lit-3386-broaden-list-files-hook-type-check

Conversation

@oss-agent-shin

@oss-agent-shin oss-agent-shin commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the remaining secondary bug called out in GH #28294

The customer's analysis of #28339 and #27984 confirmed:

"The type check isinstance(_response, OpenAIFileObject) in files_endpoints.py:list_files (around line 1378) also remains. The managed files hook returns AsyncCursorPage for the file list, which is correctly handled in the hook (managed_files.py:1166) but the endpoint's post-call check discards it. This is masked by the in-place mutation of response.data, but the type check is still incorrect."

After proxy_logging_obj.post_call_success_hook runs, list_files reassigns the response only when the hook returns an OpenAIFileObject. UnifiedFileIdHook.async_post_call_success_hook actually returns an openai.pagination.AsyncCursorPage for the list-files response shape (see enterprise/litellm_enterprise/proxy/hooks/managed_files.py:1209-1232), so isinstance(_response, OpenAIFileObject) is always False and the hook's return value is silently dropped. In practice the bug is partially masked by the hook also mutating response.data in place, but the type check itself is wrong and any future hook that returns a fresh AsyncCursorPage (built from a different upstream payload, or rebuilt to drop a forbidden item, etc.) is silently discarded.

The other site that uses the same pattern — acreate_file (POST /v1/files) at line ~524 — does not need this change because acreate_file only returns OpenAIFileObject.

Fix 1 from the GH issue (CheckBatchCost poller writing output files with created_by = default_user_id instead of job.created_by) is already in place — see the _minimal_auth block at enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py:307-326, which was added in #27984 (5/15). That code path passes user_id=job.created_by into store_unified_file_id, which is what writes LiteLLM_ManagedFileTable.created_by. No change needed there in this PR.

Change

--- a/litellm/proxy/openai_files_endpoints/files_endpoints.py
+++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py
@@ -45,6 +45,7 @@ from litellm.types.llms.openai import (
     OpenAIFileObject,
     OpenAIFilesPurpose,
 )
+from openai.pagination import AsyncCursorPage  # noqa: E402  # used in list_files post-call type check
@@ -1377,7 +1378,7 @@ 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

Two new pytest cases pin the behavior at the FastAPI endpoint layer (TestClient against the live /v1/files route, real app.dependency_overrides, real proxy_logging_obj).

Evidence

BEFORE the fix — the regression test fails because the hook's filtered AsyncCursorPage is dropped and the endpoint returns the raw upstream AsyncCursorPage to the caller:

$ pytest tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py -v
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_honors_async_cursor_page_returned_by_hook FAILED
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_passes_through_unchanged_when_hook_returns_none PASSED

E   AssertionError: list_files dropped the hook return value;
E   got ['file-raw-input-aaa', 'file-raw-output-bbb', 'file-raw-other-ccc'].
E   The endpoint type check must include AsyncCursorPage.

1 failed, 1 passed in 8.95s

AFTER the fix — both tests pass; the endpoint reassigns response to the hook's filtered AsyncCursorPage, so the caller sees only their own (managed-id) files:

$ pytest tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py -v
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_honors_async_cursor_page_returned_by_hook PASSED
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_passes_through_unchanged_when_hook_returns_none PASSED

2 passed in 9.00s

The test drives the endpoint through fastapi.testclient.TestClient (full FastAPI request/response stack), patches litellm.afile_list to act as the upstream provider, and patches proxy_server.proxy_logging_obj.post_call_success_hook to return a fresh AsyncCursorPage instance with different data. The before/after difference is observable at the HTTP layer in r.json()["data"].

Risk

  • Scope: type check broadened in exactly one place (list_files). acreate_file (POST /v1/files) unchanged.
  • Backward compat: hooks that return None still pass through (covered by test_list_files_passes_through_unchanged_when_hook_returns_none). Hooks that return OpenAIFileObject (legacy / synthetic single-file responses) still honored.
  • Today's UnifiedFileIdHook behavior: mutates response.data in place AND returns the same response, so the user-visible behavior is unchanged for the production hook. The fix corrects the type check so that any future hook (including a refactor of UnifiedFileIdHook that constructs a fresh page) is honored.

Note on diff shape

Pushed via the GitHub Contents API (current GITHUB_TOKEN lacks repo + workflow scopes). The net working-tree change is exactly the two files shown above.

Closes LIT-3386.


Verification (ship-pr)

  • Base branch: litellm_oss_agent_shin_daily_branch (per Shin policy)
  • Greptile: 5/5 ("Safe to merge")
  • Veria AI - PR Review: success
  • GitHub Actions: 20/20 workflow runs green (UI Build, Semgrep, Code Quality, Linting, all Unit Test suites, Documentation, Helm, MCP, Proxy SERVER_ROOT_PATH, etc.)
  • Check runs: 43/44 success. The 1 non-green check is codecov/patch — Codecov bot informational only, not part of GitHub Actions, not blocking the merge gate.
  • Tests: 3 new pytest cases in tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py, all passing locally. The first test fails on a reverted main, confirming it is a real regression guard.
  • Runtime evidence: BEFORE-fix and AFTER-fix pytest output captured inline above (full request/response through FastAPI TestClient against /v1/files).
  • Diff scope: exactly 2 files. No accidental changes elsewhere.
  • No secrets in PR or test fixtures.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a silent discard of the post_call_success_hook return value in list_files by broadening the post-call type guard from OpenAIFileObject to (OpenAIFileObject, AsyncCursorPage), matching the actual type returned by UnifiedFileIdHook for file-list responses.

  • files_endpoints.py: Adds AsyncCursorPage import (no spurious noqa annotation) and widens the single isinstance check at the hook reassignment site; acreate_file is intentionally unchanged as it only returns OpenAIFileObject.
  • test_list_files_post_call_hook.py: Three new mock-only regression tests cover all branches — hook returns AsyncCursorPage (the primary fix), hook returns None (passthrough), and hook returns OpenAIFileObject (legacy path guard).

Confidence Score: 5/5

Change is minimal and surgical — one import and one isinstance tuple widening — with no behaviour change for existing hooks that mutate in place or return None.

The fix touches exactly one conditional in one endpoint, is backward-compatible with every existing hook return shape, and all three branches of the broadened check are pinned by new regression tests. The import is correctly placed among other imports with no misleading annotations.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/openai_files_endpoints/files_endpoints.py Broadens post-call hook type guard in list_files to accept AsyncCursorPage in addition to OpenAIFileObject; adds clean AsyncCursorPage import between existing import blocks.
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py New regression test file with three mock-only tests pinning all branches of the broadened isinstance check: AsyncCursorPage return, None return, and legacy OpenAIFileObject return.

Reviews (2): Last reviewed commit: "test(proxy): add coverage for OpenAIFile..." | Re-trigger Greptile

Comment thread litellm/proxy/openai_files_endpoints/files_endpoints.py Outdated
@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@oss-agent-shin

Copy link
Copy Markdown
Contributor Author

Addressed both Greptile P2 comments in two commits:

  1. Dropped the spurious # noqa: E402 from the AsyncCursorPage import — E402 only fires for imports after non-import code, and that line is between two import blocks. (commit 0af4143)
  2. Added test_list_files_still_honors_openai_file_object_returned_by_hook — covers the legacy OpenAIFileObject branch of the broadened (OpenAIFileObject, AsyncCursorPage) tuple, so any future refactor that drops OpenAIFileObject will fail this test. (commit 141b669)

Test suite is now 3 passing locally:

$ pytest tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py -v
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_honors_async_cursor_page_returned_by_hook PASSED
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_passes_through_unchanged_when_hook_returns_none PASSED
tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py::test_list_files_still_honors_openai_file_object_returned_by_hook PASSED

3 passed in 8.99s

@greptile-apps please re-review.

@oss-agent-shin

Copy link
Copy Markdown
Contributor Author

Closing — bulk cleanup of PRs filed by this account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants