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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@moonmingmoonming commented May 7, 2026

Copy link
Copy Markdown
Member

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)
CopilotAI review requested due to automatic review settings May 7, 2026 01:32
@coderabbitai

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

CopilotAI 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
FileDescription
README.mdUpdates top-level feature list to mention provider keys and other admin-managed resources.
docs/architecture.mdUpdates architecture vocabulary and snapshot table listing to ProviderKey and removes Team.
docs/api-admin.mdRenames credentials section to provider keys and removes standalone budgets/teams collection docs.
config.example.yamlUpdates comments describing which entities live in etcd and are managed via Admin API.
crates/aisix-etcd/src/loader.rsSwitches snapshot loading from credentialsprovider_keys and removes legacy kind handling.
crates/aisix-etcd/src/key.rsUpdates docs describing etcd key “kind” segments.
crates/aisix-core/src/snapshot.rsUpdates module docs to reflect the snapshot shape living in models::AisixSnapshot.
crates/aisix-core/src/resource.rsUpdates docs to replace Team/Budget/Credential examples with ProviderKey.
crates/aisix-core/src/models/mod.rsRemoves credential/team modules and exports; adds provider_key.
crates/aisix-core/src/models/schema.rsRemoves credential/team schemas; adds provider key schema + validator.
crates/aisix-core/src/models/snapshot.rsReplaces credentials/teams tables with provider_keys.
crates/aisix-core/src/models/provider_key.rsIntroduces ProviderKey entity and Resource impl.
crates/aisix-core/src/models/credential.rsDeletes old Credential entity.
crates/aisix-core/src/models/team.rsDeletes old Team entity.
crates/aisix-core/src/lib.rsUpdates re-exports from Credential/Team to ProviderKey.
crates/aisix-admin/src/lib.rsReplaces credentials/teams routes with provider_keys routes; removes team tests.
crates/aisix-admin/src/provider_keys_handlers.rsAdds CRUD handlers for /admin/v1/provider_keys.
crates/aisix-admin/src/store.rsUpdates ConfigStore/InMemoryStore CRUD from credentials/teams to provider keys only.
crates/aisix-admin/src/etcd_store.rsUpdates etcd-backed store to use provider_keys subkey and remove credentials/teams.
crates/aisix-admin/src/credentials_handlers.rsDeletes old credentials handlers.
crates/aisix-admin/src/teams_handlers.rsDeletes old teams handlers.

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

Comment threaddocs/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 threaddocs/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 threadREADME.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 threaddocs/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
CopilotAI 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.

CopilotAI 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 threadREADME.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 threadREADME.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 threaddocs/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 threadREADME.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 mainMay 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 freeto 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

@moonming