Skip to content

chore(admin): rename Credential→ProviderKey, drop Team — align with AISIX-Cloud naming - #95

Merged
moonming merged 5 commits into
mainfrom
chore/v3-naming-alignment
May 7, 2026
Merged

chore(admin): rename Credential→ProviderKey, drop Team — align with AISIX-Cloud naming#95
moonming merged 5 commits into
mainfrom
chore/v3-naming-alignment

Conversation

@moonming

@moonming moonming commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

The standalone Admin API's resource vocabulary drifted from the AISIX-Cloud control plane: cp-api has always called the upstream-secret entity ProviderKey, but ai-gateway exposed the same concept as Credential under /admin/v1/credentials. Same shape, different name — confusing for anyone reading dashboard + standalone docs side by side, and pinned cp-api's mustMarshalModelKV to a denormalised projection (see the comment around internal/cpapi/resources/handlers.go:855).

This PR realigns the standalone surface to AISIX-Cloud's vocabulary so the next phase (Model.provider_configprovider_key_id reference) can land cleanly on top.

Renames

  • CredentialProviderKey
  • field namedisplay_name
  • field api_keysecret
  • etcd prefix /credentials//provider_keys/
  • admin route /admin/v1/credentials/admin/v1/provider_keys
  • validate_credentialvalidate_provider_key

Deletions

  • Team is removed entirely from the standalone surface. It's a SaaS-tier concept owned by AISIX-Cloud (org / member / role) — the in-gateway "Team as ApiKey container" entity confused the layering. Standalone deployments do per-key budgeting via ApiKey.max_budget_usd and per-key rate-limiting via ApiKey.rate_limit. cp-api's Team table stays untouched.

Out of scope (deferred to follow-up PR)

  • Model restructure ({name, model, provider_config}{display_name, model_name, provider_key_id}) — touches all five provider bridges, the proxy hot path, and cp-api's mustMarshalModelKV (cross-repo coordination). Cleaner as its own reviewable diff.
  • ApiKey.display_name field — same follow-up.

Test plan

  • cargo check --workspace --tests clean
  • cargo clippy --workspace --tests -- -D warnings clean
  • cargo test --workspace green (full suite, ~600 tests)
  • Dropped Team test cases; ProviderKey tests cover the new field shape (display_name, secret)
  • docs/api-admin.md + docs/architecture.md + README.md + config.example.yaml reflect the new naming

Summary by CodeRabbit

  • New Features

    • Admin API: provider key management (full CRUD), Spend and health endpoints.
    • DP v3 one-shot registration flow for managed deployments.
  • Documentation

    • Expanded README (“What’s shipped today”) and Standalone vs Managed matrix.
    • Updated Admin API docs, architecture doc, and config example to reflect provider keys, spend/health, and operational behavior.
  • Removals

    • Credentials and teams management removed from the Admin API and snapshot surface.

…ISIX-Cloud naming

The standalone Admin API's resource names drifted from the AISIX-Cloud
control plane: cp-api has always called the upstream-secret entity
`ProviderKey`, but ai-gateway exposed the same concept as `Credential`
under `/admin/v1/credentials`. Same shape, different name in two
places — confusing for anyone reading dashboard + standalone docs side
by side, and pinned the cp-api → kine projection on a "Phase 2"
note (see internal/cpapi/resources/handlers.go around mustMarshalModelKV).

This PR realigns the standalone surface to AISIX-Cloud's vocabulary so
the next phase (Model.provider_config → provider_key_id reference, in a
separate PR) can land cleanly on top.

Renames
- `Credential` → `ProviderKey`
- field `name` → `display_name`
- field `api_key` → `secret`
- etcd prefix `/credentials/` → `/provider_keys/`
- admin route `/admin/v1/credentials` → `/admin/v1/provider_keys`
- `validate_credential` → `validate_provider_key`

Deletions
- `Team` is removed entirely from the standalone surface. It's a
  SaaS-tier concept owned by AISIX-Cloud (org / member / role) — the
  in-gateway "Team as ApiKey container" entity confused the layering.
  Standalone deployments do per-key budgeting via
  `ApiKey.max_budget_usd` and per-key rate-limiting via
  `ApiKey.rate_limit`. cp-api's Team table stays untouched.

Out of scope
- Model restructure (`{name, model, provider_config}` →
  `{display_name, model_name, provider_key_id}`) deferred to a
  follow-up PR. That change touches all five provider bridges, the
  proxy hot path, and the cp-api `mustMarshalModelKV` projection
  (cross-repo coordination), so it's better as its own reviewable diff.
- ApiKey.display_name field deferred to the same follow-up.

Verified
- `cargo check --workspace --tests` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test --workspace` green (full suite)
Copilot AI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 5ff83324-cfa3-428a-a4ba-977543488923

📥 Commits

Reviewing files that changed from the base of the PR and between 0b68536 and 7e723dd.

📒 Files selected for processing (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

📝 Walkthrough

Walkthrough

Replaces Team/Credential resources with a ProviderKey resource end‑to‑end (model, schema, snapshot, etcd loader, store, admin handlers, docs), removes teams_handlers and credential model/schema, and separately adds a DP v3 one‑shot registration flow (keygen, /dp/register client, atomic persistence, tests).

Changes

ProviderKey integration & removal of Team/Credential

Layer / File(s) Summary
Data Shape & Core Models
crates/aisix-core/src/models/provider_key.rs, crates/aisix-core/src/models/credential.rs, crates/aisix-core/src/models/mod.rs
Adds ProviderKey struct and Resource impl; deletes Credential module; updates models::mod to expose ProviderKey and remove Credential/Team.
Schema & Validation
crates/aisix-core/src/models/schema.rs
Introduces provider_key_schema() and validate_provider_key(); removes credential/team validators and updates Schemas::compile().
Snapshot & Loader
crates/aisix-core/src/models/snapshot.rs, crates/aisix-core/src/resource.rs, crates/aisix-core/src/snapshot.rs, crates/aisix-etcd/src/loader.rs
Adds provider_keys: ResourceTable<ProviderKey> to AisixSnapshot and includes it in totals; etcd loader recognizes "provider_keys", validates and inserts entries; docs/tests updated to reflect snapshot shape change.
Store Trait & Implementations
crates/aisix-admin/src/store.rs, crates/aisix-admin/src/etcd_store.rs
Extends ConfigStore with provider-key CRUD (put/get/list/delete); InMemoryStore adds provider_keys DashMap; EtcdConfigStore adds PROVIDER_KEYS_SUBKEY and etcd-backed provider-key methods; removes credential/team CRUD from trait/impls.
Admin Handlers & Routing
crates/aisix-admin/src/provider_keys_handlers.rs, crates/aisix-admin/src/credentials_handlers.rs, crates/aisix-admin/src/teams_handlers.rs, crates/aisix-admin/src/lib.rs
Adds Axum CRUD handlers for /admin/v1/provider_keys (list/get/create/update/delete) with schema validation, uniqueness checks, UUID id assignment and revision bumping; deletes teams_handlers.rs; wires routes into admin router and adjusts modules.
Public API & Core Exports
crates/aisix-core/src/lib.rs
Updates re-exports: adds validate_provider_key, removes validate_credential, validate_team, and Team export.
Docs & Config Examples
README.md, config.example.yaml, docs/api-admin.md, docs/architecture.md, crates/aisix-etcd/src/key.rs
README extended with “What’s shipped today” and Standalone vs Managed matrix; config.example.yaml notes omitted managed resources; admin docs add ProviderKeys/Spend and remove Credentials/Budgets; architecture/docs and etcd key examples updated.

DP v3 registration & server helpers

Layer / File(s) Summary
Control Flow & Wire Types
crates/aisix-server/src/register.rs
Implements DP v3 registration: request JSON includes dp_protocol_version = "v3", hostname, version, public_key; defines request/response wire types and parsing logic.
Client & HTTP Handling
crates/aisix-server/src/register.rs
HTTP client call to /dp/register with Bearer auth, timeout, optional extra root CA injection, non-2xx handling with status + truncated body, and JSON response decoding.
Local Keygen & Host Identity
crates/aisix-server/src/register.rs
Generates local ECDSA P‑256 keypair, gathers host identity (hostname helper), and encodes public key for the register request.
Atomic Persistence
crates/aisix-server/src/register.rs
Atomically persists ca.crt, client.crt, client.key (PKCS#8) with 0600 permissions, writes dp_id and env_id, exposes helpers to detect/read existing bundle, and Unix-only atomic write implementation.
Tests / Error Handling
crates/aisix-server/src/register.rs, crates/aisix-server/src/cert_bundle.rs, crates/aisix-server/src/main.rs
Adds tests (Wiremock) covering v3 fields, persistence, file permissions, env_id behavior, and HTTP error propagation. Minor formatting refinements in cert handling and an error construction in main.
Docs / Config Notes
README.md, config.example.yaml
README and config examples updated to document managed-mode bootstrap and DP certificate bootstrap/persistence behavior.

Sequence Diagram(s)

sequenceDiagram
    participant DP as "DP (agent)"
    participant KeyGen as "Local KeyGen"
    participant CP as "Control Plane (/dp/register)"
    participant FS as "Filesystem (mtls dir)"

    DP->>KeyGen: generate ECDSA P-256 keypair
    KeyGen-->>DP: public_key, private_key
    DP->>CP: POST /dp/register {hostname, version, dp_protocol_version:"v3", public_key}
    CP-->>DP: 2xx {ca, client_crt, dp_id, rotation_paths, telemetry_paths}
    DP->>FS: atomically write ca.crt, client.crt, client.key (0600)
    DP->>FS: persist dp_id and env_id
    DP-->>DP: mark bundle exists for future boots
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

Copilot AI 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.

Pull request overview

This PR realigns the standalone Admin API and underlying etcd resource vocabulary with AISIX-Cloud naming by renaming CredentialProviderKey, removing the standalone Team entity, and updating docs/config accordingly to reflect the streamlined standalone surface.

Changes:

  • Rename the upstream-secret resource from Credential to ProviderKey (fields and etcd/admin route prefixes updated accordingly).
  • Remove the Team entity and its Admin API CRUD surface from standalone.
  • Update snapshot loading/schema validation and documentation to reflect the new resource set.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
README.md Updates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.md Updates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.md Renames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yaml Updates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rs Switches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rs Updates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rs Updates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rs Updates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rs Removes credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rs Removes credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rs Replaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rs Introduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rs Deletes old Credential entity.
crates/aisix-core/src/models/team.rs Deletes old Team entity.
crates/aisix-core/src/lib.rs Updates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rs Replaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rs Adds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rs Updates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rs Updates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rs Deletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rs Deletes old teams handlers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/api-admin.md
Comment on lines +127 to +130
Gemini, DeepSeek). Use this when multiple Models share the same
upstream provider key — the Model can then reference the
ProviderKey by id rather than embedding the secret. Naming aligns
with the AISIX-Cloud control plane's `ProviderKey` table.
Comment thread docs/api-admin.md
Comment on lines +124 to 125
### 4.3 ProviderKeys — `/admin/v1/provider_keys`

Comment on lines +4 to +7
//! (OpenAI, Anthropic, Gemini, DeepSeek, …) once and have many Models
//! reference it by id (`provider_key_id`). Rotating the secret then
//! becomes a single PUT against the ProviderKey rather than rewriting
//! every Model that uses it.
Comment thread README.md Outdated
Comment on lines 12 to 14
- **Proxy API (`:3000`)** — OpenAI-compatible `/v1/chat/completions`, `/v1/embeddings`, `/v1/models`, `/v1/messages` (Anthropic native), plus passthrough
- **Admin API (`:3001`)** — CRUD for models, API keys, teams, budgets, credentials, guardrails, fallbacks; playground proxy; OpenAPI (Scalar) at `/openapi`
- **Admin API (`:3001`)** — CRUD for models, API keys, provider keys, guardrails, cache policies, observability exporters; per-key budgets inline; playground proxy; OpenAPI (Scalar) at `/openapi`
- **Config store** — etcd with watch-driven, lock-free `ArcSwap` snapshot
Comment thread docs/architecture.md
Comment on lines +19 to +21
- Operator-driven configuration. Models, ApiKeys, ProviderKeys, Guardrails,
CachePolicies, and ObservabilityExporters all live in etcd and are
mutated via the Admin API at runtime.
Comment on lines 76 to 99
"api_keys" => {
if let Some(entry) = validate_and_parse::<ApiKey>(
&raw.key,
raw.revision,
parsed,
&value,
validate_apikey,
&mut stats,
) {
snapshot.apikeys.insert(entry);
}
}
"credentials" => {
if let Some(entry) = validate_and_parse::<Credential>(
"provider_keys" => {
if let Some(entry) = validate_and_parse::<ProviderKey>(
&raw.key,
raw.revision,
parsed,
&value,
validate_credential,
validate_provider_key,
&mut stats,
) {
snapshot.credentials.insert(entry);
snapshot.provider_keys.insert(entry);
}
}
Comment on lines +42 to +76
pub async fn create_provider_key(
_auth: AdminAuth,
State(state): State<AdminState>,
Json(raw): Json<Value>,
) -> Result<Json<ResourceEntry<ProviderKey>>, AdminError> {
let provider_key = decode(&raw)?;
let all = state.store.list_provider_keys().await?;
assert_unique_display_name(&all, &provider_key.display_name, None)?;

let id = Uuid::new_v4().to_string();
let entry = ResourceEntry::new(&id, provider_key, STARTING_REVISION);
state.store.put_provider_key(entry.clone()).await?;
Ok(Json(entry))
}

pub async fn update_provider_key(
_auth: AdminAuth,
Path(id): Path<String>,
State(state): State<AdminState>,
Json(raw): Json<Value>,
) -> Result<Json<ResourceEntry<ProviderKey>>, AdminError> {
let existing = state
.store
.get_provider_key(&id)
.await?
.ok_or(AdminError::NotFound)?;
let provider_key = decode(&raw)?;

let all = state.store.list_provider_keys().await?;
assert_unique_display_name(&all, &provider_key.display_name, Some(&id))?;

let entry = ResourceEntry::new(&id, provider_key, existing.revision + 1);
state.store.put_provider_key(entry.clone()).await?;
Ok(Json(entry))
}
moonming added 3 commits May 7, 2026 09:47
Pre-existing rustfmt drift on main was tripping `cargo fmt --check` in
the lint job after #95 landed (it surfaces here because the lint job
runs on every PR, not just on main). The diff is purely whitespace —
breaking long fn-call args across lines and aligning the
`..ManagedConfig::default()` struct-update tail. No semantic changes.

Folding it into #95 because:
- Skipping it would leave PR #95 red on a check unrelated to its
  intent
- A separate "lint-only" PR would still need to be rebased into #95
  to unblock it
Three sections added so anyone landing on the repo can answer:
1. What does aisix do today? — list each /v1/* route, each provider
   bridge, each Admin resource, plus cache / guardrail / ratelimit /
   observability surfaces actually in main.
2. What's the difference between standalone and managed (DP-with-CP)
   mode? — table covering tenancy, ProviderKey storage, budgets,
   audit, RBAC, billing, etc. Calls out which resources are SaaS-tier
   only (Budget / Team / Member / Audit / Billing) so standalone
   users know the inline alternatives.
3. Where is this going? — P0 / P1 / P2 milestone breakdown, each item
   linked to its tracking issue.

Pulled the "scaffold (PR #1)" status line — the surface described
above is past that bar. The roadmap section makes the maturity story
explicit instead.
Per product feedback — don't position the project against named
competitors. The "Rust-native, single static binary" pitch stands on
its own. Touched:

- README.md hero blurb: replaced "in the spirit of LiteLLM /
  Portkey" with the standalone product description
- README.md P2 roadmap bullets: "~95 LiteLLM providers" → "~95
  long-tail providers" (same items, no name-drop)
- docs/architecture.md §3.3: dropped the parenthetical "(LiteLLM's
  choice)" when contrasting etcd vs relational stores
Copilot AI review requested due to automatic review settings May 7, 2026 01:53
…ed RL P0→P1

Bedrock guardrails are blocking enterprise demos (compliance asks),
moves up. Redis-backed distributed rate limiting is nice-to-have for
multi-DP HA but standalone single-process limiter covers the common
case for v1.0, moves down.

Copilot AI 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.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

docs/api-admin.md:179

  • Docs describe a GET /admin/v1/spend endpoint backed by BudgetTracker, but there is no corresponding route/handler in the current codebase and it is not present in the admin OpenAPI spec. Please either add the endpoint or remove/update this section so the docs match the shipped API.
### 4.6 Spend — `GET /admin/v1/spend`

Current-month accumulated USD spend per ApiKey from the in-process
`BudgetTracker`. Returns the period (`YYYY-MM`) plus per-key entries.

Comment thread README.md
Comment on lines +30 to +39
- **Admin API (`:3001`)** — CRUD on every entity, JSON-Schema validated, OpenAPI 3 + Scalar UI at `/admin/openapi-scalar`
- `/admin/v1/models`
- `/admin/v1/apikeys` (+ `POST .../rotate`)
- `/admin/v1/provider_keys`
- `/admin/v1/guardrails`
- `/admin/v1/cache_policies`
- `/admin/v1/observability_exporters`
- `/admin/v1/health` — per-model upstream health (Healthy / Degraded / Down)
- `/admin/v1/spend` — current-month USD per ApiKey
- `/playground/chat/completions` — in-process forward to the proxy router
Comment thread README.md
| Pricing / cost | Per-Model `cost.input_per_1k` / `cost.output_per_1k` | Pricing rows synced from models.dev + per-model overrides |
| Personal Access Tokens | None | `aisix_pat_*` for CLI / CI |
| Billing | None | Stripe portal handoff, plan management, metering |
| Guardrail / cache / exporter CRUD | `/admin/v1/*` direct write | Dashboard CRUD → cp-api validates → projects to env's etcd via outbox |
Comment thread docs/architecture.md
Comment on lines +19 to +21
- Operator-driven configuration. Models, ApiKeys, ProviderKeys, Guardrails,
CachePolicies, and ObservabilityExporters all live in etcd and are
mutated via the Admin API at runtime.
Comment on lines +42 to +55
pub async fn create_provider_key(
_auth: AdminAuth,
State(state): State<AdminState>,
Json(raw): Json<Value>,
) -> Result<Json<ResourceEntry<ProviderKey>>, AdminError> {
let provider_key = decode(&raw)?;
let all = state.store.list_provider_keys().await?;
assert_unique_display_name(&all, &provider_key.display_name, None)?;

let id = Uuid::new_v4().to_string();
let entry = ResourceEntry::new(&id, provider_key, STARTING_REVISION);
state.store.put_provider_key(entry.clone()).await?;
Ok(Json(entry))
}
anyhow::anyhow!(
"managed.cp_base_url required when cert bundle is provided",
)
anyhow::anyhow!("managed.cp_base_url required when cert bundle is provided",)
Comment thread README.md
Comment on lines +51 to +52
- **Telemetry events** — DP-side `UsageEvent` per request with cache_status, reasoning, provider-id detail, guardrail bypass reason. Posted to cp-api in managed mode; consumed by `/admin/v1/spend` in standalone.

@moonming
moonming merged commit 9d1bac9 into main May 7, 2026
7 checks passed
@moonming
moonming deleted the chore/v3-naming-alignment branch May 7, 2026 02:07
moonming added a commit that referenced this pull request May 7, 2026
…96)

The hand-written OpenAPI document was stuck at the original two
resources (Models + ApiKeys) — provider_keys, the apikey rotate sub-
resource, /admin/v1/health, /playground/chat/completions, and the
unauthenticated /health, /metrics, /admin/openapi.json,
/admin/openapi-scalar were all missing. Scalar UI loaders look at this
JSON to populate the left-hand sidebar, so anyone opening
/admin/openapi-scalar in the browser had no way to discover those
routes.

This commit:

- Documents every route mounted in `build_router` (lib.rs line 47-96)
- Adds reusable component schemas: Model (with the new
  display_name/provider/model_name/provider_key_id shape from #95),
  ApiKey, ProviderKey, RateLimit, Routing, ModelCost, AdminError,
  plus *Entry wrappers for the response shape `{id, value, revision}`
- Marks the four public routes (/health, /metrics, /admin/openapi.*)
  with `security: []` so Scalar's "Try it" doesn't prompt for an
  admin key on them
- Pins coverage with a unit test that walks every documented path +
  every reusable schema and fails on first missing entry

Bumps the raw-string delimiter from `r#"..."#` to `r##"..."##` because
the embedded JSON `$ref` pointers (`"#/components/schemas/Foo"`)
collide with the `#` in the closing delimiter.

Drive-by README correction: the previous entry overpromised
`/admin/v1/guardrails`, `/admin/v1/cache_policies`,
`/admin/v1/observability_exporters`, and `/admin/v1/spend` as if they
were live admin routes. They aren't — those resources exist as core
types and are honoured at runtime, but standalone CRUD over them is
direct-etcd-write today. Listed honestly with a one-line "handlers
will follow" note.

Verified
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test -p aisix-admin` green (46 passed, includes the two new
  openapi assertions covering path coverage + public-route security)
moonming added a commit that referenced this pull request May 7, 2026
Realigns the standalone Model schema with the AISIX-Cloud control
plane's normalised shape — the projection cp-api has been waiting
for since PRD-09b §6 (the comment in mustMarshalModelKV calls this
out as "Phase 2 swaps to {model, provider_key_id} with DP-side
join, but requires Model.provider_config refactor across 26 DP
files which is a separate PR" — that's this PR).

Old shape (pre-#95 + this PR):
  { name, model: "<provider>/<id>", provider_config: { api_key, api_base } }

New shape:
  { display_name, provider, model_name, provider_key_id }

Where provider_key_id references a ProviderKey row (introduced as a
top-level resource in #95) carrying secret + api_base. Routing
models keep the same `routing` block but drop the upstream-config
triple — the router resolves a target Model and dispatches against
THAT model's provider_key_id.

Why
- One ProviderKey, many Models. Rotating the upstream secret used
  to require rewriting every Model row that embedded it; now it's
  a single PUT against the ProviderKey.
- AISIX-Cloud parity. cp-api already has a `ProviderKey` table;
  managed-mode DPs need this shape to consume what cp-api projects
  into kine.
- Snapshot-table integrity. The DP can validate at load time that
  every Model.provider_key_id resolves to a ProviderKey in the same
  snapshot, instead of carrying inline secrets it can't cross-check.

Changes by area

aisix-core
- Model: replaced { name, model, provider_config } with
  { display_name, provider: Option<Provider>, model_name:
  Option<String>, provider_key_id: Option<String> }. Routing models
  set `routing` and leave the upstream triple as None.
- Removed ProviderConfig struct entirely.
- JSON Schema: oneOf encodes the direct-vs-routing XOR
  (direct ⇒ all three of provider/model_name/provider_key_id
  required; routing ⇒ all three forbidden).
- Resource::name() now returns &display_name; ApiKey.allowed_models
  matches against the same field (already did, just renamed).

aisix-gateway
- BridgeContext gains `provider_key: Arc<ProviderKey>`. Constructor
  signature is now `new(request_id, model, provider_key)`.

aisix-provider-{openai,anthropic,gemini,deepseek}
- Bridge helpers (resolve_base / api_key / upstream_model) take
  `&BridgeContext` and read from ctx.provider_key + ctx.model
  rather than the now-gone provider_config.

aisix-proxy
- New `dispatch.rs` resolves both Model and ProviderKey from the
  snapshot before each per-endpoint handler builds BridgeContext.
- Every endpoint (chat / completions / embeddings / messages /
  responses / rerank / images / audio / passthrough) updated to
  use the new resolver — no more inline `model.provider_config.api_key`.
- 422 with a clear error envelope when a Model references a
  provider_key_id that isn't in the snapshot.

Tests + fixtures
- Every fixture across the workspace updated to the new JSON shape
  (~30 files: aisix-admin, aisix-cache, aisix-ratelimit,
  aisix-proxy, aisix-gateway, aisix-server, aisix-guardrails,
  aisix-etcd).

Verified
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test --workspace` green (520+ tests, 0 failures)

Cross-repo follow-up
- AISIX-Cloud's `mustMarshalModelKV` (internal/cpapi/resources/handlers.go)
  needs to switch from writing the inline `provider_config` shape to
  the new `{display_name, provider, model_name, provider_key_id}`
  shape. That's tracked separately and lands in AISIX-Cloud.
moonming added a commit that referenced this pull request May 7, 2026
Realigns the standalone Model schema with the AISIX-Cloud control
plane's normalised shape — the projection cp-api has been waiting
for since PRD-09b §6 (the comment in mustMarshalModelKV calls this
out as "Phase 2 swaps to {model, provider_key_id} with DP-side
join, but requires Model.provider_config refactor across 26 DP
files which is a separate PR" — that's this PR).

Old shape (pre-#95 + this PR):
  { name, model: "<provider>/<id>", provider_config: { api_key, api_base } }

New shape:
  { display_name, provider, model_name, provider_key_id }

Where provider_key_id references a ProviderKey row (introduced as a
top-level resource in #95) carrying secret + api_base. Routing
models keep the same `routing` block but drop the upstream-config
triple — the router resolves a target Model and dispatches against
THAT model's provider_key_id.

Why
- One ProviderKey, many Models. Rotating the upstream secret used
  to require rewriting every Model row that embedded it; now it's
  a single PUT against the ProviderKey.
- AISIX-Cloud parity. cp-api already has a `ProviderKey` table;
  managed-mode DPs need this shape to consume what cp-api projects
  into kine.
- Snapshot-table integrity. The DP can validate at load time that
  every Model.provider_key_id resolves to a ProviderKey in the same
  snapshot, instead of carrying inline secrets it can't cross-check.

Changes by area

aisix-core
- Model: replaced { name, model, provider_config } with
  { display_name, provider: Option<Provider>, model_name:
  Option<String>, provider_key_id: Option<String> }. Routing models
  set `routing` and leave the upstream triple as None.
- Removed ProviderConfig struct entirely.
- JSON Schema: oneOf encodes the direct-vs-routing XOR
  (direct ⇒ all three of provider/model_name/provider_key_id
  required; routing ⇒ all three forbidden).
- Resource::name() now returns &display_name; ApiKey.allowed_models
  matches against the same field (already did, just renamed).

aisix-gateway
- BridgeContext gains `provider_key: Arc<ProviderKey>`. Constructor
  signature is now `new(request_id, model, provider_key)`.

aisix-provider-{openai,anthropic,gemini,deepseek}
- Bridge helpers (resolve_base / api_key / upstream_model) take
  `&BridgeContext` and read from ctx.provider_key + ctx.model
  rather than the now-gone provider_config.

aisix-proxy
- New `dispatch.rs` resolves both Model and ProviderKey from the
  snapshot before each per-endpoint handler builds BridgeContext.
- Every endpoint (chat / completions / embeddings / messages /
  responses / rerank / images / audio / passthrough) updated to
  use the new resolver — no more inline `model.provider_config.api_key`.
- 422 with a clear error envelope when a Model references a
  provider_key_id that isn't in the snapshot.

Tests + fixtures
- Every fixture across the workspace updated to the new JSON shape
  (~30 files: aisix-admin, aisix-cache, aisix-ratelimit,
  aisix-proxy, aisix-gateway, aisix-server, aisix-guardrails,
  aisix-etcd).

Verified
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test --workspace` green (520+ tests, 0 failures)

Cross-repo follow-up
- AISIX-Cloud's `mustMarshalModelKV` (internal/cpapi/resources/handlers.go)
  needs to switch from writing the inline `provider_config` shape to
  the new `{display_name, provider, model_name, provider_key_id}`
  shape. That's tracked separately and lands in AISIX-Cloud.
moonming added a commit that referenced this pull request May 7, 2026
…102)

* feat(model): split provider_config inline into ProviderKey reference

Realigns the standalone Model schema with the AISIX-Cloud control
plane's normalised shape — the projection cp-api has been waiting
for since PRD-09b §6 (the comment in mustMarshalModelKV calls this
out as "Phase 2 swaps to {model, provider_key_id} with DP-side
join, but requires Model.provider_config refactor across 26 DP
files which is a separate PR" — that's this PR).

Old shape (pre-#95 + this PR):
  { name, model: "<provider>/<id>", provider_config: { api_key, api_base } }

New shape:
  { display_name, provider, model_name, provider_key_id }

Where provider_key_id references a ProviderKey row (introduced as a
top-level resource in #95) carrying secret + api_base. Routing
models keep the same `routing` block but drop the upstream-config
triple — the router resolves a target Model and dispatches against
THAT model's provider_key_id.

Why
- One ProviderKey, many Models. Rotating the upstream secret used
  to require rewriting every Model row that embedded it; now it's
  a single PUT against the ProviderKey.
- AISIX-Cloud parity. cp-api already has a `ProviderKey` table;
  managed-mode DPs need this shape to consume what cp-api projects
  into kine.
- Snapshot-table integrity. The DP can validate at load time that
  every Model.provider_key_id resolves to a ProviderKey in the same
  snapshot, instead of carrying inline secrets it can't cross-check.

Changes by area

aisix-core
- Model: replaced { name, model, provider_config } with
  { display_name, provider: Option<Provider>, model_name:
  Option<String>, provider_key_id: Option<String> }. Routing models
  set `routing` and leave the upstream triple as None.
- Removed ProviderConfig struct entirely.
- JSON Schema: oneOf encodes the direct-vs-routing XOR
  (direct ⇒ all three of provider/model_name/provider_key_id
  required; routing ⇒ all three forbidden).
- Resource::name() now returns &display_name; ApiKey.allowed_models
  matches against the same field (already did, just renamed).

aisix-gateway
- BridgeContext gains `provider_key: Arc<ProviderKey>`. Constructor
  signature is now `new(request_id, model, provider_key)`.

aisix-provider-{openai,anthropic,gemini,deepseek}
- Bridge helpers (resolve_base / api_key / upstream_model) take
  `&BridgeContext` and read from ctx.provider_key + ctx.model
  rather than the now-gone provider_config.

aisix-proxy
- New `dispatch.rs` resolves both Model and ProviderKey from the
  snapshot before each per-endpoint handler builds BridgeContext.
- Every endpoint (chat / completions / embeddings / messages /
  responses / rerank / images / audio / passthrough) updated to
  use the new resolver — no more inline `model.provider_config.api_key`.
- 422 with a clear error envelope when a Model references a
  provider_key_id that isn't in the snapshot.

Tests + fixtures
- Every fixture across the workspace updated to the new JSON shape
  (~30 files: aisix-admin, aisix-cache, aisix-ratelimit,
  aisix-proxy, aisix-gateway, aisix-server, aisix-guardrails,
  aisix-etcd).

Verified
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test --workspace` green (520+ tests, 0 failures)

Cross-repo follow-up
- AISIX-Cloud's `mustMarshalModelKV` (internal/cpapi/resources/handlers.go)
  needs to switch from writing the inline `provider_config` shape to
  the new `{display_name, provider, model_name, provider_key_id}`
  shape. That's tracked separately and lands in AISIX-Cloud.

* test: migrate Phase B fixtures — etcd_integration + e2e smoke

The Phase B Model restructure commit landed the lib changes but the
test fixtures in crates/aisix-admin/tests/etcd_integration.rs and
tests/e2e/src/cases/smoke.test.ts still posted the old
{name, model:"openai/...", provider_config:{...}} shape. Both surfaces
fail in CI with the schema's
"Additional properties are not allowed" rejection.

- etcd_integration.rs: models_round_trip_through_real_etcd and
  loader_picks_up_every_admin_write switched to {display_name,
  provider, model_name, provider_key_id}
- smoke.test.ts: now posts a ProviderKey first, then references its
  id from the Model — matches the production flow the dashboard
  drives. Adds AdminClient.createProviderKey for the test harness.

* ci: kick the CI again — webhook missed 4d35529

* ci: trigger re-run for 4d35529 (webhook missed)

* fix(messages): port PR #100 cross-provider /v1/messages to Phase B Model

PR #100 (cross-provider /v1/messages — Anthropic protocol over
non-Anthropic upstreams) landed on main with the pre-Phase-B Model
API: model.provider() (method call), gemini_model(name, api_base)
helpers, etc. After rebasing Phase B on top of #100, the Anthropic
matrix tests + cross_provider_dispatch all stop compiling.

This commit ports the survivors:

- cross_provider_dispatch: switched to model.provider field access,
  picks up provider_key via dispatch::resolve_provider_key, threads
  it through BridgeContext::new(req_id, model, pk).
- gemini_model / deepseek_model / anthropic_model_entry test helpers
  drop their api_base parameter — Phase B moves api_base onto
  ProviderKey, and the matrix harness now builds a fresh PK with
  the wiremock URI on every test.
- Three test sites that still passed an extra api_base argument
  updated to the single-arg helper signature.

* fix(supervisor): incremental watch must mirror every resource kind

The supervisor's `apply_put`, `apply_delete`, and `clone_snapshot`
helpers only handled `models` + `api_keys` — Phase B's ProviderKey
and #97's Guardrail / CachePolicy / ObservabilityExporter were
silently no-ops. Admin writes for those four resources landed in
etcd fine, but the watch event got dropped and the proxy snapshot
never updated, so dispatch saw a Model whose `provider_key_id`
pointed at thin air. Smoke test #102 hit this:

    chat returned 500: bridge is misconfigured: model references
    unknown provider_key_id

Fix is mechanical: extend the for-loops in apply_put + clone_snapshot
and the match arms in apply_delete to cover every ResourceTable.

Add `apply_put_propagates_every_resource_kind` + the matching
delete test as forcing functions — any future resource type added
to AisixSnapshot fails this test until the supervisor is updated.

Verified
- cargo fmt --all --check clean
- cargo clippy --workspace --tests -- -D warnings clean
- cargo test --workspace — 548 passed, 0 failed (was 546 + 2 new)

* test(e2e): poll for snapshot readiness instead of fixed 500ms sleep

The smoke test's `chat completion forwards to mock upstream` case
intermittently fails on CI with `unknown provider_key_id` even though
`a Model + ApiKey written via Admin API are visible to /v1/models`
passes immediately before. The fixed-time `waitConfigPropagation()`
times out in 500ms; on slower CI runners only the Model row makes it
into the snapshot inside that window, while the ProviderKey row the
Model references arrives a beat later — long enough for the chat call
to look up `provider_key_id` and miss.

waitConfigPropagation now accepts an optional `condition` callback
that polls a positive readiness probe on a 50ms cadence with a 5s
deadline. The smoke test uses two such probes:

- After the Admin writes, poll /v1/models for the Model id (covers the
  Model row's propagation as before).
- Before the chat assertion, poll the chat path itself, retrying as
  long as the response carries the `unknown provider_key_id` config
  error. That's the only signal that captures the *complete* snapshot
  state (Model + ProviderKey + ApiKey), since the proxy doesn't
  expose ProviderKey directly.

The upstream-was-hit assertion still passes because both probe and
the real call land on `/v1/chat/completions`.

Local repro stays green; CI now has 5s of headroom for the second-
event race instead of the old 0ms past the fixed sleep.
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.

2 participants