diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 263fec77d12..1cad9043b2e 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -187,7 +187,8 @@ def _is_model_cost_zero(model: Optional[Union[str, List[str]]], llm_router: Opti continue try: # Use router's get_model_group_info method directly for better reliability - model_group_info = llm_router.get_model_group_info(model_group=model_name) + resolved_name = _resolve_model_group_alias(model_name, llm_router) + model_group_info = llm_router.get_model_group_info(model_group=resolved_name) if model_group_info is None: # Model not found or no pricing info available @@ -224,7 +225,7 @@ def _is_model_cost_zero(model: Optional[Union[str, List[str]]], llm_router: Opti # not from defaulted sparse auto-registration entries. # See: https://github.com/BerriAI/litellm/issues/24770 safe_name = str(model_name).replace("\n", "").replace("\r", "") - if not _is_cost_explicitly_configured(model_name, llm_router): + if not _is_cost_explicitly_configured(resolved_name, llm_router): verbose_proxy_logger.debug( "Model %s has zero cost but no explicit cost " "configuration in model_cost entry — treating as unknown " @@ -253,6 +254,20 @@ def _is_model_cost_zero(model: Optional[Union[str, List[str]]], llm_router: Opti return True +def _resolve_model_group_alias(model: str, llm_router: "Router") -> str: + """ + Map a user-facing ``model_group_alias`` name onto the model group it points at. + + Returns ``model`` unchanged when it isn't an alias. + """ + item = llm_router.model_group_alias.get(model) + if isinstance(item, str): + return item + if isinstance(item, dict): + return item.get("model", model) + return model + + def _is_cost_explicitly_configured(model: str, llm_router: "Router") -> bool: """ Check if any deployment in the model group has cost fields explicitly diff --git a/tests/test_litellm/proxy/auth/test_unmapped_model_budget_enforcement.py b/tests/test_litellm/proxy/auth/test_unmapped_model_budget_enforcement.py index d7e32cf1c16..dadaa6768ac 100644 --- a/tests/test_litellm/proxy/auth/test_unmapped_model_budget_enforcement.py +++ b/tests/test_litellm/proxy/auth/test_unmapped_model_budget_enforcement.py @@ -162,6 +162,49 @@ class TestUnmappedModelBudgetEnforcement: # Subsequent call sees the new pricing and enforces budget. assert _is_model_cost_zero(model="ramping-model", llm_router=router) is False + def test_free_model_behind_model_group_alias_bypasses_budget(self): + """ + Regression test for https://github.com/BerriAI/litellm/issues/35369: + an alias pointing at an explicitly free model group must bypass budget + checks exactly like the underlying model group does. + """ + router = Router( + model_list=[ + { + "model_name": "free-model", + "litellm_params": { + "model": "openai/nonexistent-but-free-model", + "api_key": "sk-fake", + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + }, + }, + ], + model_group_alias={ + "free-alias": "free-model", + "free-alias-hidden": {"model": "free-model", "hidden": True}, + }, + ) + assert _is_model_cost_zero(model="free-model", llm_router=router) is True + assert _is_model_cost_zero(model="free-alias", llm_router=router) is True + assert _is_model_cost_zero(model="free-alias-hidden", llm_router=router) is True + + def test_paid_model_behind_model_group_alias_enforces_budget(self): + """An alias pointing at a paid model group must still enforce budget.""" + router = Router( + model_list=[ + { + "model_name": "paid-model", + "litellm_params": { + "model": "openai/gpt-4o-mini", + "api_key": "sk-fake", + }, + }, + ], + model_group_alias={"paid-alias": "paid-model"}, + ) + assert _is_model_cost_zero(model="paid-alias", llm_router=router) is False + def test_handles_router_without_zero_cost_cache_attribute(self): """Tolerate router-like objects (e.g. ``MagicMock`` stand-ins) that do not expose ``_zero_cost_cache`` — the auth check must still