fix(proxy): keep the flat DB where shape for single-condition searches

The upstream CI test
test_apply_search_filter_honours_exact_model_name_in_db_query pins the
Prisma where shape: exact model_name stays a top-level key, and plain
substring search keeps {model_name: {contains, mode}} at the top level.
Only genuinely multi-condition queries (status filter and/or router-id
exclusion) now wrap in AND.
This commit is contained in:
thementalcoding 2026-09-29 14:53:57 +02:00
parent 13d76ca1fb
commit 6fc7f08b6e
3 changed files with 27 additions and 11 deletions

View file

@ -14467,11 +14467,19 @@ async def _fetch_db_models_for_search(
filter for `team_public_model_name` instead and keep the DB cost
bounded by `search`.
"""
model_name_condition: Final[dict[str, Any]] = (
{"model_name": {"contains": search_lower, "mode": "insensitive"}}
if model_name is None
else {"model_name": model_name}
)
match_conditions: list[dict[str, Any]] = (
[
{
"OR": [
{"model_name": {"contains": search_lower, "mode": "insensitive"}},
model_name_condition,
# Substring search also matches the underlying LiteLLM model
# name (e.g. "openrouter/deepseek/deepseek-chat"), so users
# can find deployments by provider or upstream model id.
# JSON string_contains is case-sensitive on Postgres (see
# note above); router-side matching below covers the
# case-insensitive path for rows already in the router.
@ -14480,7 +14488,7 @@ async def _fetch_db_models_for_search(
}
]
if model_name is None
else [{"model_name": model_name}]
else [model_name_condition]
)
# Status filter runs inside the DB query too: the fetch is capped, so
# matches of the other status must not consume the page budget.
@ -14488,7 +14496,12 @@ async def _fetch_db_models_for_search(
match_conditions.append({"model_info": {"path": ["blocked"], "equals": blocked}})
if db_model_ids_in_router:
match_conditions.append({"model_id": {"not": {"in": list(db_model_ids_in_router)}}})
db_where_condition: Final[dict[str, Any]] = {"AND": match_conditions}
# Keep the single-condition shape flat: it is what existing callers (and
# tests) assert, and Prisma treats both forms identically.
if len(match_conditions) == 1:
db_where_condition: Final[dict[str, Any]] = match_conditions[0]
else:
db_where_condition = {"AND": match_conditions}
# Unsorted searches only need enough DB rows to fill the current
# page after counting router-side matches. Sorted searches need

View file

@ -2594,7 +2594,10 @@ async def test_apply_search_filter_honours_exact_model_name_in_db_query():
proxy_config=proxy_config,
)
where = prisma_client.db.litellm_proxymodeltable.count.call_args.kwargs["where"]
assert where["model_name"] == {"contains": "sonnet", "mode": "insensitive"}
assert where["OR"] == [
{"model_name": {"contains": "sonnet", "mode": "insensitive"}},
{"litellm_params": {"path": ["model"], "string_contains": "sonnet"}},
]
@pytest.mark.asyncio

View file

@ -312,13 +312,13 @@ export function AllModelsTable({
] ?? ROUTING_STATUS_FILTER_VALUES.all}
</SelectTrigger>
<SelectContent>
{(Object.keys(ROUTING_STATUS_FILTER_VALUES) as Array<keyof typeof ROUTING_STATUS_FILTER_VALUES>).map(
(value) => (
<SelectItem key={value} value={value}>
{ROUTING_STATUS_FILTER_VALUES[value]}
</SelectItem>
),
)}
{(
Object.keys(ROUTING_STATUS_FILTER_VALUES) as Array<keyof typeof ROUTING_STATUS_FILTER_VALUES>
).map((value) => (
<SelectItem key={value} value={value}>
{ROUTING_STATUS_FILTER_VALUES[value]}
</SelectItem>
))}
</SelectContent>
</Select>
</DataTableFilterField>