Skip to content

v0.6.0 — Identity-based access control

Choose a tag to compare

@github-actions github-actions released this 29 Jul 06:46
c78f1a1

🚀 Get started

New to AISIX? Get the gateway running and route your first LLM request in minutes:

📦 Download

Pull the container image from the GitHub Container Registry:

docker pull ghcr.io/api7/aisix:0.6.0

Rolling 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 modelretries moves 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-beta or 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 — exp and aud required, iss pinned to a matched enabled provider with no catch-all path, plus optional required_scopes and bound_claims that 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 | all with allow and deny pattern lists. all covers current and future tools and is a deliberate mode, never a default (#822)
  • mcp_access on an API keyinherit takes the inherited grant unchanged, restrict intersects the key's own patterns on top, deny grants 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_access keeps its allowed_tools behavior 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 modesGET /v1/videos/:id/content now 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

  • retries is 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_headers values 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}, and provider_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_headers is 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 the x-aisix-* and x-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/videos and the files, batches, and fine-tuning surface applied no default_headers at 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 content and the text of its content_blocks. The two are independent wire fields and the bridges forward content_blocks upstream when it is present, so benign text in content with a payload in content_blocks previously slipped past every input guardrail while the model still received it (#815)

🔭 Observability

  • A failed request's access log line now carries error_kind and error. error_kind is a stable token to filter or alert on without parsing prose; error is the reason. Success lines carry neither, so error_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

Full Changelog: v0.5.0...v0.6.0