v0.6.0 — Identity-based access control
🚀 Get started
New to AISIX? Get the gateway running and route your first LLM request in minutes:
- 📖 Documentation — https://docs.api7.ai/ai-gateway/
- ⚡ Self-hosted quickstart (gateway + etcd via Docker Compose) — https://docs.api7.ai/ai-gateway/getting-started/self-hosted-quickstart
📦 Download
Pull the container image from the GitHub Container Registry:
docker pull ghcr.io/api7/aisix:0.6.0Rolling tags (0.6, latest) and the docker.io/api7/aisix mirror for private/offline deployments are listed on the package page.
AISIX takes the caller's identity from the IdP, and decides what that caller may reach. A request can now authenticate with a JWT issued by an OIDC provider instead of a gateway API key, and MCP tool access resolves through a layered policy — an environment default, a team entitlement, and per-key narrowing — instead of a flat allowlist edited key by key. Alongside them: retry budgets that live on the model and apply on every endpoint, operator-templated upstream headers, and video downloads that never expose a provider credential.
- Inbound OIDC/JWT authentication — a request may carry an IdP-issued JWT instead of a gateway key; the gateway validates it against a per-environment trust list and resolves it to the bound caller identity (#825)
- Layered MCP access policies — an environment default, a team entitlement, and per-key narrowing resolve into one effective tool ACL, so a fleet of keys no longer needs per-key editing (#822)
- Retry budgets on the model —
retriesmoves onto the model and applies on every endpoint, not just the chat path (#816) - Request-context variables in upstream headers —
"x-tenant-id": "${request.api_key.team_id}"renders per request, so an internal model service can attribute traffic without a credential per tenant (#835) - Client-header forwarding allowlist — named inbound headers such as
anthropic-betaor a trace context relay upstream on the standard endpoints (#835) - Video downloads without credential exposure — a per-provider content-delivery mode streams the bytes through the gateway, with OpenAI Sora as its first consumer and Runway added as the fourth provider (#820, #817)
⚠️ Breaking changes
Retries are on by default. Before this release, a model group that did not set routing.retries performed zero retries. The budget now resolves model → group → a new deployment-wide upstream.retries default, and that default is 2. A deployment that never configured retries starts retrying after upgrade. Each retry re-sends the full request body and stacks on top of whatever the provider's own edge already retries, so a struggling upstream sees more load rather than less. To keep the previous behavior, set upstream.retries: 0 in the gateway config (#816).
🔐 Authentication
oidc_providers— per-environment trust rows pinning an issuer, its audiences, and its JWKS URI, with OIDC discovery as the fallback. A bearer is treated as a JWT only when it has three segments and the first decodes to a JOSE header, so dotted custom-imported keys keep working (#825)- Validation is default-deny throughout: asymmetric algorithms only — HMAC is excluded so a public JWKS can never double as a shared signing secret —
expandaudrequired,isspinned to a matched enabled provider with no catch-all path, plus optionalrequired_scopesandbound_claimsthat traverse nested claims by dot-path (#825) - An API key binds to a
(jwt_provider, jwt_subject)pair, so an IdP identity carries that key's budgets, rate limits, and access rules. A subject is only ever resolved for the trust provider named on the key, so a second trusted issuer cannot assert someone else's subject (#825) - The JWT path sits at the single auth choke point, so every proxy surface inherits it — chat, completions, messages, responses, embeddings, rerank, audio, images, videos, files, batches, fine-tuning,
/mcp,/a2a, realtime WebSocket subprotocol auth, and passthrough. Once a real JWT enters the path there is no fallback to key lookup (#825)
🧩 MCP
mcp_policies— a resource kind carrying one environment-default row plus one row per team:mode: none | selected | allwithallowanddenypattern lists.allcovers current and future tools and is a deliberate mode, never a default (#822)mcp_accesson an API key —inherittakes the inherited grant unchanged,restrictintersects the key's own patterns on top,denygrants nothing. Narrowing only: a key can never widen what it inherited (#822)- Deny is a global union — a deny pattern from the environment policy, from the key's team policy, or from the key itself always subtracts, so an environment-level deny survives a team policy takeover and still applies to keys that predate the feature (#822)
- A key with no
mcp_accesskeeps itsallowed_toolsbehavior exactly as before, and a policy can only narrow it — upgrading cannot silently grant access (#822) - Policy expiry was dropped before release; a disabled policy neither grants nor denies (#826)
🎬 Video generation
- Content-delivery modes —
GET /v1/videos/:id/contentnow dispatches per provider. Providers that return signed, credential-free URLs keep the existing 302 redirect and cost no relay bandwidth; a provider whose download requires its own credential is proxied instead, with the gateway injecting the bearer so the credential never reaches the client (#820) - The proxied path streams rather than buffers, so memory stays constant regardless of file size, and a non-2xx upstream is returned as a JSON error envelope instead of being streamed back labelled
video/mp4(#820) - OpenAI Sora is the first proxied provider (#820); Runway — the Gen family plus Runway-hosted Veo — is the fourth mapped provider (#817)
🧭 Routing and traffic controls
retriesis a model field, resolving model → group → deployment default, and applies across every endpoint rather than only the chat path. See the breaking-change note above for the default (#816)downstream.idle_timeout_secs— bounds how long an accepted connection may sit idle between requests. An in-flight request or SSE stream is never interrupted, however long it runs. It defaults to 0, meaning the gateway never closes first: closing first is what hands the node in front a connection it still believes is usable. If you set it, keep it above the pool idle timeout of whatever sits in front (#818)downstream.sse_keepalive_interval_secs— heartbeat comments on a streaming response that has not yet produced anything, so a proxy in front does not treat a model slow to its first token as an abandoned connection. Defaults to 15 seconds (#818)
🔌 Upstream requests
default_headersvalues on a provider key may reference a closed vocabulary of request-context variables:request.id,request.api_key.{id,name,team_id,user_id},model.{id,name}, andprovider_key.{id,name}. No secret is reachable — the plaintext bearer and the provider credential are simply not in the variable context. A header whose variables do not all resolve is dropped rather than sent blank, since an empty value reads upstream as a real answer (#835)forward_client_headersis an operator allowlist of exact header names or single-*globs, matched case-insensitively and empty by default, so every endpoint keeps its current behavior until it is set. Auth headers, the transport family, and thex-aisix-*andx-stainless-*prefixes are refused from a client even under a*allowlist (#835)- Precedence is gateway > operator > client, and it is structural: each caller inserts its own headers first and nothing in the pipeline overwrites a name already present (#835)
/v1/videosand the files, batches, and fine-tuning surface applied nodefault_headersat all — a gap in that handler family. Both now carry the resolved set on every round-trip (#835)
🛡️ Guardrails
- Input guardrails scan the union of a message's
contentand the text of itscontent_blocks. The two are independent wire fields and the bridges forwardcontent_blocksupstream when it is present, so benign text incontentwith a payload incontent_blockspreviously slipped past every input guardrail while the model still received it (#815)
🔭 Observability
- A failed request's access log line now carries
error_kindanderror.error_kindis a stable token to filter or alert on without parsing prose;erroris the reason. Success lines carry neither, soerror_kind=means something failed and nothing else. Previously a 502, a DNS failure, a reaped pooled connection, and an unanswered SYN all produced an identical line (#819)
What's Changed
- feat(videos): Runway (Gen/Veo) adapter on /v1/videos by @moonming in #817
- fix(guardrails): scan the union of content and content_blocks by @moonming in #815
- fix(observability): name the failure on the access log line by @jarvis9443 in #819
- feat(downstream): configurable idle timeout and SSE heartbeat on the inbound side by @jarvis9443 in #818
- feat(videos): content-streaming proxy + OpenAI Sora as first consumer by @moonming in #820
- feat(retries): move the retry budget onto the model and apply it everywhere by @jarvis9443 in #816
- feat(mcp): layered MCP access policies — env default, team entitlement, key narrowing by @jarvis9443 in #822
- test(e2e): de-flake the downstream idle-timeout suite's stale-pool race by @jarvis9443 in #824
- feat(auth): inbound OIDC/JWT authentication — trust providers + jwt_subject key binding by @jarvis9443 in #825
- revert(mcp): drop policy expiry (expires_at) from mcp_policies by @jarvis9443 in #826
- build(deps): bump yaml from 2.5.1 to 2.8.3 in /tests/e2e by @dependabot[bot] in #827
- build(deps): bump quinn-proto from 0.11.14 to 0.11.16 by @dependabot[bot] in #833
- build(deps): bump serde_with from 3.18.0 to 3.21.0 by @dependabot[bot] in #832
- build(deps): bump cmov from 0.5.3 to 0.5.4 by @dependabot[bot] in #831
- build(deps-dev): bump vitest from 2.1.9 to 3.2.6 in /tests/e2e by @dependabot[bot] in #829
- build(deps): bump undici from 6.19.8 to 6.27.0 in /tests/e2e by @dependabot[bot] in #828
- build(deps-dev): bump ws from 8.18.0 to 8.21.0 in /tests/e2e by @dependabot[bot] in #830
- feat(headers): request-context variables in default_headers + client-header forwarding allowlist by @jarvis9443 in #835
- ci(e2e): install with pnpm 11, matching the version developers run by @jarvis9443 in #836
- ci(e2e): run the harness on Node 22, which pnpm 11 requires by @jarvis9443 in #837
New Contributors
- @dependabot[bot] made their first contribution in #827
Full Changelog: v0.5.0...v0.6.0