From 907ad4cbb92a002c3b134a0b554bd1bcc436f7b3 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:53:45 -0700 Subject: [PATCH 1/6] fix(proxy): list_files honors AsyncCursorPage from post-call hook (LIT-3386) --- litellm/proxy/openai_files_endpoints/files_endpoints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 378cbbda89c0..7b2330767fbe 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -45,6 +45,7 @@ OpenAIFileObject, OpenAIFilesPurpose, ) +from openai.pagination import AsyncCursorPage # noqa: E402 # used in list_files post-call type check from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, @@ -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 ### ALERTING ### From 33114e54721b334c0805baaacf9e9228edbd2943 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:53:47 -0700 Subject: [PATCH 2/6] test(proxy): pin list_files hook AsyncCursorPage return is honored (LIT-3386) --- .../test_list_files_post_call_hook.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py new file mode 100644 index 000000000000..da51d8daf474 --- /dev/null +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py @@ -0,0 +1,129 @@ +"""Regression tests for LIT-3386 / GH #28294 (Point 72). + +The list_files endpoint runs proxy_logging_obj.post_call_success_hook after the +upstream provider call. UnifiedFileIdHook (managed files) returns an +openai.pagination.AsyncCursorPage for the list-files response shape (filtered +to the caller's owned files). + +Previously the endpoint guarded reassignment with +isinstance(_response, OpenAIFileObject), which is always False for an +AsyncCursorPage, so the hook return was silently discarded. The in-place +mutation of response.data inside the hook masked the bug in production paths +but the type check itself was incorrect and would fail for any future hook +that returns a fresh AsyncCursorPage instance. + +This regression test pins the broadened type check +(OpenAIFileObject, AsyncCursorPage). +""" +import pytest +from unittest.mock import patch +from fastapi.testclient import TestClient +from openai.pagination import AsyncCursorPage +from openai.types.file_object import FileObject + + +def _file(file_id): + return FileObject( + id=file_id, + object="file", + bytes=10, + created_at=0, + filename="x.jsonl", + purpose="batch", + status="processed", + ) + + +@pytest.fixture +def client(): + # Import inside the fixture so we resolve the live proxy_server module after + # tests/test_litellm/conftest.py::setup_and_teardown has reloaded it. + from litellm.proxy import proxy_server + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.auth.user_api_key_auth import ( + user_api_key_auth as user_api_key_auth_dep, + ) + + def _override_auth(): + return UserAPIKeyAuth(user_id="user-42", api_key="sk-test", token="sk-test") + + proxy_server.app.dependency_overrides[user_api_key_auth_dep] = _override_auth + yield TestClient(proxy_server.app) + proxy_server.app.dependency_overrides.pop(user_api_key_auth_dep, None) + + +@pytest.fixture +def unfiltered_page(): + return AsyncCursorPage( + data=[ + _file("file-raw-input-aaa"), + _file("file-raw-output-bbb"), + _file("file-raw-other-ccc"), + ] + ) + + +def _patch_provider(unfiltered_page): + import litellm + + async def _fake(*args, **kwargs): + return unfiltered_page + + return patch.object(litellm, "afile_list", side_effect=_fake) + + +def _patch_hook(side_effect): + # Patch the live module attribute, not a stale imported reference - + # conftest.py reloads litellm.proxy.proxy_server at module scope, which + # replaces proxy_logging_obj. + from litellm.proxy import proxy_server + + return patch.object( + proxy_server.proxy_logging_obj, + "post_call_success_hook", + side_effect=side_effect, + ) + + +def test_list_files_honors_async_cursor_page_returned_by_hook(client, unfiltered_page): + """LIT-3386: list_files must honor AsyncCursorPage returned by the post-call hook. + + The UnifiedFileIdHook (managed files) returns a filtered AsyncCursorPage for + file-list responses. Previously the endpoint's isinstance check only allowed + OpenAIFileObject, silently discarding the hook return. With Point 72's + secondary bug fixed, the endpoint reassigns response to the hook return. + """ + filtered_page = AsyncCursorPage(data=[_file("litellm_proxy:managed-aaa")]) + + async def _hook(*, data, user_api_key_dict, response): + return filtered_page + + with _patch_provider(unfiltered_page), _patch_hook(_hook): + r = client.get("/v1/files?purpose=batch") + + assert r.status_code == 200, r.text + ids = [f["id"] for f in r.json()["data"]] + assert ids == ["litellm_proxy:managed-aaa"], ( + f"list_files dropped the hook return value; got {ids}. The endpoint " + "type check must include AsyncCursorPage." + ) + + +def test_list_files_passes_through_unchanged_when_hook_returns_none( + client, unfiltered_page +): + """Hook returns None - endpoint must keep the upstream response unchanged.""" + + async def _hook(*, data, user_api_key_dict, response): + return None + + with _patch_provider(unfiltered_page), _patch_hook(_hook): + r = client.get("/v1/files?purpose=batch") + + assert r.status_code == 200, r.text + ids = [f["id"] for f in r.json()["data"]] + assert ids == [ + "file-raw-input-aaa", + "file-raw-output-bbb", + "file-raw-other-ccc", + ] From 17685ea938c42ff4f25735aa3daf5886794656a6 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:56:52 -0700 Subject: [PATCH 3/6] style(proxy): black-format LIT-3386 changes --- litellm/proxy/openai_files_endpoints/files_endpoints.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 7b2330767fbe..fe8394063d0c 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -45,7 +45,9 @@ OpenAIFileObject, OpenAIFilesPurpose, ) -from openai.pagination import AsyncCursorPage # noqa: E402 # used in list_files post-call type check +from openai.pagination import ( + AsyncCursorPage, +) # noqa: E402 # used in list_files post-call type check from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, @@ -1378,7 +1380,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, AsyncCursorPage)): + if _response is not None and isinstance( + _response, (OpenAIFileObject, AsyncCursorPage) + ): response = _response ### ALERTING ### From 9107063a88ac1926b046439c22a9cbc96bee60c0 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:56:54 -0700 Subject: [PATCH 4/6] style(test): black-format LIT-3386 test file --- .../openai_files_endpoint/test_list_files_post_call_hook.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py index da51d8daf474..dd60bbf62d3e 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py @@ -15,6 +15,7 @@ This regression test pins the broadened type check (OpenAIFileObject, AsyncCursorPage). """ + import pytest from unittest.mock import patch from fastapi.testclient import TestClient From 0af4143ac0863f3bf75006ab7dabd7767bb35410 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:59:53 -0700 Subject: [PATCH 5/6] refactor(proxy): drop spurious noqa: E402 on AsyncCursorPage import (Greptile P2) --- litellm/proxy/openai_files_endpoints/files_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index fe8394063d0c..18ee1394213d 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -47,7 +47,7 @@ ) from openai.pagination import ( AsyncCursorPage, -) # noqa: E402 # used in list_files post-call type check +) # used in list_files post-call type check from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, From 141b669b66a86350d808447fbf48746642227b08 Mon Sep 17 00:00:00 2001 From: oss-agent-shin Date: Tue, 26 May 2026 21:59:54 -0700 Subject: [PATCH 6/6] test(proxy): add coverage for OpenAIFileObject branch of broadened tuple (Greptile P2) --- .../test_list_files_post_call_hook.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py index dd60bbf62d3e..1c7b97b53170 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_list_files_post_call_hook.py @@ -128,3 +128,39 @@ async def _hook(*, data, user_api_key_dict, response): "file-raw-output-bbb", "file-raw-other-ccc", ] + + +def test_list_files_still_honors_openai_file_object_returned_by_hook( + client, unfiltered_page +): + """Regression guard for the pre-existing OpenAIFileObject branch of the + broadened isinstance tuple. + + Future refactors that accidentally drop OpenAIFileObject from the tuple + must fail this test. + """ + from litellm.types.llms.openai import OpenAIFileObject + + synthetic = OpenAIFileObject( + id="file-from-hook", + object="file", + bytes=1, + created_at=0, + filename="y.jsonl", + purpose="batch", + status="processed", + ) + + async def _hook(*, data, user_api_key_dict, response): + return synthetic + + with _patch_provider(unfiltered_page), _patch_hook(_hook): + r = client.get("/v1/files?purpose=batch") + + assert r.status_code == 200, r.text + # When the hook returns a single OpenAIFileObject (legacy / synthetic path), + # the endpoint serializes that single object - not a list page - so the + # caller sees the object's fields at the top level. + assert ( + r.json()["id"] == "file-from-hook" + ), "Pre-existing isinstance branch for OpenAIFileObject regressed."