fix(proxy): let llm_api keys read /model_group/info

Keys created with key_type=llm_api get allowed_routes=["llm_api_routes"],
which covered /v1/models and /model/info but not /model_group/info, so an
OpenAI-compatible client could list models yet got a 403 when reading the
group-level pricing, mode and context-window metadata it needs at request
time (BerriAI/litellm#37810).

Adds model_group_info_routes as a new LiteLLMRoutes member included in
llm_api_routes only. As with /model/info, membership there is not the same
as RouteChecks.is_llm_api_route(), so budget reservation, cost tracking and
DISABLE_LLM_API_ENDPOINTS are unaffected; common_checks already treats the
path as spend-free via MODEL_DISCOVERY_ROUTES. The handler scopes its
response to the caller's model access and returns no api_base or
credentials. /v2/model/info stays out: it is the paginated Admin UI
deployment listing, per the scope set in 7d6ee2a9c
This commit is contained in:
lostmartian 2026-08-22 18:00:03 +05:30
parent b9bff0998c
commit a2ef0ce560
2 changed files with 32 additions and 5 deletions

View file

@ -567,6 +567,10 @@ class LiteLLMRoutes(enum.Enum):
"/v1/model/info",
]
model_group_info_routes = [
"/model_group/info",
]
llm_api_routes = (
openai_routes
+ anthropic_routes
@ -578,6 +582,7 @@ class LiteLLMRoutes(enum.Enum):
+ litellm_native_routes
+ list(agent_inference_routes)
+ model_info_routes
+ model_group_info_routes
)
info_routes = [
"/key/info",

View file

@ -527,9 +527,31 @@ def test_virtual_key_llm_api_routes_allows_model_info(route):
assert result is True
@pytest.mark.parametrize("route", ["/model/info", "/v1/model/info"])
@pytest.mark.parametrize("route", ["/model_group/info"])
def test_virtual_key_llm_api_routes_allows_model_group_info(route):
"""AI API virtual keys must be able to read group-level model metadata
(pricing, mode, context window) for the groups they can already call.
The handler scopes its response to the caller's model access and returns
no api_base or credentials.
"""
valid_token = UserAPIKeyAuth(
user_id="test_user",
allowed_routes=["llm_api_routes"],
)
result = RouteChecks.is_virtual_key_allowed_to_call_route(
route=route,
valid_token=valid_token,
request=_mock_request("GET"),
)
assert result is True
@pytest.mark.parametrize("route", ["/model/info", "/v1/model/info", "/model_group/info"])
def test_model_info_not_classified_as_llm_api(route):
"""Membership in `llm_api_routes` must not promote /model/info to an
"""Membership in `llm_api_routes` must not promote model-metadata reads to an
`is_llm_api_route()`. That predicate gates DISABLE_LLM_API_ENDPOINTS,
global/virtual-key budget enforcement, enforce_user_param and the JWT
x-litellm-team-id attachment; model metadata is a free read and must stay
@ -539,10 +561,10 @@ def test_model_info_not_classified_as_llm_api(route):
assert RouteChecks.is_llm_api_route(route=route) is False
@pytest.mark.parametrize("route", ["/v2/model/info", "/model_group/info"])
@pytest.mark.parametrize("route", ["/v2/model/info"])
def test_virtual_key_llm_api_routes_denies_other_model_info_routes(route):
"""The grant is scoped to the two /model/info paths. The paginated Admin UI
listing and the model-group endpoint stay outside it.
"""The grant covers /model_group/info (group-level metadata scoped to the
caller's model access) but not the paginated Admin UI deployment listing.
"""
valid_token = UserAPIKeyAuth(