PR #1: Scaffold Cargo workspace, UI skeleton, and CI pipeline - #1
Conversation
Bootstraps the aisix AI gateway repo layout so subsequent PRs can land vertical feature slices without repeated plumbing churn. - Cargo workspace with 14 member crates (core / etcd / gateway / proxy / admin / obs / ratelimit / cache / guardrails / server / four provider crates) and pinned `[workspace.dependencies]` - Rust toolchain pinned via `rust-toolchain.toml`; `rustfmt.toml`, `.editorconfig`, MIT LICENSE - UI skeleton (Vite + React 18 + TS 5 + Tailwind 3) that builds into `crates/aisix-admin/ui-dist` for rust-embed - GitHub Actions CI: lint, rust unit+coverage, build-ui, build-bin (instrumented), e2e (etcd+redis services), coverage-gate (>= 90%, soft during scaffold, hardened at PR #5) - `config.example.yaml` matching spec §1/§2 bootstrap surface - `aisix-server` main.rs is a placeholder — full 10-step startup arrives in PR #5 `cargo check --workspace` passes.
There was a problem hiding this comment.
Pull request overview
Bootstraps the aisix repository with an initial Rust workspace layout, a minimal Admin UI (Vite/React/Tailwind) skeleton, and a GitHub Actions CI pipeline to lint/build and begin collecting coverage artifacts.
Changes:
- Added a Cargo workspace with multiple scaffold crates and a placeholder
aisixbinary entrypoint. - Added
ui/Vite + React + TS + Tailwind skeleton, building intocrates/aisix-admin/ui-distfor embedding. - Added CI workflow for fmt/clippy, Rust unit coverage, UI build, instrumented binary build, and a (currently soft) coverage gate.
Reviewed changes
Copilot reviewed 44 out of 45 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/vite.config.ts | Vite config for /ui/ base, build output path, and dev proxies. |
| ui/tsconfig.json | Strict TS config and path alias scaffold. |
| ui/tailwind.config.js | Tailwind content scan + dark mode scaffold. |
| ui/src/main.tsx | React entrypoint mounting App. |
| ui/src/index.css | Tailwind CSS directives. |
| ui/src/App.tsx | Placeholder “scaffold” landing page. |
| ui/postcss.config.js | Tailwind + autoprefixer wiring. |
| ui/package.json | UI dependencies and scripts scaffold. |
| ui/index.html | SPA HTML shell. |
| ui/.gitignore | UI-local ignore rules. |
| rustfmt.toml | Workspace rustfmt settings. |
| rust-toolchain.toml | Rust toolchain pin + components. |
| crates/aisix-server/src/main.rs | Placeholder aisix binary entrypoint. |
| crates/aisix-server/Cargo.toml | Binary crate manifest + initial deps. |
| crates/aisix-ratelimit/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-ratelimit/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-proxy/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-proxy/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-provider-openai/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-provider-openai/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-provider-gemini/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-provider-gemini/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-provider-deepseek/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-provider-deepseek/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-provider-anthropic/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-provider-anthropic/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-obs/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-obs/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-guardrails/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-guardrails/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-gateway/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-gateway/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-etcd/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-etcd/Cargo.toml | Crate manifest scaffold. |
| crates/aisix-core/src/lib.rs | Core crate contract doc + lint attributes. |
| crates/aisix-core/Cargo.toml | Core crate manifest scaffold. |
| crates/aisix-cache/src/lib.rs | Crate stub + lint attributes. |
| crates/aisix-cache/Cargo.toml | Cache crate manifest + feature flags scaffold. |
| crates/aisix-admin/src/lib.rs | Admin crate stub + lint attributes. |
| crates/aisix-admin/Cargo.toml | Admin crate manifest including rust-embed. |
| config.example.yaml | Example bootstrap configuration scaffold. |
| Cargo.toml | Workspace members, shared deps, and build profiles. |
| Cargo.lock | Initial dependency lockfile for the workspace. |
| .github/workflows/ci.yml | CI pipeline for lint/build/coverage artifacts. |
| .editorconfig | Editor defaults for Rust + UI formatting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/aisix.svg" /> |
There was a problem hiding this comment.
With base: "/ui/" configured in Vite, using a root-absolute favicon URL (/aisix.svg) can break when the SPA is served under /ui/ (it will resolve against /). Prefer a base-aware/relative URL so the embedded UI works under the /ui/ mount point.
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> |
There was a problem hiding this comment.
With base: "/ui/" configured in Vite, the root-absolute entrypoint (src="/src/main.tsx") can resolve incorrectly when the UI is hosted under /ui/. Prefer Vite’s recommended index.html entrypoint pattern that remains correct under a non-root base.
| "preview": "vite preview", | ||
| "lint": "eslint .", | ||
| "typecheck": "tsc -b --noEmit" |
There was a problem hiding this comment.
eslint is listed and a lint script is defined, but there is no ESLint configuration checked in (ESLint v9 requires an explicit flat config). As-is, pnpm lint will fail with “couldn't find a configuration file”. Either add a minimal eslint.config.js (or equivalent) or remove/disable the lint script until config lands.
| # - "${AISIX_ADMIN_KEY}" | ||
| - "admin-local-only-change-me" |
There was a problem hiding this comment.
The example config includes a hard-coded admin key (admin-local-only-change-me). Even in an example file, shipping a working default secret encourages insecure deployments if someone copies it verbatim. Prefer requiring an env-provided value (or a clearly nonfunctional placeholder) so a real secret must be set explicitly.
| # - "${AISIX_ADMIN_KEY}" | |
| - "admin-local-only-change-me" | |
| - "${AISIX_ADMIN_KEY}" |
| # Runtime | ||
| tokio = { version = "1.41", features = ["full"] } | ||
| tokio-stream = "0.1" | ||
| tokio-util = "0.7" |
There was a problem hiding this comment.
The workspace dependency versions (e.g. tokio = { version = "1.41" }) use Cargo’s default caret semantics, so they’ll float to newer compatible 1.x releases (as reflected in Cargo.lock). If the goal is to keep version drift highly visible and truly pinned, consider using exact versions (e.g. =1.41.0) or another explicit pinning strategy.
| tokio-util = "0.7" | |
| tokio-util = "=0.7.0" |
| cache: pnpm | ||
| cache-dependency-path: ui/pnpm-lock.yaml |
There was a problem hiding this comment.
actions/setup-node is configured with cache: pnpm and cache-dependency-path: ui/pnpm-lock.yaml, but ui/pnpm-lock.yaml is not present in this PR. With caching enabled, setup-node can fail when it can’t find the lockfile. Either commit the generated lockfile (recommended) or remove the pnpm cache settings until it exists.
| cache: pnpm | |
| cache-dependency-path: ui/pnpm-lock.yaml |
| - uses: actions/checkout@v4 | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| components: llvm-tools-preview | ||
| - uses: Swatinem/rust-cache@v2 |
There was a problem hiding this comment.
This job installs dtolnay/rust-toolchain@stable, which ignores the repo’s rust-toolchain.toml pin. Consider switching CI to use the toolchain file (or explicitly installing the pinned version) to keep CI and local dev consistent.
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| components: llvm-tools-preview | ||
| - uses: Swatinem/rust-cache@v2 |
There was a problem hiding this comment.
This job installs dtolnay/rust-toolchain@stable, which ignores the repo’s rust-toolchain.toml pin. Consider using the toolchain file (or pinning the same version) to prevent CI/local divergence when the pinned toolchain changes.
| build: { | ||
| outDir: "../crates/aisix-admin/ui-dist", | ||
| emptyOutDir: true, | ||
| sourcemap: true, | ||
| }, |
There was a problem hiding this comment.
build.sourcemap: true will emit source maps into ui-dist, which (per the repo setup) is embedded into the released binary. That can unintentionally ship the full UI source to end users. Consider disabling sourcemaps for production builds (or gating behind an env flag) and only enabling them for local debugging when needed.
| - uses: actions/checkout@v4 | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| components: rustfmt, clippy | ||
| - uses: Swatinem/rust-cache@v2 |
There was a problem hiding this comment.
This job installs dtolnay/rust-toolchain@stable, which ignores the repo’s rust-toolchain.toml pin. To avoid local-vs-CI drift (fmt/clippy behavior, MSRV), consider installing from the toolchain file (or explicitly pinning the same version) in CI.
etcd-client 0.14 and opentelemetry-otlp use prost-build; both require protoc at build time. The ubuntu-latest runners don't ship it, so the rust jobs were failing with 'Could not find protoc'. Added arduino/setup-protoc@v3 to lint, rust-unit, and build-bin.
The scaffold PR doesn't ship a lockfile (it's generated at first install), so setup-node@v4's cache-dependency-path lookup 404s and kills the lint job. Re-add the cache config once PR #11 commits a lockfile.
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.
…ISIX-Cloud naming (#95) * chore(admin): rename Credential→ProviderKey, drop Team — align with AISIX-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) * ci: fix cargo fmt --check drift on main (cert_bundle / main / register) 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 * docs(readme): document shipped features, CP/DP differences, roadmap 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. * docs: drop competitor name-drops in README + architecture intro 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 * docs(readme): reshuffle roadmap — Bedrock guardrails P1→P0, distributed 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.
…ample Audit on PR #181 returned BLOCK with 3 HIGH and 3 MEDIUM. Address the verified ones (one HIGH was based on a stale workflow comment — the published image really is `ghcr.io/api7/ai-gateway`): H2: Cert-bundle docker example was missing `CP_BASE_URL`. Boot would fail at `crates/aisix-server/src/main.rs:154` ("managed.cp_base_url required when cert bundle is provided") — cert-bundle path skips `/dp/register` but the heartbeat worker still needs the CP origin. Added the env var and rewrote the "what each flag does" entry to clarify the heartbeat dependency. H3: `CP_ETCD_ENDPOINT` value had `https://` prefix; `main.rs:173` formats it as `format!("https://{cp_etcd}")`, so `https://https://...` would have been the actual endpoint. Strip the scheme; doc-comment on `cfg.managed.cp_etcd_endpoint` (`config.rs:161`) already requires bare host:port. M1: Env-var table row for `CP_ETCD_ENDPOINT` was inverted. Code: register path strict-requires it (main.rs:213); cert-bundle path falls back to `cp_base_url`'s host:port (main.rs:161-166). Swap the description. M2: "Verifying" log block fabricated a `gateway_id=aigg_xxx` field the code never emits (main.rs:167-171 for cert-bundle, register.rs for register path — both write `dp_id`, `env_id`, `etcd`). Replace with the actual emitted fields and split into cert-bundle vs register branches so operators can match either. M3: Restart-semantics step #1 implied cert-bundle env vars trump bundle-on-disk; main.rs gates cert-bundle on `if !bundle_on_disk && bundle_provided` (line 130). Reword to "no bundle on disk yet" so the precedence reads bundle-on-disk → cert-bundle → register.
…ag (#181) * docs(managed-mode): document cert-bundle bootstrap path + fix image tag The README's "what's shipped" headline calls out the cert-bundle bootstrap path (no `/dp/register` round-trip) as a first-class managed-mode option, but `docs/managed-mode.md` had zero coverage of it. An operator following the doc could only set up the register-based path, missing the recommended one entirely. This PR fills in the gap. Source-of-truth: `crates/aisix-server/src/cert_bundle.rs` and `crates/aisix-core/src/config.rs`. Changes: 1. New "First boot — pre-provisioned cert bundle (recommended)" section. Covers the three required PEMs, both inline and file-path env-var variants, and how `env_id` + `dp_id` are parsed out of the leaf cert's URI SAN to scope etcd reads to `/aisix/<env_id>/`. 2. Re-titled the existing "First boot — register against the control plane" to "Alternative: register against the control plane" so the recommended path leads. 3. Added `AISIX_MANAGED__CP_ETCD_ENDPOINT`, `AISIX_MANAGED__CP_CA_CERT_FILE`, and the six cert-bundle `CP_{CERT,KEY,CA}_{PEM,FILE}` env vars to the override table. 4. Updated "Restart semantics" to handle three first-boot branches (cert-bundle / register / fail-fast) instead of two, and noted that `env_id` is persisted alongside `dp_id` in the bundle dir. 5. Image tag corrected from `ghcr.io/moonming/ai-gateway:main` → `ghcr.io/api7/ai-gateway:main` to match the README and the actual published image. * docs(managed-mode): address PR #181 audit — fix cert-bundle docker example Audit on PR #181 returned BLOCK with 3 HIGH and 3 MEDIUM. Address the verified ones (one HIGH was based on a stale workflow comment — the published image really is `ghcr.io/api7/ai-gateway`): H2: Cert-bundle docker example was missing `CP_BASE_URL`. Boot would fail at `crates/aisix-server/src/main.rs:154` ("managed.cp_base_url required when cert bundle is provided") — cert-bundle path skips `/dp/register` but the heartbeat worker still needs the CP origin. Added the env var and rewrote the "what each flag does" entry to clarify the heartbeat dependency. H3: `CP_ETCD_ENDPOINT` value had `https://` prefix; `main.rs:173` formats it as `format!("https://{cp_etcd}")`, so `https://https://...` would have been the actual endpoint. Strip the scheme; doc-comment on `cfg.managed.cp_etcd_endpoint` (`config.rs:161`) already requires bare host:port. M1: Env-var table row for `CP_ETCD_ENDPOINT` was inverted. Code: register path strict-requires it (main.rs:213); cert-bundle path falls back to `cp_base_url`'s host:port (main.rs:161-166). Swap the description. M2: "Verifying" log block fabricated a `gateway_id=aigg_xxx` field the code never emits (main.rs:167-171 for cert-bundle, register.rs for register path — both write `dp_id`, `env_id`, `etcd`). Replace with the actual emitted fields and split into cert-bundle vs register branches so operators can match either. M3: Restart-semantics step #1 implied cert-bundle env vars trump bundle-on-disk; main.rs gates cert-bundle on `if !bundle_on_disk && bundle_provided` (line 130). Reword to "no bundle on disk yet" so the precedence reads bundle-on-disk → cert-bundle → register.
…ptions Two README clarifications surfaced during independent audit of the canonical-schemas PR: 1. **Naming namespace** — file names use the snake_case singular form of the Rust type (`api_key.schema.json`); the etcd key prefix uses the plural `Resource::kind()` value (`api_keys`). The two conventions are deliberately distinct (per-type artifact vs. collection prefix). Spelling it out keeps downstream tooling authors from assuming one when the other applies. 2. **Forward-compat exceptions** — three resources intentionally omit `additionalProperties: false` in their generated schemas: `guardrail` (serde flatten + tag incompatibility), `cache_policy` (cp-api may ship fields ahead of DP rollout), and `observability_exporter` (same forward-compat reason). Downstream consumers that default to strict validation should know to relax the check on these three. Both notes are documentation-only; the underlying schemas (and the Rust types) are unchanged. Refs #304 (#1).
`merged_openapi` previously parsed and merged the embedded resource schemas on the first `/admin/openapi.json` request. That delayed any panic from a corrupt schema fragment until well after boot — a worse ops failure mode than crashing immediately on startup, especially since the panic is captured by axum's error handling and surfaces as a 500 to whoever happens to hit Scalar first. Move the init call up: `build_router` now calls `openapi::merged_openapi()` once at construction time, before any request can land. The result is cached in the same `OnceLock` so the handler still does a free lookup. Visibility on `merged_openapi` flips from private to `pub(crate)` to make the pre-warm callable from `lib.rs`; no other surface change. Surfaced by independent audit of #310. Refs #304 (#1).
Adds a new `schema-drift` job to the CI workflow that runs `cargo run -p aisix-core --bin dump-schema` and asserts `git diff --exit-code schemas/` is clean. PRs that modify resource struct in `crates/aisix-core/src/models/` but forget to regenerate the schema files now fail CI with a fix instruction in the error message. ## Why Refs #304 item #1. The `dump-schema` tool and `schemas/resources/*.schema.json` files were introduced in #308; without an enforcement mechanism the committed schemas can silently diverge from the Rust types as the resource graph evolves (especially during issue #302 Phase A, which is actively mutating ProviderKey / Model). This job is that enforcement. ## Job placement Sits as a peer to `lint` — fast, independent, no service deps. Runs in parallel with `lint` / `rust-unit` / `build-bin`. Not a `needs:` target of any downstream job, so a drift failure does not block the e2e or coverage signals. ## Verification - Positive path: `cargo run -p aisix-core --bin dump-schema` on the HEAD of this PR succeeds and `git diff --exit-code schemas/` is empty (no drift in tree) - Negative path: locally introduced a synthetic drift by truncating `schemas/resources/api_key.schema.json` to `{}`. `git diff --exit-code schemas/` returned non-zero — the check fires as expected. Reverted with `git checkout schemas/resources/api_key.schema.json`. - YAML parses with `python3 -c "import yaml; yaml.safe_load(open(...))"`. ## Stack Builds on #308 (which adds the binary + initial schemas). Base will switch to `main` once #308 merges. Refs #304 (#1).
The hand-written OpenAPI 3.1 document in `crates/aisix-admin/src/openapi.rs` previously inlined its own copy of every resource schema (`Model`, `ApiKey`, `ProviderKey`, `Guardrail`, `CachePolicy`, `ObservabilityExporter`, `RateLimit`, `Routing`, plus the nested `ModelCost` / `BackgroundModelCheck`). That left three places to keep in sync whenever a resource field changed: the Rust struct, the inline OpenAPI schema, and the cp-api / dashboard side. This PR cuts the duplication. The Rust struct is now the single source of truth; `dump-schema` (PR #308) writes canonical draft-07 JSON Schemas into `schemas/resources/*.schema.json`; CI (PR #309) enforces those files match the structs. This commit: 1. Removes the ten inlined resource schemas from `OPENAPI_JSON_BASE` (the const formerly named `OPENAPI_JSON`). 2. Embeds the eight canonical schema files at compile time via `include_str!` into a new `RESOURCE_SCHEMAS` const. 3. Adds `merged_openapi()` — runs once on first request, parses the base spec, parses each embedded schema, hoists `definitions/*` into top-level `components.schemas`, rewrites `$ref: #/definitions/X` to `$ref: #/components/schemas/X` (JSON Schema draft-07 → OpenAPI 3.1), and caches the result in an `OnceLock<String>`. 4. Changes `openapi_json()` to serve the merged doc instead of the raw `OPENAPI_JSON_BASE`. 5. Updates the three openapi unit tests to parse `merged_openapi()`. ## What this means for `/admin/openapi.json` The served document keeps the same wrapper schemas (`ModelEntry`, `ApiKeyEntry`, `ModelStatusView`, `ModelKind`, `RuntimeStatus`, `SystemTime`, `AdminError`) and gains 16 new top-level component schemas hoisted from the resource definitions (`Adapter`, `BedrockConfig`, `CacheBackend`, `CooldownConfig`, `GuardrailHookPoint`, `KeywordPattern`, `OnAllFilteredPolicy`, `ParamConstraints`, `Provider`, `RequestOverrides`, `ResponseOverrides`, `RoutingStrategy`, `RoutingTarget`, `StreamDoneMarker`, `TelemetryTags`, etc.). The resource schemas themselves are now precise reflections of the Rust types — e.g. `Guardrail` uses a proper `oneOf` discriminator on `kind` instead of the previous flat `additionalProperties: true` hand-wave; `Provider` lists its 6 variants from the actual enum; `Adapter` lists the 5 wire-shape kebab-case values from #302 Phase A. ## Verification - `cargo check -p aisix-admin` clean - `cargo clippy --workspace --all-targets -- -D warnings` clean - `cargo fmt --all -- --check` clean - `cargo test -p aisix-admin --lib` — all 7 openapi tests pass, including the regression test `openapi_apikey_schema_excludes_max_budget_usd` - External validation: parsed the merged doc, collected 43 `$ref` references across 32 distinct targets, all resolve inside `#/components/schemas/*` (0 unresolved) ## Why nested `if let` instead of let-chains Workspace is on `edition = "2021"`. The merge logic uses one level of nesting in two spots; not pretty, but `edition = "2024"` is a separate decision not in this PR's scope. ## Stack Builds on: - #307 (JsonSchema derives on resource structs) - #308 (dump-schema binary + initial schema files) - #309 (CI drift enforcement) Merge order: 307 → 308 → 309 → this PR. Base will switch to `main` once #308 merges. Refs #304 (#1).
`merged_openapi` previously parsed and merged the embedded resource schemas on the first `/admin/openapi.json` request. That delayed any panic from a corrupt schema fragment until well after boot — a worse ops failure mode than crashing immediately on startup, especially since the panic is captured by axum's error handling and surfaces as a 500 to whoever happens to hit Scalar first. Move the init call up: `build_router` now calls `openapi::merged_openapi()` once at construction time, before any request can land. The result is cached in the same `OnceLock` so the handler still does a free lookup. Visibility on `merged_openapi` flips from private to `pub(crate)` to make the pre-warm callable from `lib.rs`; no other surface change. Surfaced by independent audit of #310. Refs #304 (#1).
`crates/aisix-admin/src/openapi.rs` uses `include_str!` to embed
every `schemas/resources/*.schema.json` at compile time. The Docker
release stage previously copied only `Cargo.{toml,lock}`,
`rust-toolchain.toml`, `rustfmt.toml`, and `crates/` — `cargo build`
inside the container therefore failed with eight
"couldn't read .../schemas/resources/*.schema.json" errors.
Adds a `COPY schemas ./schemas` line and an inline comment pinning
the dependency between `include_str!` and the docker context.
Surfaced by CI on PR #310 (build job).
Refs #304 (#1).
…ped reads, redact 5xx message, Vertex content-type guard Five concrete fixes from the Copilot inline review on PR #323. Two stale comments (#3, #4 — already fixed in commit 3) are skipped. **#1+#7 — Azure OpenAI-compatible code preservation.** Azure's envelope omits `error.type` and carries only `error.code`. The bridge previously put the upstream code into `view.kind` and left `view.code` as `None`. For OpenAI-compat tokens Azure inherits unchanged (e.g. `rate_limit_exceeded`), this meant downstream OpenAI clients received `error.type=rate_limit_exceeded` but `error.code=null` — exactly the SDK-retry break issue #322 is about. Fix: - Azure parser populates BOTH `view.kind` AND `view.code` from the upstream `error.code` field. - `render_openai_envelope`'s AzureOpenAI branch now prefers the translation-table-derived code (so explicit Azure tokens like `DeploymentNotFound` → `model_not_found` still win), falling back to `view.code` for OpenAI-compat pass-through. **#2 — Drain the response stream after hitting the cap.** `read_body_capped` previously broke out of the read loop the moment `limit` bytes were buffered. With reqwest/hyper that leaves unread bytes in the response and prevents connection reuse — during a burst of upstream errors the gateway would churn TCP connections instead of recycling the keep-alive pool. Fix: keep iterating the stream, discarding chunks past the cap. Memory stays bounded by `limit`. **#5 — Redact upstream `error.message` on 5xx.** The 5xx branch of `render_bridge_upstream_envelope` was forwarding `BridgeError::UpstreamStatus.message` verbatim — which for OpenAI / Anthropic comes from the parsed upstream `error.message`. Upstream 5xx bodies routinely embed operator-internal detail (engine names, shard ids, queue depth). Fix: on 5xx, emit a canned `"upstream returned {status}"` message; the full upstream body remains in operator logs via tracing. **#6 — Stale "follow-up" comment.** The docstring on `render_bridge_upstream_envelope` claimed cross-wire translation would ship in a follow-up, but it already shipped in commit 2. Rewrite the comment to describe current behaviour (4xx → `error_translate`; 5xx → canned envelope; `Unknown` wire → legacy generic envelope). **#8 — Content-type guard on Vertex (and Azure, while at it).** `capture_upstream_error_http` already gates serde parsing on `Content-Type: application/json` so a 64 KB HTML error page from a fronting WAF doesn't waste CPU on a doomed JSON parse. The Vertex and Azure bridges call serde directly because they need a custom parse path (canned message for redaction) — same guard now applies. Promoted `content_type_is_json` and added a `response_is_json` helper to the gateway's public surface; both bridges call it before `parse_*_error_*`. New tests: - `upstream_openai_5xx_with_json_envelope_collapses_and_redacts_message` pins the 5xx redaction (asserts `engine offline` / `shard 47` / `engine_overloaded` don't reach the customer envelope). - `chat_429_preserves_openai_compatible_code_for_sdk_retry` (Azure) pins that `parsed.code` carries the OpenAI-compat upstream code. - `chat_400_non_json_body_skips_envelope_parse` (Azure) and `chat_gemini_non_json_body_skips_envelope_parse` (Vertex) pin the new content-type guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dor consts Round-3 audit follow-up + scope expansion confirmed by user. Cuts the last remnants of the pre-#302 vendor-enumeration design that the prior clean-cut PR left as soft-deprecated: #1 Provider enum - Delete the enum + its Adapter mapping + the `every_provider_variant_has_as_str_and_adapter` test. - Drop the `Provider` re-export from `aisix_core::{lib, models}`. #2 OpenAiBridge::with_name + `name` field - Drop the builder method and the per-instance `name` field; the bridge is the singular OpenAI family bridge and reports `"openai"` unconditionally. - Same surgery on AnthropicBridge for symmetry (its `with_name` was unused at every call site). #3 Per-vendor default-base consts + match arms - Delete DEEPSEEK_DEFAULT_BASE, GOOGLE_DEFAULT_BASE, COHERE_DEFAULT_BASE plus the 11 long-tail consts (GROQ/MISTRAL/TOGETHERAI/FIREWORKS/ PERPLEXITY/MOONSHOTAI/ALIBABA/ZHIPUAI/BASETEN/HUGGINGFACE/CEREBRAS). - Replace OpenAiBridge::default_base() with the hardcoded OpenAI bare-host fallback inside resolve_base; the safety guard now returns a Config error for any non-`"openai"` vendor whose PK reaches the bridge with an empty api_base, preventing a silent credential leak to api.openai.com. #4 normalize_canonical_deepseek + normalize_canonical_cohere - Delete both helpers + DEEPSEEK_CANONICAL_HOSTS / COHERE_CANONICAL_HOSTS. - normalize_api_base loses its `provider` parameter; the canonical `/v1` synthesis only applies to api.openai.com. Operators paste the documented URL for every other vendor (cp-api populates it from adapter_map's `default_base_url`). Compat shim: build_hub() keeps `register_specialized("openai", …)` + `register_specialized("anthropic", …)` so pre-Phase-A ProviderKeys that carry `provider` but not `adapter` still dispatch through the two-tier lookup. cp-api admits every catalog vendor post-Phase-A with `adapter` populated, so the family bridge above covers them without any specialized entry needed. Tests deleted: every test that exercised the deleted surface (deepseek/cohere canonical-host normalization, gemini/cohere/long-tail with_name vendor-default targeting, with_name x-aisix-bridge header variant). Kept a single non-OpenAI host pass-through test that pins the family bridge's verbatim treatment of any non-openai api_base (corporate proxies, alternative deployments). Doc updates: stale `Provider::default_base_url` / `Provider::Cohere` / `OpenAiBridge::with_name` references in dispatch.rs, rerank.rs, provider_key.rs, azure-openai/lib.rs replaced with the post-clean-cut language. Net: −464 LOC. Workspace cargo test + clippy clean.
…es + sanitize_tag Audit on the previous push found 3 MEDIUM findings. This commit addresses two of them (#1 and #3); #2 (test for snapshot→emit populate path) is covered by the 4 new sanitize_tag unit tests + existing test plan. MEDIUM-1 RESOLVED: my PR-body justification for deferring messages.rs was factually wrong — `emit_anthropic_usage_event` already takes `provider_key_id: &str` (called from 3 sites that already pass it). Adding the same snapshot-lookup-and-populate block as chat.rs is mechanical, no signature change required. Done in messages.rs:921-939. This means /v1/messages flows now also emit the 5 telemetry tag fields, not just /v1/chat/completions. MEDIUM-3 RESOLVED: added `sanitize_tag(s: String) -> String` helper in chat.rs (pub(crate) so messages.rs can re-use). Strips ASCII control characters and caps length at 256 chars. Applied to all 4 operator-defined tag strings emitted by both emit_usage_event and emit_anthropic_usage_event. Defence-in-depth against a malicious operator crafting a label like `"production\ninjected-internal-key: secret"` that, while JSON-escaped on this gateway↔cp-api hop, could forge log lines on downstream consumers that re-stringify the tag. The right place to enforce a strict admission policy is at PK CRUD validation in cp-api — sanitize_tag is a belt-and-suspenders guard, not a replacement. 4 new unit tests in `sanitize_tag_tests`: - empty stays empty - strips \n, \r, \0 - caps at 256 chars - preserves normal ASCII + Unicode (labels in any language) `cargo test -p aisix-proxy --lib` → 287 passed (was 283 + 4 new). `cargo check --workspace` → clean. PR body's "messages.rs deferred" claim was wrong; I've also removed the duplicate `let snap = state.snapshot.load();` in messages.rs that the new block superseded (matching the same dedupe chat.rs got).
Summary
Bootstraps the aisix AI Gateway repo layout so every subsequent vertical feature PR has stable plumbing to build on. No business logic — just the scaffold.
The full plan (tech research, API design, tech selection, E2E test plan, >90% coverage gate, and a per-feature test matrix covering spec §1–§12 and §3.1–§3.8) is captured in `/Users/ming/.claude/plans/distributed-floating-ullman.md` and will be incrementally landed via PR #2+.
What's included
Workspace
Cargo.tomlworkspace with 14 member crates mirroring the plan's module boundaries:aisix-core,aisix-etcd,aisix-gateway,aisix-proxy,aisix-adminaisix-obs,aisix-ratelimit,aisix-cache,aisix-guardrails,aisix-serveraisix-provider-{openai,anthropic,gemini,deepseek}[workspace.dependencies](tokio 1.41, axum 0.7, etcd-client 0.14, arc-swap 1.7, utoipa 5, opentelemetry 0.27, reqwest 0.12, moka 0.12, redis 0.27, and the rest — all dropped into one place so version drift stays visible in PRs)rust-toolchain.tomlpinning stable +rustfmt,clippy,llvm-tools-previewlto = "thin",codegen-units = 1,strip = "symbols"; dedicatedcoverageprofile for instrumented buildsUI
ui/— Vite 5 + React 18 + TS 5 + Tailwind 3 + TanStack Router/Query + Monaco + i18next + next-themes (+openaiJS SDK for the playground)crates/aisix-admin/ui-distsorust-embedinaisix-adminbakes the SPA into the single binaryCI (
.github/workflows/ci.yml)lint—cargo fmt --check,cargo clippy -D warnings, pnpm install/typecheckrust-unit—cargo-llvm-covLCOV outputbuild-ui— producesui-distartifact for both local and CI binary buildsbuild-bin— buildsaisixwithRUSTFLAGS="-C instrument-coverage"so E2E runs generate.profrawe2e— spins etcd + redis service containers, runs the TS harness (landing with PR PR #2: aisix-core — Config, ResourceEntry, Snapshot, error taxonomies #2)coverage-gate— merges unit + E2E LCOV, enforces >= 90% threshold (soft during scaffold milestones, flips to hard fail at PR feat(server): startup sequence + health listeners #5 per the plan's ratchet strategy)Meta
README.md— project pitch, workspace map, dev commandsconfig.example.yaml— bootstrap config per spec §1/§2 (etcd + proxy + admin + observability + cache). No models/keys — those live in etcd.LICENSE— MIT.editorconfig,.gitignore,rustfmt.tomlVerified locally
cargo check --workspace✅ (all 14 crates compile on stable)src/lib.rsis a single-file module with a doc comment plus#![forbid(unsafe_code)]/#![deny(rust_2018_idioms)]What's intentionally NOT here
aisix-core— Config types,ResourceEntry<T>(Deref),Snapshot(ArcSwap),ProxyError/AdminErrortaxonomiesaisix-etcd—ConfigProvidertrait + watch supervisor (5×5s connect retry, 1→60s exp-backoff reconnect, compaction →Resync)Model,ApiKey,Team,Budget,Credential,Guardrail,FallbackPolicy+ JSON Schema 2020-12 validatorsaisix-serverbootstrap — the 10-step startup sequence from spec §1, graceful shutdown, TLS, and the E2E harness lighting upApp.spawn()Test plan
cargo check --workspaceon macOS (Rust stable)cargo build -p aisix-serverproduces a runnable binary that prints the scaffold placeholderpnpm -C ui install && pnpm -C ui buildproducescrates/aisix-admin/ui-distFuture PRs will replace the placeholder
main.rswith the real bootstrap and start filling in the ~285 E2E tests defined in the plan's §4.1–§4.16.