What happened
A key created with key_type="llm_api" can call models but cannot read model metadata. handle_key_type sets allowed_routes=["llm_api_routes"] (litellm/proxy/management_endpoints/key_management_endpoints.py:476-477), and LiteLLMRoutes.llm_api_routes (litellm/proxy/_types.py:518-528) covers /models and /v1/models but none of the model-metadata endpoints, which live in info_routes (litellm/proxy/_types.py:529-551):
/model/info, /v1/model/info, /v2/model/info, /model_group/info
RouteChecks.is_virtual_key_allowed_to_call_route therefore returns 403 for all four, so an OpenAI-compatible client that discovers pricing, mode or context-window limits from /model_group/info gets nothing while /v1/chat/completions on the very same key works. The check runs in should_call_route before any role branching (litellm/proxy/auth/user_api_key_auth.py:2536), so this applies to every caller holding such a key, including one whose user row is proxy_admin
This looks like a gap rather than a deliberate restriction: the same key group already reaches /v1/models, and llm_api_routes already carries a method-aware carve-out for the read-only MCP server discovery endpoints (litellm/proxy/auth/route_checks.py:155-166), which is the same shape of problem
Reproduction
Proxy on localhost:4000 with a Postgres backend, one deployment in config.yaml, master_key: sk-e2e-1234
# 1. create a key with the llm_api preset
KEY=$(curl -s -X POST http://127.0.0.1:4000/key/generate \
-H "Authorization: Bearer sk-e2e-1234" -H 'Content-Type: application/json' \
-d '{"key_type":"llm_api","key_alias":"repro"}' | jq -r .key)
# 2. confirm the preset that /key/generate applied
curl -s "http://127.0.0.1:4000/key/info?key=$KEY" -H "Authorization: Bearer sk-e2e-1234" \
| jq '{key_type: .info.key_type, allowed_routes: .info.allowed_routes}'
# 3. metadata endpoints vs /v1/models with the same key
for r in /v1/model/info /model/info /v2/model/info /model_group/info /v1/models; do
printf '%-20s %s\n' "$r" "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $KEY" "http://127.0.0.1:4000$r")"
done
Output:
{"key_type":"llm_api","allowed_routes":["llm_api_routes"]}
/v1/model/info 403
/model/info 403
/v2/model/info 403
/model_group/info 403
/v1/models 200
Response body on any of the four:
{"detail":"Virtual key is not allowed to call this route. Only allowed to call routes: ['llm_api_routes']. Tried to call route: /model_group/info"}
Expected behavior
A key scoped to llm_api_routes should be able to read metadata for the models it is already allowed to call, the same way it can list them through /v1/models
Suggested fix
A GET-only carve-out inside the virtual-key check, mirroring _is_get_mcp_server_discovery_route, keeps the blast radius at the ACL layer:
_MODEL_DISCOVERY_ROUTES = frozenset(
("/model/info", "/v1/model/info", "/v2/model/info", "/model_group/info")
)
@staticmethod
def _is_get_model_discovery_route(route: str, request: Optional[Request]) -> bool:
if request is None or request.method.upper() != "GET":
return False
return route in _MODEL_DISCOVERY_ROUTES
called next to the existing MCP carve-out under if allowed_route == "llm_api_routes":
Worth noting why simply appending the four paths to LiteLLMRoutes.llm_api_routes is a heavier change: RouteChecks.is_llm_api_route membership also drives budget reservation (litellm/proxy/spend_tracking/budget_reservation.py:141), cost tracking (litellm/proxy/hooks/proxy_track_cost_callback.py:74), tag routing and the global-spend gate (litellm/proxy/auth/auth_checks.py:341,359), plus _check_llm_api_route_access (litellm/proxy/auth/route_checks.py:779), none of which should treat a metadata GET as an LLM call. Both /models and /v1/models already need explicit exclusions in two of those call sites for the same reason
The writes on the shared prefix (/model/new, /model/update, /model/delete) are separate paths, so neither variant reaches them, and both /model/info and /model_group/info already scope their response to the caller's model access
One thing worth a maintainer's call: remove_sensitive_info_from_deployment (litellm/proxy/common_utils/openai_endpoint_utils.py:29-47) strips api_key, client_secret, vertex_credentials and the AWS keys, but leaves api_base in the /model/info response. If that is considered infrastructure detail an LLM-API key should not see, the carve-out could cover /model_group/info only, whose response carries no api_base
Related observability gap
should_call_route is called outside the try/except that routes authorization failures through UserAPIKeyAuthExceptionHandler._handle_authentication_error (litellm/proxy/auth/user_api_key_auth.py:2536 vs 2543-2559), so a route denial raises straight to FastAPI and never gets the log line that budget or model-access denials get. In practice a 403 from this path leaves nothing in the proxy log beyond the uvicorn access line, which makes it hard to tell which key was denied. Happy to file that separately if it is worth its own issue
Relevant log output
INFO: 10.245.4.246:33766 - "GET /v2/model/info HTTP/1.1" 403 Forbidden
Are you a ML Ops Team?
No
What LiteLLM version are you on?
v1.94.2
What happened
A key created with
key_type="llm_api"can call models but cannot read model metadata.handle_key_typesetsallowed_routes=["llm_api_routes"](litellm/proxy/management_endpoints/key_management_endpoints.py:476-477), andLiteLLMRoutes.llm_api_routes(litellm/proxy/_types.py:518-528) covers/modelsand/v1/modelsbut none of the model-metadata endpoints, which live ininfo_routes(litellm/proxy/_types.py:529-551):/model/info,/v1/model/info,/v2/model/info,/model_group/infoRouteChecks.is_virtual_key_allowed_to_call_routetherefore returns 403 for all four, so an OpenAI-compatible client that discovers pricing, mode or context-window limits from/model_group/infogets nothing while/v1/chat/completionson the very same key works. The check runs inshould_call_routebefore any role branching (litellm/proxy/auth/user_api_key_auth.py:2536), so this applies to every caller holding such a key, including one whose user row isproxy_adminThis looks like a gap rather than a deliberate restriction: the same key group already reaches
/v1/models, andllm_api_routesalready carries a method-aware carve-out for the read-only MCP server discovery endpoints (litellm/proxy/auth/route_checks.py:155-166), which is the same shape of problemReproduction
Proxy on
localhost:4000with a Postgres backend, one deployment inconfig.yaml,master_key: sk-e2e-1234Output:
Response body on any of the four:
{"detail":"Virtual key is not allowed to call this route. Only allowed to call routes: ['llm_api_routes']. Tried to call route: /model_group/info"}Expected behavior
A key scoped to
llm_api_routesshould be able to read metadata for the models it is already allowed to call, the same way it can list them through/v1/modelsSuggested fix
A GET-only carve-out inside the virtual-key check, mirroring
_is_get_mcp_server_discovery_route, keeps the blast radius at the ACL layer:called next to the existing MCP carve-out under
if allowed_route == "llm_api_routes":Worth noting why simply appending the four paths to
LiteLLMRoutes.llm_api_routesis a heavier change:RouteChecks.is_llm_api_routemembership also drives budget reservation (litellm/proxy/spend_tracking/budget_reservation.py:141), cost tracking (litellm/proxy/hooks/proxy_track_cost_callback.py:74), tag routing and the global-spend gate (litellm/proxy/auth/auth_checks.py:341,359), plus_check_llm_api_route_access(litellm/proxy/auth/route_checks.py:779), none of which should treat a metadata GET as an LLM call. Both/modelsand/v1/modelsalready need explicit exclusions in two of those call sites for the same reasonThe writes on the shared prefix (
/model/new,/model/update,/model/delete) are separate paths, so neither variant reaches them, and both/model/infoand/model_group/infoalready scope their response to the caller's model accessOne thing worth a maintainer's call:
remove_sensitive_info_from_deployment(litellm/proxy/common_utils/openai_endpoint_utils.py:29-47) stripsapi_key,client_secret,vertex_credentialsand the AWS keys, but leavesapi_basein the/model/inforesponse. If that is considered infrastructure detail an LLM-API key should not see, the carve-out could cover/model_group/infoonly, whose response carries noapi_baseRelated observability gap
should_call_routeis called outside thetry/exceptthat routes authorization failures throughUserAPIKeyAuthExceptionHandler._handle_authentication_error(litellm/proxy/auth/user_api_key_auth.py:2536vs2543-2559), so a route denial raises straight to FastAPI and never gets the log line that budget or model-access denials get. In practice a 403 from this path leaves nothing in the proxy log beyond the uvicorn access line, which makes it hard to tell which key was denied. Happy to file that separately if it is worth its own issueRelevant log output
Are you a ML Ops Team?
No
What LiteLLM version are you on?
v1.94.2