diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index d8318962633..38fcfa52971 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -344,16 +344,18 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints): # This is because the encoded object ids stored in the managed objects table do not contain the provider information # To support provider filtering, we would need to store the provider information in the encoded object ids if provider: - raise Exception( - "Filtering by 'provider' is not supported when using managed batches." + raise HTTPException( + status_code=400, + detail="Filtering by 'provider' is not supported when using managed batches.", ) # Model name filtering is not supported for managed batches # This is because the encoded object ids stored in the managed objects table do not contain the model name # A hash of the model name + litellm_params for the model name is encoded as the model id. This is not sufficient to reliably map the target model names to the model ids. if target_model_names: - raise Exception( - "Filtering by 'target_model_names' is not supported when using managed batches." + raise HTTPException( + status_code=400, + detail="Filtering by 'target_model_names' is not supported when using managed batches.", ) owner_filter = build_owner_filter(user_api_key_dict) diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index f7c332f2849..33d559bf22c 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -682,7 +682,12 @@ async def list_batches( # Try to use managed objects table for listing batches (returns encoded IDs). managed_files_obj: Final = proxy_logging_obj.get_proxy_hook("managed_files") - if managed_files_obj is not None and hasattr(managed_files_obj, "list_user_batches"): + if ( + managed_files_obj is not None + and hasattr(managed_files_obj, "list_user_batches") + and not provider + and not target_model_names + ): verbose_proxy_logger.debug("Using managed objects table for batch listing") response = await cast(Any, managed_files_obj).list_user_batches( user_api_key_dict=user_api_key_dict, diff --git a/tests/e2e/batches/test_batches_e2e.py b/tests/e2e/batches/test_batches_e2e.py index 1376bdbed38..ad7ea8b9e68 100644 --- a/tests/e2e/batches/test_batches_e2e.py +++ b/tests/e2e/batches/test_batches_e2e.py @@ -295,31 +295,10 @@ def test_batch_lifecycle( ) if cap.can_list: - list_result = client.list_batches(key=key, provider=provider) - managed_filter_unsupported = False - match list_result: - case UnknownApiError(body=body) if ( - "Filtering by 'provider' is not supported when using managed batches" in body - ): - managed_filter_unsupported = True - listed = unwrap(client.list_batches(key=key, provider=None)) - case _: - listed = unwrap(list_result) + listed = unwrap(client.list_batches(key=key, provider=provider)) if listed.object is not None: assert listed.object == "list", f"list envelope object={listed.object!r}" match = next((b for b in listed.data if b.id == batch.id), None) - if ( - match is None - and managed_filter_unsupported - and cap.scenario == "provider_fallback" - ): - # provider_fallback keeps the provider's raw batch id (not re-encoded - # into a managed/proxy id). When the gateway rejects provider-scoped - # list, the only available list is the unfiltered managed view, which - # does not index raw provider ids. Membership cannot be asserted here; - # create + retrieve (and raw_id_matches_provider above) already pin - # routing for this scenario. - return assert match is not None, "created batch absent from list" assert match.object == "batch" 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 e1e5cc6c532..b96c971d0c0 100644 --- a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py +++ b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py @@ -2007,15 +2007,16 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex DualCache(), prisma_client=prisma_client ) - # Filtering by provider should raise Exception - with pytest.raises(Exception) as exc_info: + # Filtering by provider should raise HTTPException + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), limit=10, provider="openai", ) - assert str(exc_info.value) == ( + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == ( "Filtering by 'provider' is not supported when using managed batches." ) @@ -2033,15 +2034,16 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_ DualCache(), prisma_client=prisma_client ) - # Filtering by provider should raise Exception - with pytest.raises(Exception) as exc_info: + # Filtering by target_model_names should raise HTTPException + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), limit=10, target_model_names="gpt-5.5,gpt-3.5", ) - assert str(exc_info.value) == ( + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == ( "Filtering by 'target_model_names' is not supported when using managed batches." ) diff --git a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py index e758aa5ca7f..6b26414b545 100644 --- a/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/batches_endpoints/test_endpoints.py @@ -1461,7 +1461,10 @@ async def call_list( # --------------------------------------------------------------------------- # # Branch 1 - ManagedObjectTable listing. This is the default production path -# (the managed_files hook is registered) and wins over every other branch. +# (the managed_files hook is registered) and wins over every other branch, as +# long as the request carries no provider / target_model_names filter: the +# managed objects table cannot satisfy either, so filtered requests fall +# through to the provider seams instead of 500ing out of the hook. # --------------------------------------------------------------------------- # @@ -1476,8 +1479,6 @@ async def test_list__managed_files_path(list_harness): user=user, limit=7, after="batch-cursor", - provider="openai", - target_model_names="m1,m2", ) # DISPATCH - managed-files seam fired, neither provider seam did. @@ -1485,8 +1486,8 @@ async def test_list__managed_files_path(list_harness): user_api_key_dict=user, limit=7, after="batch-cursor", - provider="openai", - target_model_names="m1,m2", + provider=None, + target_model_names=None, llm_router=list_harness.router, ) list_harness.litellm_alist.assert_not_called() @@ -1494,6 +1495,27 @@ async def test_list__managed_files_path(list_harness): assert resp is page +@pytest.mark.asyncio +async def test_list__provider_filter_skips_managed_files(list_harness): + list_user_batches = list_harness.set_managed_files(FakeListPage([])) + + await call_list(list_harness, provider="vertex_ai") + + list_user_batches.assert_not_called() + assert list_harness.alist_kwargs()["custom_llm_provider"] == "vertex_ai" + + +@pytest.mark.asyncio +async def test_list__target_model_names_filter_skips_managed_files(list_harness): + list_user_batches = list_harness.set_managed_files(FakeListPage([])) + + await call_list(list_harness, target_model_names="m1,m2") + + list_user_batches.assert_not_called() + list_harness.litellm_alist.assert_not_called() + assert list_harness.router_kwargs()["model"] == "m1" + + @pytest.mark.asyncio async def test_list__managed_files_beats_model_param(list_harness): """Branch 1 is checked before the model branch: a model in the body does not