Skip to content

fix(proxy): authorize key model aliases the same way as team aliases - #43049

Merged
mateo-berri merged 10 commits into
mainfrom
litellm_key_alias_model_access
Sep 25, 2026
Merged

mateo-berri merged 10 commits into
mainfrom
litellm_key_alias_model_access

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • fix(proxy): list key and team model aliases in GET /v1/models #42908 made GET /v1/models list a key's own aliases
  • Auth never honored key aliases, only team, global and router ones
  • So a listed key alias returned 403 key_model_access_denied on completions
  • Existing tests/test_keys.py::test_key_model_list started failing on main (assert 2 == 1)

How it solves it:

  • Auth resolves the key alias to its target and checks the target
  • Applies on the key check and on the team check the same key goes through
  • Alias to a model outside the allowlist stays hidden and denied
  • Agent keys acting for an echoed caller team pass their key aliases to the caller team check as well, matching the member and user legs
  • The agent access group ceiling resolves the team alias to its target, then applies the key alias, so it checks the model that will be dispatched and never admits an alias by name; a team alias to a deleted team deployment is skipped, as the request pipeline skips it
  • Auth resolves aliases in the same order the request pipeline does (team alias, key alias, global model_alias_map, key alias again), so it authorizes the model that will actually be dispatched
  • Key aliases also reach the user, team member, access group, project and agent checks that a key request flows through
  • When a key alias resolves, auth checks only the target the proxy will dispatch, so a key allowed only the alias name (or a chained global alias) cannot reach a target outside its allowlist
  • Regression tests: unit, live-proxy integration, and the updated test_key_model_list

User Flow

Before: a developer whose key is limited to gpt-4o-mini with alias mistral-7b sees the alias listed and then gets refused when they use it

  1. Admin creates their key with POST https://litellm-domain/key/generate {"models": ["gpt-4o-mini"], "aliases": {"mistral-7b": "gpt-4o-mini"}}
  2. They send GET https://litellm-domain/v1/models and see both gpt-4o-mini and mistral-7b
  3. They send POST https://litellm-domain/v1/chat/completions with "model": "mistral-7b" and get 403 {"type": "key_model_access_denied"}
  4. The same happens on a team key whose team allows gpt-4o-mini, with team_model_access_denied instead

After: every model the developer sees in the list is one they can call

  1. Admin creates their key with POST https://litellm-domain/key/generate {"models": ["gpt-4o-mini"], "aliases": {"mistral-7b": "gpt-4o-mini"}}
  2. They send GET https://litellm-domain/v1/models and see both gpt-4o-mini and mistral-7b
  3. They send POST https://litellm-domain/v1/chat/completions with "model": "mistral-7b" and get 200 with x-litellm-model-group: gpt-4o-mini
  4. A key alias pointing at a model outside the key's allowlist is still absent from the list and still returns 403 when called

Relevant 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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Real proxy with Postgres, real OpenAI calls to gpt-4o-mini, master key sk-1234. Before is main at e64e635185 (a few commits behind the merge base fc29fb513c, none of them touch auth or model listing) on port 4000, After is the PR tip 86d5c5c9e9 on port 4001, both proxies on two uvicorn workers

Screen recording of the same six cases (before e64e635185, after cf63470939, the same six cases rerun unchanged at 86d5c5c9e9) is in this Slack thread: https://berriaillm.slack.com/archives/D0BE7TV8KSQ/p1790280733589729?thread_ts=1790280733.589729&cid=D0BE7TV8KSQ

Shared setup:

KEY=$(curl -s http://localhost:$PORT/key/generate -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"models":["gpt-4o-mini"],"aliases":{"mistral-7b":"gpt-4o-mini"}}' | jq -r .key)

Before (e64e635)

Key alias listed by /v1/models

  1. curl -s http://localhost:4000/v1/models -H "Authorization: Bearer $KEY" | jq -c '[.data[].id]'
  2. ["gpt-4o-mini","mistral-7b"]
  3. 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}'
  4. HTTP/1.1 403 Forbidden with 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

  1. 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"}}
  2. GET /v1/models lists ["gpt-4o-mini","mistral-7b"]
  3. POST /v1/chat/completions with "model":"mistral-7b" returns HTTP/1.1 403 Forbidden, "type":"team_model_access_denied"

Alias to a model outside the allowlist

  1. Generate a key with {"models":["gpt-4o-mini"],"aliases":{"bad-alias":"gpt-4"}}
  2. GET /v1/models lists ["gpt-4o-mini"] only
  3. POST /v1/chat/completions with "model":"bad-alias" returns HTTP/1.1 403 Forbidden, "type":"key_model_access_denied"

After (86d5c5c)

Key alias listed by /v1/models

  1. curl -s http://localhost:4001/v1/models -H "Authorization: Bearer $KEY" | jq -c '[.data[].id]'
  2. ["gpt-4o-mini","mistral-7b"]
  3. 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}'
  4. 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

  1. Same team and team key as Before, against port 4001
  2. GET /v1/models lists ["gpt-4o-mini","mistral-7b"]
  3. POST /v1/chat/completions with "model":"mistral-7b" returns HTTP/1.1 200 OK, x-litellm-model-group: gpt-4o-mini

Alias to a model outside the allowlist

  1. Generate a key with {"models":["gpt-4o-mini"],"aliases":{"bad-alias":"gpt-4"}}
  2. GET /v1/models lists ["gpt-4o-mini"] only
  3. POST /v1/chat/completions with "model":"bad-alias" returns HTTP/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 with listed 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_access tests plus the global-vs-key alias precedence tests in tests/test_litellm/proxy/auth/test_auth_checks.py, and tests/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_list contract updated: /v1/models now expects {gpt-3.5-turbo, mistral-7b}, since that helper always sets the alias
  • Org-level model checks do not receive key aliases (no key context reaches them); every check a virtual key request passes through does
  • When a key alias fires, auth checks only the dispatched target, so a key allowed just the alias name (with the target outside its allowlist) is now denied where it used to pass and then dispatch the disallowed target
  • The team alias step in auth is the plain rename; the runtime skip for stale aliases that point at deleted team-scoped deployments is not mirrored

Low

  • _alias_map in model_listing_utils.py is now public alias_map, shared by listing and auth
  • GET /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 here
  • Before leg captured on e64e635185 rather than the exact merge base; the intervening commits are cost-map and Vertex batch changes

Live PR risk (base e64e635185 on :4000 vs head 86d5c5c9e9 on :4001)

Same eight scenarios, same order, both sides, real Postgres and real OpenAI. Script and full output: live_pr_risk_43049.sh and live_pr_risk_43049.out in the Slack thread linked above, rerun at 86d5c5c9e9 with identical results apart from the run id. Verdict: one intended behavior change, no regression on the untested dependents this run could reach

Breaking: none

Backward incompatible: a key allowed gpt-4o-mini with aliases: {"gpt-4o-mini": "gpt-5.5"} calling "model":"gpt-4o-mini". Base answers 200 with x-litellm-model-group: gpt-5.5, so the key was served a model outside its allowlist. Head answers 403 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 change

Regression 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_denied both), team-scoped key inheriting team models with an alias to a model the team lacks (403 team_model_access_denied both), all-proxy-models key with an alias (200 both), GET /v1/model/info and GET /model_group/info for 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_model callers can_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.py Anthropic context-management compaction, which calls _can_object_call_model without key aliases and keeps its pre-PR behavior (untested by this PR); alias_map rename consumers in model_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 cache


Note

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/models could list a key’s aliases but completions still returned 403, because auth only considered team, global, and router aliases.

Auth now builds key_model_aliases from the token (via shared alias_map) and passes them through key, team, user, project, team-member, access-group, fallback, and agent-caller checks. _can_object_call_model resolves aliases in the same order as dispatch (team → key → global model_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_list now 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

mateo-berri and others added 2 commits September 24, 2026 21:15
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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High risk] Authorization checks now apply key model aliases.

The PR appears safe to merge based on the reviewed changes and resolved prior findings.

Summary

The PR makes key aliases available to model authorization checks and adds coverage for listed aliases being callable. Since the previous review, the agent access-group check was updated to retain the requested name when a team alias targets a deleted deployment.

Reviews (10) · Last reviewed commit: "fix(proxy): keep the requested name when..."

Comment thread litellm/proxy/auth/auth_checks.py Outdated
Comment thread litellm/proxy/auth/auth_checks.py
@codspeed

codspeed Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_key_alias_model_access (86d5c5c) with main (5a75f09)

Open in CodSpeed

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/auth/auth_checks.py 92.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

…r model scopes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py Outdated
…olves

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py
…fore authorizing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py
Comment thread litellm/proxy/auth/auth_checks.py
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py Outdated
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/auth/auth_checks.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/auth/auth_checks.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/auth/auth_checks.py Outdated
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d2d6c65. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/auth/auth_checks.py Outdated
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

…ed deployment

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit 1d51a8d into main Sep 25, 2026
103 of 104 checks passed
@mateo-berri
mateo-berri deleted the litellm_key_alias_model_access branch September 25, 2026 02:51

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 86d5c5c9 Waiting Sep 25, 2026 by devin-ai-integration[bot] via oauth #1003
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant