fix(batches): let provider-filtered list requests bypass managed-files listing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-08 03:24:47 +00:00
parent f05d468769
commit f25f4a68ad
5 changed files with 48 additions and 38 deletions

View file

@ -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)

View file

@ -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,

View file

@ -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"

View file

@ -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."
)

View file

@ -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