Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
409932d
fix(proxy): expand partial wildcard prefixes against provider-strippe…
devin-ai-integration[bot] Sep 28, 2026
7dec2cb
fix(health): splice wildcard prefix onto provider-stripped health che…
devin-ai-integration[bot] Sep 28, 2026
409eb78
fix(health): use provider-stripped wildcard suffix at the ahealth_che…
devin-ai-integration[bot] Sep 29, 2026
1ecff12
test(e2e): spawn wildcard repro proxy with isolated interpreter flag
devin-ai-integration[bot] Sep 29, 2026
d945e2e
fix(health): filter wildcard health probes to ids matching the litera…
devin-ai-integration[bot] Sep 29, 2026
d15980d
test(integration): move partial wildcard expansion repro onto the sha…
devin-ai-integration[bot] Sep 29, 2026
b258196
test(proxy): cover full-map wildcard prefix expansion
gabriele-berri Oct 15, 2026
e24584d
fix(proxy): list partial wildcard ids through the deployment's own wi…
mateo-berri Oct 16, 2026
5041e7e
fix(proxy): list wildcard ids only for routes that rename the namespace
mateo-berri Oct 16, 2026
a32b99b
fix(proxy): bound wildcard matching and list /model/info ids by the s…
mateo-berri Oct 16, 2026
07c1694
fix(proxy): list each wildcard id that routes to a known model, throu…
mateo-berri Oct 16, 2026
1354c51
fix(proxy): list ids through every route-type infix, fixed targets, a…
mateo-berri Oct 16, 2026
3c83326
test(proxy): cover wildcard listing routes, row precedence, health pr…
mateo-berri Oct 16, 2026
668fa29
fix(proxy): keep wildcard ids a narrower route sends to the same mode…
mateo-berri Oct 16, 2026
557f368
test(proxy): cover different-model narrower routes in key listing and…
mateo-berri Oct 16, 2026
fa613b1
fix(proxy): keep wildcard ids a narrower route sends to the same mode…
mateo-berri Oct 16, 2026
29887b5
test(proxy): cover same-model narrower routes by infix, provider and …
mateo-berri Oct 16, 2026
be0d53d
test(proxy): cover a provider-scoped key listing beside a catch-all w…
mateo-berri Oct 16, 2026
743790b
fix(proxy): list ids a narrower route serves for keys scoped to a wid…
mateo-berri Oct 16, 2026
ef70f84
Merge remote-tracking branch 'origin/main' into litellm_partial_wildc…
mateo-berri Oct 16, 2026
bb2b5b1
fix(proxy): list bare-model wildcard ids under their provider and ski…
mateo-berri Oct 17, 2026
67488f8
fix(proxy): skip ids a narrower route sends to another model for keys…
mateo-berri Oct 17, 2026
bc4d050
fix(proxy): keep exact deployments and same-provider team ids in wild…
mateo-berri Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions litellm/litellm_core_utils/get_llm_provider_logic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import re
from fnmatch import fnmatchcase
from types import MappingProxyType
from typing import Final, cast
from urllib.parse import urlparse

Expand All @@ -14,6 +17,27 @@
from litellm.secret_managers.main import get_secret, get_secret_str

from ..types.router import GenericLiteLLMParams, LiteLLM_Params
from ..types.utils import LlmProviders

_KNOWN_PROVIDERS: Final = frozenset(provider.value for provider in LlmProviders)
_GLOB_CHARS_OTHER_THAN_STAR: Final = re.compile(r"[\[?]")
_ROUTE_TYPE_PREFIXES_BY_PROVIDER: Final = MappingProxyType(
{
"azure": ("o_series/", "gpt5_series/"),
"bedrock": (
"converse/",
"invoke/",
"claude_platform/",
"converse_like/",
"async_invoke/",
"openai/",
"mantle/",
"chat_completions/",
"nova/",
"nova-2/",
),
}
)


def _endpoint_matches_api_base(endpoint: str, api_base: str) -> bool:
Expand Down Expand Up @@ -146,6 +170,26 @@ def declared_authenticating_provider(model: str | None, custom_llm_provider: str
return declared if declared in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO else None


def strip_provider_family_prefix(model: str, custom_llm_provider: str) -> str:
leading, sep, model_id = model.partition("/")
if not sep:
return model
is_provider_family: Final = leading == custom_llm_provider or (
leading in _KNOWN_PROVIDERS and custom_llm_provider.startswith(f"{leading}_")
)
return model_id if is_provider_family else model


def model_matches_wildcard(model_id: str, wildcard: str) -> bool:
return fnmatchcase(model_id, _GLOB_CHARS_OTHER_THAN_STAR.sub(r"[\g<0>]", wildcard))


def split_route_type_prefix(model_path: str, custom_llm_provider: str) -> tuple[str, str]:
prefixes: Final = ("responses/", *_ROUTE_TYPE_PREFIXES_BY_PROVIDER.get(custom_llm_provider, ()))
prefix: Final = next((prefix for prefix in prefixes if model_path.startswith(prefix)), "")
return prefix, model_path.removeprefix(prefix)


def inferred_provider(model: str | None) -> str | None:
if not model:
return None
Expand Down
38 changes: 30 additions & 8 deletions litellm/litellm_core_utils/health_check_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
"""

import base64
from collections.abc import Awaitable, Callable, Mapping
import sys
from collections.abc import Awaitable, Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Final, Literal

from litellm.litellm_core_utils.get_llm_provider_logic import (
model_matches_wildcard,
split_route_type_prefix,
strip_provider_family_prefix,
)
from litellm.llms.base_llm.ocr.transformation import DocumentType
from litellm.rust_bridge import runtime
from litellm.rust_bridge.ocr.entrypoints import NATIVE_OCR_HEALTH_CHECK_DOCUMENT
Expand Down Expand Up @@ -80,6 +86,21 @@ def _ocr_health_check_document(model: str, custom_llm_provider: str) -> Document
return native(model, custom_llm_provider)


def _wildcard_health_check_models(
wildcard_suffix: str, custom_llm_provider: str, candidate_models: Sequence[str]
) -> tuple[str, ...]:
route_type_prefix, model_pattern = split_route_type_prefix(wildcard_suffix, custom_llm_provider)
model_ids: Final = dict.fromkeys(
strip_provider_family_prefix(model, custom_llm_provider) for model in candidate_models
)
matching: Final = tuple(
f"{custom_llm_provider}/{route_type_prefix}{model_id}"
for model_id in model_ids
if model_matches_wildcard(model_id, model_pattern)
)
return matching[:3] or tuple(dict.fromkeys(candidate_models))[:3]


class HealthCheckHelpers:
@staticmethod
async def ahealth_check_wildcard_models(
Expand All @@ -93,17 +114,18 @@ async def ahealth_check_wildcard_models(
pick_cheapest_chat_models_from_llm_provider,
)

# this is a wildcard model, we need to pick a random model from the provider
cheapest_models = pick_cheapest_chat_models_from_llm_provider(custom_llm_provider=custom_llm_provider, n=3)
cheapest_models = pick_cheapest_chat_models_from_llm_provider(
custom_llm_provider=custom_llm_provider, n=sys.maxsize
)
if len(cheapest_models) == 0:
raise Exception(
f"Unable to health check wildcard model for provider {custom_llm_provider}. Add a model on your config.yaml or contribute here - https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json"
)
if len(cheapest_models) > 1:
fallback_models = cheapest_models[1:] # Pick the last 2 models from the shuffled list
else:
fallback_models = None
model_params["model"] = cheapest_models[0]
candidates: Final = _wildcard_health_check_models(
wildcard_suffix=model, custom_llm_provider=custom_llm_provider, candidate_models=cheapest_models
)
fallback_models: Final = list(candidates[1:]) or None
model_params["model"] = candidates[0]
model_params["litellm_logging_obj"] = litellm_logging_obj
model_params["fallbacks"] = fallback_models
model_params["max_tokens"] = model_params.get("max_tokens", 16) # GPT-5 models require max_output_tokens >= 16
Expand Down
Loading
Loading