Skip to content

PR #1: Scaffold Cargo workspace, UI skeleton, and CI pipeline - #1

Merged
moonming merged 4 commits into
mainfrom
scaffold/workspace
Apr 17, 2026
Merged

PR #1: Scaffold Cargo workspace, UI skeleton, and CI pipeline#1
moonming merged 4 commits into
mainfrom
scaffold/workspace

Conversation

@moonming

Copy link
Copy Markdown
Collaborator

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.toml workspace with 14 member crates mirroring the plan's module boundaries:
    • aisix-core, aisix-etcd, aisix-gateway, aisix-proxy, aisix-admin
    • aisix-obs, aisix-ratelimit, aisix-cache, aisix-guardrails, aisix-server
    • aisix-provider-{openai,anthropic,gemini,deepseek}
  • Pinned [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.toml pinning stable + rustfmt, clippy, llvm-tools-preview
  • Release profile: lto = "thin", codegen-units = 1, strip = "symbols"; dedicated coverage profile for instrumented builds

UI

  • ui/ — Vite 5 + React 18 + TS 5 + Tailwind 3 + TanStack Router/Query + Monaco + i18next + next-themes (+ openai JS SDK for the playground)
  • Build output goes to crates/aisix-admin/ui-dist so rust-embed in aisix-admin bakes the SPA into the single binary

CI (.github/workflows/ci.yml)

  • lintcargo fmt --check, cargo clippy -D warnings, pnpm install/typecheck
  • rust-unitcargo-llvm-cov LCOV output
  • build-ui — produces ui-dist artifact for both local and CI binary builds
  • build-bin — builds aisix with RUSTFLAGS="-C instrument-coverage" so E2E runs generate .profraw
  • e2e — 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 commands
  • config.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.toml

Verified locally

  • cargo check --workspace ✅ (all 14 crates compile on stable)
  • Scaffold only — each crate src/lib.rs is a single-file module with a doc comment plus #![forbid(unsafe_code)] / #![deny(rust_2018_idioms)]

What's intentionally NOT here

Deferred to Scope
PR #2 aisix-core — Config types, ResourceEntry<T> (Deref), Snapshot (ArcSwap), ProxyError / AdminError taxonomies
PR #3 aisix-etcdConfigProvider trait + watch supervisor (5×5s connect retry, 1→60s exp-backoff reconnect, compaction → Resync)
PR #4 Data models — Model, ApiKey, Team, Budget, Credential, Guardrail, FallbackPolicy + JSON Schema 2020-12 validators
PR #5 aisix-server bootstrap — the 10-step startup sequence from spec §1, graceful shutdown, TLS, and the E2E harness lighting up App.spawn()

Test plan

  • cargo check --workspace on macOS (Rust stable)
  • CI runs green on all 6 jobs (this PR validates the pipeline itself)
  • Coverage-gate artifact is uploaded (no threshold enforcement yet — soft pass)
  • cargo build -p aisix-server produces a runnable binary that prints the scaffold placeholder
  • pnpm -C ui install && pnpm -C ui build produces crates/aisix-admin/ui-dist

Future PRs will replace the placeholder main.rs with the real bootstrap and start filling in the ~285 E2E tests defined in the plan's §4.1–§4.16.

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.
Copilot AI review requested due to automatic review settings April 17, 2026 03:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

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 aisix binary entrypoint.
  • Added ui/ Vite + React + TS + Tailwind skeleton, building into crates/aisix-admin/ui-dist for 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.

Comment thread ui/index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/aisix.svg" />

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread ui/index.html
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread ui/package.json
Comment on lines +9 to +11
"preview": "vite preview",
"lint": "eslint .",
"typecheck": "tsc -b --noEmit"

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread config.example.yaml
Comment on lines +24 to +25
# - "${AISIX_ADMIN_KEY}"
- "admin-local-only-change-me"

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
# - "${AISIX_ADMIN_KEY}"
- "admin-local-only-change-me"
- "${AISIX_ADMIN_KEY}"

Copilot uses AI. Check for mistakes.
Comment thread Cargo.toml
# Runtime
tokio = { version = "1.41", features = ["full"] }
tokio-stream = "0.1"
tokio-util = "0.7"

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
tokio-util = "0.7"
tokio-util = "=0.7.0"

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml Outdated
Comment on lines +34 to +35
cache: pnpm
cache-dependency-path: ui/pnpm-lock.yaml

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
cache: pnpm
cache-dependency-path: ui/pnpm-lock.yaml

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +47 to +51
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +96 to +99
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread ui/vite.config.ts
Comment on lines +9 to +13
build: {
outDir: "../crates/aisix-admin/ui-dist",
emptyOutDir: true,
sourcemap: true,
},

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines +18 to +22
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.
The tests/e2e harness arrives with PR #2+; until then the e2e job
has nothing to run. Skip install/run steps when package.json is
absent, and mark the whole job continue-on-error so the overall PR
status stays green. Flip back to hard at PR #5.
@moonming
moonming merged commit 929f67a into main Apr 17, 2026
6 checks passed
@moonming
moonming deleted the scaffold/workspace branch April 17, 2026 04:34
moonming added a commit that referenced this pull request May 7, 2026
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.
moonming added a commit that referenced this pull request May 7, 2026
…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.
moonming added a commit that referenced this pull request May 9, 2026
…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.
moonming added a commit that referenced this pull request May 10, 2026
…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.
moonming added a commit that referenced this pull request May 17, 2026
…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).
moonming added a commit that referenced this pull request May 17, 2026
`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).
moonming added a commit that referenced this pull request May 17, 2026
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).
moonming added a commit that referenced this pull request May 17, 2026
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).
moonming added a commit that referenced this pull request May 17, 2026
`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).
moonming added a commit that referenced this pull request May 17, 2026
`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).
moonming added a commit that referenced this pull request May 18, 2026
…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>
moonming added a commit that referenced this pull request May 22, 2026
…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.
moonming added a commit that referenced this pull request May 22, 2026
…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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants