Skip to content

fix(proxy): list key and team model aliases in GET /v1/models - #42908

Merged
mateo-berri merged 13 commits into
mainfrom
litellm_models_list_team_aliases
Sep 24, 2026
Merged

mateo-berri merged 13 commits into
mainfrom
litellm_models_list_team_aliases

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Team and key model_aliases route on /chat/completions but never show in GET /v1/models
  • Claude Code and other managed clients cannot discover the alias name they are meant to send
  • Same gap under JWT auth and team virtual keys, in the OpenAI and Anthropic list shapes

How it solves it:

  • /v1/models adds one row per caller alias whose target the caller can already list
  • Team aliases only apply when listing the team the key authenticated as
  • Alias names come from the caller's own key and team maps; their targets resolve through team, key, the global model_alias_map, then key again, the passes /chat/completions runs, so list, retrieval and routing agree on one target
  • /v1/models/{alias} returns the target's metadata under the requested alias id, also when the target is a team scoped model

User Flow

Before: a Claude Code user on a team whose admin aliased claude-sonnet-4-5 to gpt-4.1-mini cannot see that alias in the model list, even though it works

  1. They send POST https://litellm-domain/v1/chat/completions with "model": "claude-sonnet-4-5" and a team JWT, and get a 200 answered by gpt-4.1-mini
  2. They send GET https://litellm-domain/v1/models with the same JWT, with or without anthropic-version, and get ["gpt-4.1-mini"] only
  3. Claude Code's model picker shows no claude-sonnet-4-5 entry, and GET https://litellm-domain/v1/models/claude-sonnet-4-5 returns 400 does not exist or is not accessible

After: the same user sees the alias in the list and can pick it

  1. They send POST https://litellm-domain/v1/chat/completions with "model": "claude-sonnet-4-5" and a team JWT, and get a 200 answered by gpt-4.1-mini
  2. They send GET https://litellm-domain/v1/models with the same JWT, with or without anthropic-version, and get ["gpt-4.1-mini", "claude-sonnet-4-5"]
  3. Claude Code's model picker lists claude-sonnet-4-5[1m], and GET https://litellm-domain/v1/models/claude-sonnet-4-5 returns 200 with the target's metadata

Relevant issues

Fixes #37034

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)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Setup shared by both legs: live proxy on localhost:4000 with Postgres, JWT auth against a local JWKS, a team repro-team-9069 with models: [gpt-4.1-mini] and model_aliases: {claude-sonnet-4-5: gpt-4.1-mini}, a JWT whose groups claim binds the caller to that team, and real OpenAI calls. $JWT is that token. Screen recording of the run below is linked from the internal Slack thread: https://berriaillm.slack.com/archives/C0C04J86WTC/p1790237318325969?thread_ts=1790235554.889039&cid=C0C04J86WTC

Before (09ebb28)

Alias routes on chat completions

  1. curl -s localhost:4000/v1/chat/completions -H "Authorization: Bearer $JWT" -H 'content-type: application/json' -d '{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"say pong"}],"max_tokens":5}'
  2. model: claude-sonnet-4-5 | content: Pong!

OpenAI shape list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT"
  2. ["gpt-4.1-mini"]

Anthropic shape list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT" -H "anthropic-version: 2023-06-01"
  2. ["gpt-4.1-mini"]

Claude Code picker list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT" -H "anthropic-version: 2023-06-01" -H "user-agent: claude-cli/2.1.267 (external, cli)"
  2. ["claude-router-6770742d342e312d6d696e69[1m]"]

Alias model info

  1. curl -s localhost:4000/v1/models/claude-sonnet-4-5 -H "Authorization: Bearer $JWT"
  2. {"detail":"The model claude-sonnet-4-5 does not exist or is not accessible"}

After (2eb9a1d, whose behavior 579abf4, 4a06c64, 459bbd3, 8e970ce, 884653a and 2557744 keep for this flow; identical output on a3a2a1a, d6ec911, f187b8a, 8a205fb and 470725b)

Alias routes on chat completions

  1. curl -s localhost:4000/v1/chat/completions -H "Authorization: Bearer $JWT" -H 'content-type: application/json' -d '{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"say pong"}],"max_tokens":5}'
  2. model: claude-sonnet-4-5 | content: Pong!

OpenAI shape list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT"
  2. ["gpt-4.1-mini", "claude-sonnet-4-5"]

Anthropic shape list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT" -H "anthropic-version: 2023-06-01"
  2. ["gpt-4.1-mini", "claude-sonnet-4-5"]

Claude Code picker list

  1. curl -s localhost:4000/v1/models -H "Authorization: Bearer $JWT" -H "anthropic-version: 2023-06-01" -H "user-agent: claude-cli/2.1.267 (external, cli)"
  2. ["claude-router-6770742d342e312d6d696e69[1m]", "claude-sonnet-4-5[1m]"]

Alias model info

  1. curl -s localhost:4000/v1/models/claude-sonnet-4-5 -H "Authorization: Bearer $JWT"
  2. {"id":"claude-sonnet-4-5","object":"model","created":1677610602,"owned_by":"openai","mode":"chat","max_input_tokens":1047576,"max_output_tokens":32768}

Master key on the After proxy still lists ["gpt-4.1-mini", "gpt-4.1"], so team aliases stay scoped to the caller

Type

🐛 Bug Fix

Caveats (if any)

Medium

  • An alias to a model the caller cannot list stays hidden, matching the 401 it would get on completions
  • An alias that collides with a listed model id keeps the listed row at list and at GET /v1/models/{id}, the alias is not shown twice, while /chat/completions still rewrites that name through the alias
  • The listed-id guard at GET /v1/models/{id} uses the same undiscoverable filter as the list, so an alias named like an undiscoverable model resolves to the alias target, while undiscoverable models themselves stay retrievable by id
  • The Claude Code picker itself was not driven headfully in this run (no npm access on the box); the picker view is covered by a curl with the claude-cli user agent and a unit test at that user agent

Low

  • Alias rows reuse the target's metadata, so owned_by and token limits are the target's
  • Global model_alias_map names are a rewrite hop only, they do not become rows in the caller's list
  • A malformed alias entry (non string key or value) is skipped on its own, the rest of the map still lists
  • return_wildcard_routes=true does not expand aliases against wildcard patterns

QA runbook

Needs the e2e stack from tests/e2e/CONTRIBUTING.md: proxy with JWT auth against the local Keycloak, OPENAI_API_KEY for the cheap OpenAI model, master key for team creation

  • tests/e2e/other/test_jwt_auth_e2e.py::TestJwtTeamHeader::test_team_model_alias_is_listed_by_v1_models_under_the_same_token_that_routes_it[openai_shape] - a team alias the JWT caller can complete on is listed by GET /v1/models for that caller in the OpenAI shape
    • Create a Keycloak user in a new group, then POST /team/new with the master key: {"team_id": "<group>", "team_alias": "aliased", "models": ["gpt-4.1-mini"], "model_aliases": {"my-alias": "gpt-4.1-mini"}}
    • Get an access token for that user and POST /v1/chat/completions with {"model": "my-alias", ...}, expect 200 with a non-empty choices array
    • GET /v1/models with the same token, expect data[].id to contain both my-alias and gpt-4.1-mini
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/other/test_jwt_auth_e2e.py::TestJwtTeamHeader::test_team_model_alias_is_listed_by_v1_models_under_the_same_token_that_routes_it[anthropic_shape] - the same alias is listed in the Anthropic Models API shape Claude Code reads
    • Same team, token and completion as above
    • GET /v1/models with the same token plus anthropic-version: 2023-06-01, expect data[].id to contain both my-alias and gpt-4.1-mini
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky

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

Link to Devin session: https://app.devin.ai/sessions/819b5689891f44fb88c984dd8c4149fb
Open in Devin Desktop: https://app.devin.ai/desktop/session/819b5689891f44fb88c984dd8c4149fb?variant=devin


Note

Medium Risk
Changes model discovery and /v1/models/{id} resolution for authenticated callers; behavior is scoped to alias presentation but must stay aligned with existing routing and team scoping.

Overview
GET /v1/models and /v1/models/{id} now expose key and team model_aliases the same way /chat/completions already routes them, so managed clients (e.g. Claude Code under JWT) can discover alias names next to their targets in OpenAI and Anthropic list shapes.

New listing helpers in model_listing_utils build caller-specific alias maps (team maps only when listing the authenticated team), append one list row per alias whose fully rewritten target is already listable, and resolve alias lookups using the same pass order as completions: team → key → global model_alias_map → key again. Collisions with an existing listed id keep the original row; malformed map entries are skipped; global alias names rewrite but are not added as rows.

model_info applies the same rewrite for {id} lookups, returns metadata under the requested alias id, and aligns undiscoverable-model handling with the list so alias names like an undiscoverable deployment still resolve to the alias target.

Reviewed by Cursor Bugbot for commit 2557744. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 24, 2026 08:09
@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

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding findings remain.

Findings

  1. P1 Listed ID retrieves different model ▶

Summary

The PR makes caller key and team model aliases discoverable through model listing and retrievable by alias ID.

  • Alias rows use the accessible target’s metadata and follow the request-time rewrite order.
  • Tests cover list shapes, retrieval, collisions, malformed entries, and JWT-authenticated routing.

Reviews (11) · Last reviewed commit: "test(proxy): annotate the new alias list..."

Comment thread litellm/proxy/common_utils/model_listing_utils.py Outdated
Comment thread litellm/proxy/proxy_server.py Outdated
Comment thread litellm/proxy/proxy_server.py Outdated
Comment thread tests/e2e/other/other_client.py
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@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/proxy_server.py Outdated
user_api_key_dict.aliases, user_api_key_dict.team_model_aliases, user_api_key_dict.team_id, team_id
),
)
or model_id,

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.

Alias retrieve overwrites response id

Medium Severity

GET /v1/models/{alias} resolves the alias through resolve_public_name and then sets id from internal_to_public. When the target is a team-scoped model, that map replaces the requested alias with the team's public name, so retrieve returns a different id than the listing row for the same alias.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d6ec911. Configure here.

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.

Fixed in f187b8a: when the request is an alias the response id is the alias, regression test added for a team scoped target

@codspeed

codspeed Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_models_list_team_aliases (2557744) with main (ddc7ee6)

Open in CodSpeed

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…p the alias as the retrieved id

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.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/common_utils/model_listing_utils.py
Comment thread litellm/proxy/common_utils/model_listing_utils.py
…ly malformed alias entries

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

Comment on lines 11426 to 11428
resolved_model_id: Final = TeamModelNameTranslator.resolve_public_name(
model_id=model_id,
model_id=aliased_model_id or model_id,
available_models=all_models,

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.

P1 Listed ID retrieves different model

If an undiscoverable team model and a discoverable model share a public ID, /v1/models filters out the undiscoverable model and shows the discoverable model’s metadata. This retrieval path instead resolves the ID using all_models, which still contains the undiscoverable model. If that model comes first, GET /v1/models/{id} returns its metadata for the ID shown in the list, so clients see conflicting information for the same model.

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.

Pre-existing and out of scope: model_info has always resolved through all_models including undiscoverable deployments, and test_v1_models_by_id_still_serves_the_hidden_model_to_non_admin pins that retrieval. This PR only excludes undiscoverable ids from the alias guard so alias resolution mirrors listing; a public id shared by a hidden and a visible deployment is a config collision the existing first-occurrence resolver already handled this way before this change.

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.

You're right — the existing model_info contract intentionally resolves direct IDs through all_models, including undiscoverable deployments, and test_v1_models_by_id_still_serves_the_hidden_model_to_non_admin pins that behavior. This PR only removes undiscoverable IDs from the alias collision guard so alias resolution matches the list; it does not introduce the hidden/visible public-ID collision behavior. I’m withdrawing this comment.

…alias guard

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.

Stale Bugbot comment from a previous run.

Comment thread tests/test_litellm/proxy/test_model_list_aliases.py Outdated
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.

Stale Bugbot comment from a previous run.

Comment thread tests/test_litellm/proxy/test_model_list_aliases.py Outdated
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 2557744. 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 8477fe4 into main Sep 24, 2026
129 of 136 checks passed
@mateo-berri
mateo-berri deleted the litellm_models_list_team_aliases branch September 24, 2026 13:41
devin-ai-integration Bot added a commit that referenced this pull request Sep 24, 2026
* fix(proxy): list key and team model aliases in GET /v1/models


* fix(proxy): keep alias listing helpers within the type discipline budget


* test(proxy): cover alias rows on GET /v1/models and /v1/models/{id}


* fix(proxy): apply team then key aliases like chat completions and keep the alias as the retrieved id


* fix(proxy): apply key aliases twice like chat completions and skip only malformed alias entries


* fix(proxy): apply the global model_alias_map between the key alias passes like chat completions


* fix(proxy): list only the caller's own aliases and never rewrite a listed model id on retrieval


* style(proxy): ruff format model_info alias lookup


* fix(proxy): hide undiscoverable names from model retrieval so an alias named like one resolves to its target


* fix(proxy): keep undiscoverable models retrievable by id while excluding them from the alias guard


* refactor(proxy): pass an immutable name sequence into the model_info alias guard


* test(proxy): type the model list alias test helpers


* test(proxy): annotate the new alias listing test fixtures and helpers


---------

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

This branch had an error being deployed

1 failed deployment
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.

[Feature]: Add Team ALIASES to /v1/models list

2 participants