diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index 56e7f7ba633..8821736d0ff 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -315,7 +315,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): where_clause: Dict[str, Any] = {"file_purpose": "batch", **owner_filter} - if after is not None: + if after: cursor_row = ( await self.prisma_client.db.litellm_managedobjecttable.find_first( where={**where_clause, "unified_object_id": after} @@ -329,9 +329,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): page_size = limit or 20 cursor_args: Dict[str, Any] = ( - {"cursor": {"unified_object_id": after}, "skip": 1} - if after is not None - else {} + {"cursor": {"unified_object_id": after}, "skip": 1} if after else {} ) batches = await self.prisma_client.db.litellm_managedobjecttable.find_many( diff --git a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py index bbcdbac2709..50af6465d06 100644 --- a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py +++ b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py @@ -2309,6 +2309,35 @@ async def test_list_batches_rejects_unknown_after_cursor(): ) +@pytest.mark.asyncio +async def test_list_batches_treats_empty_after_as_no_cursor(): + """``?after=`` means "start from the beginning", as it always has. + + Only a cursor the client actually sent is validated, so an SDK that always + emits the query parameter does not get a 400 on its first page. + """ + from litellm.proxy._types import UserAPIKeyAuth + + rows = [_managed_batch_row(i) for i in range(2)] + prisma_client = _fake_managed_object_table(rows) + + proxy_managed_files = _PROXY_LiteLLMManagedFiles( + DualCache(), prisma_client=prisma_client + ) + + page = await proxy_managed_files.list_user_batches( + user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), limit=2, after="" + ) + + assert [batch.id for batch in page["data"]] == [ + rows[1].unified_object_id, + rows[0].unified_object_id, + ] + prisma_client.db.litellm_managedobjecttable.find_first.assert_not_called() + _, call_kwargs = prisma_client.db.litellm_managedobjecttable.find_many.call_args + assert "cursor" not in call_kwargs + + @pytest.mark.asyncio async def test_list_batches_rejects_after_cursor_owned_by_another_user(): """The cursor lookup must be scoped to the rows the caller can list.