chore(admin): rename Credential→ProviderKey, drop Team — align with AISIX-Cloud naming - #95
Conversation
…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)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughReplaces 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). ChangesProviderKey integration & removal of Team/Credential
DP v3 registration & server helpers
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Note 🎁 Summarized by CodeRabbit FreeYour 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 |
There was a problem hiding this comment.
Pull request overview
This PR realigns the standalone Admin API and underlying etcd resource vocabulary with AISIX-Cloud naming by renaming Credential → ProviderKey, removing the standalone Team entity, and updating docs/config accordingly to reflect the streamlined standalone surface.
Changes:
- Rename the upstream-secret resource from
CredentialtoProviderKey(fields and etcd/admin route prefixes updated accordingly). - Remove the
Teamentity 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 credentials → provider_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.
| 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. |
| ### 4.3 ProviderKeys — `/admin/v1/provider_keys` | ||
|
|
| //! (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. |
| - **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 |
| - Operator-driven configuration. Models, ApiKeys, ProviderKeys, Guardrails, | ||
| CachePolicies, and ObservabilityExporters all live in etcd and are | ||
| mutated via the Admin API at runtime. |
| "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); | ||
| } | ||
| } |
| 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)) | ||
| } |
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
…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.
There was a problem hiding this comment.
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/spendendpoint backed byBudgetTracker, 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.
| - **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 |
| | 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 | |
| - Operator-driven configuration. Models, ApiKeys, ProviderKeys, Guardrails, | ||
| CachePolicies, and ObservabilityExporters all live in etcd and are | ||
| mutated via the Admin API at runtime. |
| 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",) |
| - **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. | ||
|
|
…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)
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.
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.
…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.
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 asCredentialunder/admin/v1/credentials. Same shape, different name — confusing for anyone reading dashboard + standalone docs side by side, and pinned cp-api'smustMarshalModelKVto a denormalised projection (see the comment aroundinternal/cpapi/resources/handlers.go:855).This PR realigns the standalone surface to AISIX-Cloud's vocabulary so the next phase (
Model.provider_config→provider_key_idreference) can land cleanly on top.Renames
Credential→ProviderKeyname→display_nameapi_key→secret/credentials/→/provider_keys//admin/v1/credentials→/admin/v1/provider_keysvalidate_credential→validate_provider_keyDeletions
Teamis 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 viaApiKey.max_budget_usdand per-key rate-limiting viaApiKey.rate_limit. cp-api's Team table stays untouched.Out of scope (deferred to follow-up PR)
{name, model, provider_config}→{display_name, model_name, provider_key_id}) — touches all five provider bridges, the proxy hot path, and cp-api'smustMarshalModelKV(cross-repo coordination). Cleaner as its own reviewable diff.ApiKey.display_namefield — same follow-up.Test plan
cargo check --workspace --testscleancargo clippy --workspace --tests -- -D warningscleancargo test --workspacegreen (full suite, ~600 tests)display_name,secret)docs/api-admin.md+docs/architecture.md+README.md+config.example.yamlreflect the new namingSummary by CodeRabbit
New Features
Documentation
Removals