Skip to content

fix(proxy): list virtual model aliases on /v1/models - #897

Merged
jarvis9443 merged 4 commits into
mainfrom
feat/models-list-virtual-aliases
Aug 5, 2026
Merged

fix(proxy): list virtual model aliases on /v1/models#897
jarvis9443 merged 4 commits into
mainfrom
feat/models-list-virtual-aliases

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes api7/AISIX-Cloud#1237

Problem

GET /v1/models dropped every model carrying a routing block, so a Model Group never appeared in the caller's discovery list — even for a caller API key explicitly authorized to use it.

That breaks the deployment the feature exists for: publish a group as the stable public entry point and keep the direct targets internal by scoping the caller key to the group name. Such a key got back an empty model list, and any OpenAI-compatible client that populates its model picker from /v1/models had nothing to offer.

The filter was also inconsistent with itself. It keyed on the routing block alone, so semantic routers and ensembles — equally multi-target virtual aliases — were listed all along. With a *-scoped key over one direct model, one ensemble, one semantic router and one group, the listing was:

["embed", "gpt4", "panel", "topic-router"]     # the group, and only the group, missing

So "routing constructs aren't discovery-list entries" was never the actual rule; excluding routing alone was.

Change

Define the listing as the set of names a caller may send as model, which is what the OpenAI Models API says data[].id is — "the model identifier, which can be referenced in the API endpoints" (https://platform.openai.com/docs/api-reference/models/list). Every dispatch shape qualifies.

To be precise about the scope of that phrase: a virtual alias resolves on the routes that support virtual dispatch, while /v1/realtime and the files/batches/fine-tuning surface reject one and require a direct model. That gap predates this change — ensembles and semantic routers were already listed — and matches the OpenAI contract, where /v1/models is a global listing rather than a per-route one. The docs PR states the limit where the listing is described. The one exclusion is wildcard aliases, since provider/* is a pattern rather than a requestable name; mainstream OpenAI-compatible gateways likewise return the public entry-point alias and keep the deployments behind it internal, and expand wildcard routes only on an explicit opt-in.

Authorization is untouched. The request path already gates on the requested name (can_access(&req.model)) with no per-target re-check, so the group name is already the ACL unit — the listing now applies the same judgement. A key scoped to a group returns the group and none of its targets: no new permission surface, no target disclosure.

A group carries no provider of its own, so it surfaces as owned_by: "aisix" rather than borrowing a target's.

Behavior change

Callers with a * model ACL now see group entries they did not see before. Keys whose allowed_models is a narrower glob gain the same visibility for aliases matching it — e.g. openai/* now also lists a group named openai/premium-pool. No authorization changes in either case: those names were already callable by those keys, they simply were not enumerated. This is additive for clients; only assertions pinning an exact model list need updating.

No config surface is added — this is the filter on a read-only endpoint — so there is no paired control-plane work.

Tests

tests/e2e/src/cases/models-list-model-group-e2e.test.ts drives the reported scenario against a real gateway + etcd + upstream: a group over two direct targets, one caller key scoped to the group and one unrestricted. Before the fix:

× group-only key discovers the group, can call it, and never sees its targets
  → expected [] to deeply equal [ 'ml-group' ]
× unrestricted key discovers the group alongside the direct models
  → expected [ 'ml-primary', 'ml-secondary' ] to deeply equal [ 'ml-group', 'ml-primary', …(1) ]

The group-only case then calls the id it read off the listing, so an entry that is listed but not callable would fail the test.

Unit tests pin all four dispatch shapes appearing in one listing, the owned_by fallback for a group, and the group-scoped key seeing only the group.

Six sibling e2e comments stated the old contract in prose and are corrected. Their readiness gates stay as they are — each remains right for a reason that survives the change.

Docs: api7/docs#2036

Summary by CodeRabbit

  • New Features

    • /v1/models now includes available routing, semantic, ensemble, and model-group aliases.
    • Model listings respect access permissions, showing only models callers can use.
    • Model groups expose the callable group and, where permitted, their underlying targets.
    • Listed groups can be used for chat completions.
  • Bug Fixes

    • Improved visibility handling for group-scoped access and gateway-owned models.
  • Tests

    • Added end-to-end coverage for model-group discovery, permissions, metadata, and completion requests.

`GET /v1/models` filtered out models carrying a `routing` block, so a
Model Group never appeared in the caller's discovery list even when the
caller API key was authorized for it. Deployments that publish a group
as the only public entry point — targets kept internal by scoping the
key to the group name — showed callers an empty model list, and any
OpenAI-compatible client that populates its model picker from
`/v1/models` had nothing to select.

The filter was also inconsistent with itself: it excluded only
`routing`, so semantic routers and ensembles — equally multi-target
virtual aliases — were listed all along.

The listing is now defined as the set of names a caller may send as
`model`, which is what an OpenAI client expects `data[].id` to be. Every
dispatch shape qualifies; wildcard aliases stay excluded because
`provider/*` is a pattern rather than a requestable name. The
`allowed_models` ACL that already authorizes the request decides what
appears, so a key scoped to a group returns the group and none of its
targets — no new permission surface and no target disclosure.

A group has no `provider` of its own, so it surfaces as
`owned_by: "aisix"` rather than borrowing a target's provider.

Behavior change: keys with a `*` model ACL now see group entries they
did not see before. This is additive for clients; assertions pinning an
exact model list will need updating.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a3f4fe79-aabb-4fdd-a4de-20c7f7cc564b

📥 Commits

Reviewing files that changed from the base of the PR and between fb03a32 and 530500c.

📒 Files selected for processing (1)
  • tests/e2e/src/cases/models-list-model-group-e2e.test.ts
📝 Walkthrough

Walkthrough

/v1/models now returns authorized concrete model names, including routing and Model Group aliases. Wildcard names remain excluded. Tests cover ownership, group-scoped access, discovery metadata, and group invocation.

Changes

Model discovery behavior

Layer / File(s) Summary
Model listing and authorization
crates/aisix-proxy/src/models.rs
The listing includes concrete routing aliases, excludes wildcard names, and applies allowed_models authorization.
Alias ownership and group authorization
crates/aisix-proxy/src/models.rs
Tests verify deterministic ordering, aisix ownership for provider-less routing aliases, and group identifiers for group-scoped authorization.
End-to-end discovery validation
tests/e2e/src/cases/models-list-model-group-e2e.test.ts, tests/e2e/src/cases/*routing*-e2e.test.ts, tests/e2e/src/cases/model-group-member-ratelimit-e2e.test.ts, tests/e2e/src/cases/multidim-ratelimit-policy-e2e.test.ts, tests/e2e/src/cases/upstream-timeout-defaults-e2e.test.ts
End-to-end tests verify restricted and unrestricted listings, OpenAI metadata, Model Group invocation, and propagation probe behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant APIClient as API client
  participant AisixProxy as aisix-proxy
  participant ModelCollection as model collection
  participant ModelGroup as Model Group
  APIClient->>AisixProxy: GET /v1/models with API key
  AisixProxy->>ModelCollection: collect concrete model names
  ModelCollection->>ModelGroup: include authorized routing alias
  ModelCollection-->>AisixProxy: sorted authorized models
  AisixProxy-->>APIClient: OpenAI model list
  APIClient->>AisixProxy: chat completion using group ID
  AisixProxy->>ModelGroup: dispatch request
Loading
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The second E2E test waits only for ml-primary but immediately asserts ml-secondary and ml-group, creating a propagation race and hidden dependence on the first test. Make each test independently wait until all required IDs are listed, or add a shared all-resource readiness gate before assertions.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the primary change: exposing virtual model aliases through /v1/models.
Linked Issues check ✅ Passed The changes satisfy [#1237] by exposing authorized groups and aliases, preserving ACLs and wildcard exclusion, and adding relevant coverage.
Out of Scope Changes check ✅ Passed The implementation and test updates directly support virtual alias discovery, authorization behavior, and related routing visibility requirements.
Security Check ✅ Passed No issues found in categories 1–7: /v1/models requires AuthenticatedKey, filters through allowed_models, returns only model metadata, and group-only tests exclude targets; no writes, secrets, or TL...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/models-list-virtual-aliases

Comment @coderabbitai help to get the list of available commands.

Four more sibling specs still stated that routing models never appear on
/v1/models, two of them as the stated justification for their readiness
gate. Correct all four; the gates themselves stay as they are, since
each remains right for a reason that survives the change.

Pin `owned_by` to the gateway instead of "some non-empty string". The
loose assertion could not catch the leak its own comment guards against:
resolving a group's owner from its first target's provider still yields
a non-empty string, so the e2e passed while a target's provider leaked
into the listing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/src/cases/models-list-model-group-e2e.test.ts`:
- Around line 127-137: Update the readiness callback passed to
waitConfigPropagation around the client.chat.completions.create call to return
false only for recognized transient configuration-propagation errors. Preserve
successful assistant-role validation, but rethrow authentication, upstream,
transport, invalid-response, and other unexpected errors so they are not hidden
as a timeout.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71441695-20f7-470a-95bb-5fe4007bf2f1

📥 Commits

Reviewing files that changed from the base of the PR and between b5878b5 and fb03a32.

📒 Files selected for processing (7)
  • crates/aisix-proxy/src/models.rs
  • tests/e2e/src/cases/canary-routing-e2e.test.ts
  • tests/e2e/src/cases/cost-aware-routing-e2e.test.ts
  • tests/e2e/src/cases/model-group-member-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/models-list-model-group-e2e.test.ts
  • tests/e2e/src/cases/multidim-ratelimit-policy-e2e.test.ts
  • tests/e2e/src/cases/upstream-timeout-defaults-e2e.test.ts

Comment thread tests/e2e/src/cases/models-list-model-group-e2e.test.ts
The unrestricted-key test waited for `ml-primary` and then asserted
`ml-secondary` and `ml-group` too, but both are written after it — the
gate could not imply they had landed and the test leaned on the previous
one having already warmed the snapshot.

Both caller keys are seeded after every model, so under revision-order
apply a key authenticating at all proves the models are in the snapshot.
Gate on that instead. It also drops the catch-all around the chat probe,
which turned an authentication, transport or upstream failure into a
generic 30s propagation timeout; the chat call is now a plain assertion
that surfaces the real error.
@jarvis9443
jarvis9443 merged commit b75ec96 into main Aug 5, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the feat/models-list-virtual-aliases branch August 5, 2026 08:59
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