mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
fix(proxy): keep routed models in wildcard model info, skip key-only OpenRouter catalogs, cache failed listings briefly
This commit is contained in:
parent
1d5caf0b1f
commit
99f6fa1364
5 changed files with 74 additions and 4 deletions
|
|
@ -30,6 +30,8 @@ _CREDENTIAL_LITELLM_PARAM_FIELDS = set(CredentialLiteLLMParams.model_fields)
|
|||
|
||||
_MODEL_LISTING_POOL: Final = ThreadPoolExecutor(thread_name_prefix="litellm-model-listing")
|
||||
|
||||
_PROVIDERS_LISTED_ONLY_THROUGH_A_DEPLOYMENT: Final = frozenset({LlmProviders.OPENROUTER.value})
|
||||
|
||||
_Listed: Final = TypeVar("_Listed")
|
||||
|
||||
|
||||
|
|
@ -495,6 +497,20 @@ def get_known_models_from_wildcard(
|
|||
return list(dict.fromkeys(suffix_appended_wildcard_models))
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=256)
|
||||
def _wildcard_name_regex(name_pattern: str) -> re.Pattern[str]:
|
||||
return re.compile(PatternMatchRouter().pattern_to_regex(name_pattern))
|
||||
|
||||
|
||||
def _routed_deployment_model(name_pattern: str, listed_name: str, deployment_model: str) -> str:
|
||||
name_match: Final = _wildcard_name_regex(name_pattern).match(listed_name)
|
||||
if name_match is None:
|
||||
return listed_name
|
||||
return PatternMatchRouter.set_deployment_model_name(
|
||||
matched_pattern=name_match, litellm_deployment_litellm_model=deployment_model
|
||||
)
|
||||
|
||||
|
||||
def expand_wildcard_deployments_for_model_info(
|
||||
deployments: list[dict[str, object]],
|
||||
llm_router: Router | None = None,
|
||||
|
|
@ -546,12 +562,18 @@ def expand_wildcard_deployments_for_model_info(
|
|||
row["model_name"] = name
|
||||
params = row.get("litellm_params")
|
||||
if isinstance(params, dict):
|
||||
params["model"] = name
|
||||
params["model"] = _routed_deployment_model(model_name, name, litellm_model)
|
||||
expanded.append(row)
|
||||
|
||||
return expanded
|
||||
|
||||
|
||||
def _known_models_without_a_deployment(wildcard_model: str) -> Sequence[str]:
|
||||
if wildcard_model.partition("/")[0] in _PROVIDERS_LISTED_ONLY_THROUGH_A_DEPLOYMENT:
|
||||
return ()
|
||||
return get_known_models_from_wildcard(wildcard_model=wildcard_model, litellm_params=None)
|
||||
|
||||
|
||||
def _get_wildcard_models(
|
||||
unique_models: list[str],
|
||||
return_wildcard_routes: bool | None = False,
|
||||
|
|
@ -582,7 +604,7 @@ def _get_wildcard_models(
|
|||
)
|
||||
)
|
||||
else:
|
||||
all_wildcard_models.extend(get_known_models_from_wildcard(wildcard_model=model, litellm_params=None))
|
||||
all_wildcard_models.extend(_known_models_without_a_deployment(model))
|
||||
|
||||
for model in models_to_remove:
|
||||
unique_models.remove(model)
|
||||
|
|
|
|||
|
|
@ -8386,15 +8386,18 @@ class AvailableModelsCache(InMemoryCache):
|
|||
custom_llm_provider: str,
|
||||
litellm_params: LiteLLM_Params | None,
|
||||
available_models: list[str],
|
||||
ttl: float | None = None,
|
||||
):
|
||||
"""Set cached model info"""
|
||||
cache_key: Final = self._get_cache_key(custom_llm_provider, litellm_params)
|
||||
self.set_cache(cache_key, copy.deepcopy(available_models))
|
||||
self.set_cache(cache_key, copy.deepcopy(available_models), ttl=ttl)
|
||||
|
||||
|
||||
# Global cache instance
|
||||
_model_cache: Final = AvailableModelsCache()
|
||||
|
||||
_FAILED_PROVIDER_LISTING_TTL_SECONDS: Final = 30
|
||||
|
||||
|
||||
def _infer_valid_provider_from_env_vars(
|
||||
custom_llm_provider: str | None = None,
|
||||
|
|
@ -8455,6 +8458,9 @@ def _get_valid_models_from_provider_api(
|
|||
return models
|
||||
except Exception as e:
|
||||
verbose_logger.warning("Error getting valid models: %s", e)
|
||||
_model_cache.set_cached_model_info(
|
||||
custom_llm_provider, litellm_params, [], ttl=_FAILED_PROVIDER_LISTING_TTL_SECONDS
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1185,6 +1185,35 @@ def test_openrouter_wildcard_lists_the_live_catalog_with_each_vendor_namespace(
|
|||
)
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_key_granted_openrouter_wildcard_without_an_openrouter_deployment_lists_no_environment_catalog(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import litellm
|
||||
from litellm import Router
|
||||
from litellm.proxy.auth.model_checks import get_complete_model_list
|
||||
|
||||
monkeypatch.setattr(litellm, "check_provider_endpoint", True)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-environment-only")
|
||||
catalog: Final = respx.get("https://openrouter.ai/api/v1/models/user").mock(
|
||||
return_value=httpx.Response(200, json={"data": [{"id": "anthropic/claude-haiku-5-5"}]})
|
||||
)
|
||||
router: Final = Router(
|
||||
model_list=[{"model_name": "or-team/*", "litellm_params": {"model": "openrouter/*", "api_key": "sk-or-team"}}]
|
||||
)
|
||||
|
||||
listed: Final = get_complete_model_list(
|
||||
key_models=["openrouter/*"],
|
||||
team_models=[],
|
||||
proxy_model_list=router.model_names,
|
||||
user_model=None,
|
||||
infer_model_from_keys=False,
|
||||
llm_router=router,
|
||||
)
|
||||
|
||||
assert (listed, catalog.call_count) == ([], 0)
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_custom_prefix_that_starts_a_discovered_id_still_prefixes_every_listed_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ async def test_v1_model_info_star_wildcard_filter_keeps_provider_expansion(monke
|
|||
allowed_model_names=allowed_model_names,
|
||||
)
|
||||
|
||||
assert [model["model_name"] for model in result] == ["gpt-4o"]
|
||||
assert [(model["model_name"], model["litellm_params"]["model"]) for model in result] == [("gpt-4o", "openai/gpt-4o")]
|
||||
assert router.wildcard_routed_models("gpt-4o", None) == frozenset({"openai/gpt-4o"})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8895,6 +8895,19 @@ def reset_mock_cache() -> None:
|
|||
_model_cache.flush_cache()
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_valid_models_does_not_ask_a_failing_provider_again_on_the_next_listing(reset_mock_cache: None) -> None:
|
||||
catalog: Final = respx.get("https://openrouter.ai/api/v1/models/user").mock(return_value=httpx.Response(503))
|
||||
deployment: Final = LiteLLM_Params(model="openrouter/*", api_key="sk-or-during-an-outage")
|
||||
|
||||
listings: Final = tuple(
|
||||
get_valid_models(check_provider_endpoint=True, custom_llm_provider="openrouter", litellm_params=deployment)
|
||||
for _ in range(2)
|
||||
)
|
||||
|
||||
assert (listings, catalog.call_count) == (([], []), 1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_openai_model_sets(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue