Repository navigation
fix(proxy): authorize key model aliases the same way as team aliases - #43049
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…r model scopes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…olves Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…fore authorizing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
| team_id=valid_token.team_id, | ||
| object_type="agent", | ||
| key_model_aliases=key_model_aliases_for_auth_check(valid_token), | ||
| team_model_aliases=valid_token.team_model_aliases, |
There was a problem hiding this comment.
Team aliases bypass agent model ceiling
High Severity
Passing team_model_aliases into the agent access-group ceiling lets _model_in_team_aliases admit any team alias name without requiring its target to be in ceiling.models. An agent capped to one model can then call a different model through a team alias, after which dispatch rewrites the request to that out-of-ceiling target.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit d2d6c65. Configure here.
There was a problem hiding this comment.
Agreed, fixed in 58eb2d4: the team alias is resolved to its target before the ceiling check, so no name admission remains
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
…ed deployment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 86d5c5c. Configure here.


TLDR
Problem this solves:
GET /v1/modelslist a key's ownaliases403 key_model_access_deniedon completionstests/test_keys.py::test_key_model_liststarted failing on main (assert 2 == 1)How it solves it:
model_alias_map, key alias again), so it authorizes the model that will actually be dispatchedtest_key_model_listUser Flow
Before: a developer whose key is limited to
gpt-4o-miniwith aliasmistral-7bsees the alias listed and then gets refused when they use it{"models": ["gpt-4o-mini"], "aliases": {"mistral-7b": "gpt-4o-mini"}}gpt-4o-miniandmistral-7b"model": "mistral-7b"and get403 {"type": "key_model_access_denied"}gpt-4o-mini, withteam_model_access_deniedinsteadAfter: every model the developer sees in the list is one they can call
{"models": ["gpt-4o-mini"], "aliases": {"mistral-7b": "gpt-4o-mini"}}gpt-4o-miniandmistral-7b"model": "mistral-7b"and get200withx-litellm-model-group: gpt-4o-miniRelevant issues
Follow-up to #42908
Linear ticket
Resolves LIT-8515
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Real proxy with Postgres, real OpenAI calls to
gpt-4o-mini, master keysk-1234. Before is main ate64e635185(a few commits behind the merge basefc29fb513c, none of them touch auth or model listing) on port 4000, After is the PR tip86d5c5c9e9on port 4001, both proxies on two uvicorn workersScreen recording of the same six cases (before
e64e635185, aftercf63470939, the same six cases rerun unchanged at86d5c5c9e9) is in this Slack thread: https://berriaillm.slack.com/archives/D0BE7TV8KSQ/p1790280733589729?thread_ts=1790280733.589729&cid=D0BE7TV8KSQShared setup:
Before (e64e635)
Key alias listed by /v1/models
curl -s http://localhost:4000/v1/models -H "Authorization: Bearer $KEY" | jq -c '[.data[].id]'["gpt-4o-mini","mistral-7b"]curl -si http://localhost:4000/v1/chat/completions -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"model":"mistral-7b","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'HTTP/1.1 403 Forbiddenwith body{"error":{"message":"The requested model 'mistral-7b' is not available for this API key, or the model name is invalid. Check the models available to you and try again.","type":"key_model_access_denied","param":"model","code":"403"}}Team key alias listed by /v1/models
curl -s http://localhost:4000/team/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"team_id":"rca-team-1","models":["gpt-4o-mini"]}'then generate a key with{"team_id":"rca-team-1","aliases":{"mistral-7b":"gpt-4o-mini"}}GET /v1/modelslists["gpt-4o-mini","mistral-7b"]POST /v1/chat/completionswith"model":"mistral-7b"returnsHTTP/1.1 403 Forbidden,"type":"team_model_access_denied"Alias to a model outside the allowlist
{"models":["gpt-4o-mini"],"aliases":{"bad-alias":"gpt-4"}}GET /v1/modelslists["gpt-4o-mini"]onlyPOST /v1/chat/completionswith"model":"bad-alias"returnsHTTP/1.1 403 Forbidden,"type":"key_model_access_denied"After (86d5c5c)
Key alias listed by /v1/models
curl -s http://localhost:4001/v1/models -H "Authorization: Bearer $KEY" | jq -c '[.data[].id]'["gpt-4o-mini","mistral-7b"]curl -si http://localhost:4001/v1/chat/completions -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"model":"mistral-7b","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'HTTP/1.1 200 OK,x-litellm-model-name: openai/gpt-4o-mini,x-litellm-model-group: gpt-4o-mini,x-litellm-response-cost: 4.35e-06, body{"id":"chatcmpl-...","model":"gpt-4o-mini","choices":[{"message":{"content":"Pong!","role":"assistant"}}],...}Team key alias listed by /v1/models
GET /v1/modelslists["gpt-4o-mini","mistral-7b"]POST /v1/chat/completionswith"model":"mistral-7b"returnsHTTP/1.1 200 OK,x-litellm-model-group: gpt-4o-miniAlias to a model outside the allowlist
{"models":["gpt-4o-mini"],"aliases":{"bad-alias":"gpt-4"}}GET /v1/modelslists["gpt-4o-mini"]onlyPOST /v1/chat/completionswith"model":"bad-alias"returnsHTTP/1.1 403 Forbidden,"type":"key_model_access_denied"Tests, red on main and green here:
tests/integration/authorization/test_key_alias_model_access.py(both "listed is callable" tests fail on main withlisted id ... is not callable: 403 key_model_access_denied), the new_can_object_call_model/can_team_access_model/can_key_call_model/can_user_call_model/_check_team_member_model_access/_check_agent_access_group_model_accesstests plus the global-vs-key alias precedence tests intests/test_litellm/proxy/auth/test_auth_checks.py, andtests/test_keys.py::test_key_model_list(8 passed against the fix proxy)Type
Bug Fix
Caveats (if any)
Medium
tests/test_keys.py::test_key_model_listcontract updated:/v1/modelsnow expects{gpt-3.5-turbo, mistral-7b}, since that helper always sets the aliasLow
_alias_mapinmodel_listing_utils.pyis now publicalias_map, shared by listing and authGET /v1/models/{id}is admin-only for virtual keys (401), so the retrieval half of fix(proxy): list key and team model aliases in GET /v1/models #42908 remains JWT-only. Out of scope heree64e635185rather than the exact merge base; the intervening commits are cost-map and Vertex batch changesLive PR risk (base
e64e635185on :4000 vs head86d5c5c9e9on :4001)Same eight scenarios, same order, both sides, real Postgres and real OpenAI. Script and full output:
live_pr_risk_43049.shandlive_pr_risk_43049.outin the Slack thread linked above, rerun at86d5c5c9e9with identical results apart from the run id. Verdict: one intended behavior change, no regression on the untested dependents this run could reachBreaking: none
Backward incompatible: a key allowed
gpt-4o-miniwithaliases: {"gpt-4o-mini": "gpt-5.5"}calling"model":"gpt-4o-mini". Base answers200withx-litellm-model-group: gpt-5.5, so the key was served a model outside its allowlist. Head answers403 key_model_access_denied. This is the fix itself (auth now checks the model that will be dispatched), approved by shipping this PR; the Medium caveat above names the same changeRegression risk (reachable, parity observed): key with no aliases (200 both), alias key calling its real target directly (200 both), team-scoped key narrower than its team with an alias to a team-only model (403
key_model_access_deniedboth), team-scoped key inheriting team models with an alias to a model the team lacks (403team_model_access_deniedboth),all-proxy-modelskey with an alias (200 both),GET /v1/model/infoandGET /model_group/infofor an alias key (200, one row, both), hostile alias shapes on/key/generate({"x":5}and a 5 KB target accepted then listing hides them and the call is 403 on both; a list body is 422 on both; an empty alias name is listed as""on both, pre-existing from #42908 and unchanged here)Dependency graph:
_can_object_call_modelcallerscan_key_call_model,can_team_access_model,can_user_call_model,can_project_access_model,_check_team_member_model_access,_key_access_group_grants_model,_check_agent_caller_model_access(unit tested, key and team legs verified live);user_api_key_auth._check_key_model_budget_with_fallback(now passes key aliases, unit tested only);compact.pyAnthropic context-management compaction, which calls_can_object_call_modelwithout key aliases and keeps its pre-PR behavior (untested by this PR);alias_maprename consumers inmodel_listing_utils.py(verified live via/v1/models,/v1/model/info,/model_group/info)Not verified: the compaction path (its call keeps the default
key_model_aliases=None, so its inputs and outputs are byte-for-byte those of base; not driven live) and the model-budget fallback path (needs per-model budgets plus fallbacks configured; covered by unit tests only). Both proxies ran two uvicorn workers for the final sweep; the auth path holds no per-process state beyond the shared key cacheNote
Medium Risk
Changes proxy authorization logic across many auth paths; behavior tightens when key aliases resolve to targets outside the allowlist, but aligns auth with listing and dispatch.
Overview
Fixes a mismatch where
GET /v1/modelscould list a key’saliasesbut completions still returned403, because auth only considered team, global, and router aliases.Auth now builds
key_model_aliasesfrom the token (via sharedalias_map) and passes them through key, team, user, project, team-member, access-group, fallback, and agent-caller checks._can_object_call_modelresolves aliases in the same order as dispatch (team → key → globalmodel_alias_map→ key again) and, when a key alias applies, authorizes the dispatched target rather than treating the alias name alone as sufficient—so aliases to models outside the allowlist stay denied.Adds integration tests for listed-vs-callable behavior and extensive unit coverage;
test_key_model_listnow expects both the allowed model and its alias in/v1/models.Reviewed by Cursor Bugbot for commit b6ee048. Bugbot is set up for automated code reviews on this repo. Configure here.
Final Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
86d5c5c passes /live-pr-risk
Link to Devin session: https://app.devin.ai/sessions/703014cf0915420b8e568f1e517b238d
Open in Devin Desktop: https://app.devin.ai/desktop/session/703014cf0915420b8e568f1e517b238d?variant=devin
Requested by: @mateo-berri