From a2ef0ce56000931dcbb7e4a108edd51bbc31b2cc Mon Sep 17 00:00:00 2001 From: lostmartian Date: Sat, 22 Aug 2026 18:00:03 +0530 Subject: [PATCH] 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 --- litellm/proxy/_types.py | 5 +++ .../proxy/auth/test_route_checks.py | 32 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0840d37ffa1..e0847b22e25 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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", diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index b3b73723726..abde7a4ac04 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -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(