From d14a5aedc0b0ae2b77b2ee60caa0040f31f72ecf Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:34:25 -0700 Subject: [PATCH] fix(proxy): list catalog models through routing-prefix wildcard deployments --- litellm/proxy/auth/model_checks.py | 24 +++++++++++++++++++--- tests/unit/proxy/auth/test_model_checks.py | 17 +++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index ef2d6610e2e..686e2f8bd1e 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -1,6 +1,7 @@ # What is this? ## Common checks for /v1/models and `/model/info` import copy +import itertools import re from collections.abc import Sequence from typing import Final @@ -271,16 +272,33 @@ def _hydrate_litellm_credential_name( return litellm_params +def _is_routing_segment(segment: str, provider_model_ids: tuple[str, ...]) -> bool: + return "*" not in segment and not any(model_id.startswith(f"{segment}/") for model_id in provider_model_ids) + + +def _without_routing_segments(model_pattern: str, provider_model_ids: tuple[str, ...]) -> str: + segments: Final = model_pattern.split("/") + routing_segments: Final = tuple( + itertools.takewhile(lambda segment: _is_routing_segment(segment, provider_model_ids), segments[:-1]) + ) + return "/".join(segments[len(routing_segments) :]) + + def _ids_routing_to_catalog_models( name_pattern: str, deployment_pattern: str, provider: str, catalog_ids: Sequence[str] ) -> tuple[str, ...]: if name_pattern.count("*") != deployment_pattern.count("*"): return () - deployment_regex: Final = re.compile(PatternMatchRouter().pattern_to_regex(deployment_pattern)) provider_id_prefix: Final = f"{provider}/" + provider_model_ids: Final = tuple(catalog_id.removeprefix(provider_id_prefix) for catalog_id in catalog_ids) + catalog_pattern: Final = _without_routing_segments( + deployment_pattern.removeprefix(provider_id_prefix), provider_model_ids + ) + deployment_regex: Final = re.compile( + PatternMatchRouter().pattern_to_regex(f"{provider_id_prefix}{catalog_pattern}") + ) catalog_matches: Final = ( - deployment_regex.fullmatch(f"{provider_id_prefix}{catalog_id.removeprefix(provider_id_prefix)}") - for catalog_id in catalog_ids + deployment_regex.fullmatch(f"{provider_id_prefix}{model_id}") for model_id in provider_model_ids ) return tuple( dict.fromkeys( diff --git a/tests/unit/proxy/auth/test_model_checks.py b/tests/unit/proxy/auth/test_model_checks.py index 36c07d62fa2..fd16da69218 100644 --- a/tests/unit/proxy/auth/test_model_checks.py +++ b/tests/unit/proxy/auth/test_model_checks.py @@ -1267,6 +1267,8 @@ _ROUTE_CF_ID: Final = "cloudflare/@cf/partial-route-org/model-a-instruct" _ROUTE_CF_SUFFIXED_ID: Final = "cloudflare/@cf/partial-route-org/model-b-instruct-v2" _ROUTE_HF_ID: Final = "cloudflare/@hf/partial-route-org/model-c" _ROUTE_GEMINI_ID: Final = "gemini-partial-route-9-flash" +_ROUTE_OPENAI_ID: Final = "gpt-partial-route-9-mini" +_ROUTE_BEDROCK_ID: Final = "anthropic.partial-route-9-v1:0" _ROUTE_CF_IDS: Final = {_ROUTE_CF_ID, _ROUTE_CF_SUFFIXED_ID} @@ -1297,6 +1299,19 @@ _ROUTE_CF_IDS: Final = {_ROUTE_CF_ID, _ROUTE_CF_SUFFIXED_ID} pytest.param( "gemini/*", "gemini/*", "gemini/gemini-partial-route-9*", {_ROUTE_GEMINI_ID}, id="key-filter-over-bare-id" ), + pytest.param( + "openai/*", "openai/responses/*", None, {f"openai/responses/{_ROUTE_OPENAI_ID}"}, id="responses-route" + ), + pytest.param( + "openai/*", + "openai/responses/*", + "openai/gpt-partial-route-9*", + {f"openai/responses/{_ROUTE_OPENAI_ID}"}, + id="key-filter-over-responses-route", + ), + pytest.param( + "bedrock/*", "bedrock/converse/*", None, {f"bedrock/converse/{_ROUTE_BEDROCK_ID}"}, id="converse-route" + ), ], ) def test_every_listed_partial_wildcard_id_routes_to_the_catalog_model_it_names( @@ -1331,6 +1346,8 @@ def test_every_listed_partial_wildcard_id_routes_to_the_catalog_model_it_names( _ROUTE_CF_SUFFIXED_ID: "cloudflare", _ROUTE_HF_ID: "cloudflare", _ROUTE_GEMINI_ID: "gemini", + _ROUTE_OPENAI_ID: "openai", + _ROUTE_BEDROCK_ID: "bedrock", } ): v1_models: Final = get_complete_model_list(