Release Notes
These release notes summarize user-visible changes to the AISIX gateway, control plane, dashboard, and deployment packages. Releases are listed newest first.
When an upgrade skips releases, every intermediate release's Upgrade Notes still apply. Read them oldest first, from the release after the one you are running through the release you are upgrading to. See Upgrade AISIX for supported upgrade paths and the upgrade order.
Release artifacts include the gateway image at docker.io/api7/aisix, control-plane images under docker.io/api7/aisix-cp-*, the aisix-cp Helm chart, and an offline installation package.
1.4.0
Release date: September 22, 2026
A gateway now re-sends a usage batch the control plane failed to answer, so a control-plane outage delays usage instead of losing it. A Redis that is unreachable when the gateway starts no longer holds its listeners closed. An etcd whose dial never completes no longer holds the boot open either. The gateway starts, serves its cached configuration when there is one, and retries in the background. A resource kind the gateway does not know is forward compatibility rather than a failed reload. Logging, the configuration apply, the Prometheus scrape, and upstream DNS no longer contend with request serving. The Dashboard generates only the basic gateway deployment template, and advanced gateway configuration is documented instead. The offline package forwards the CORS allowlist, and the origin allowlists accept the spellings a browser actually sends.
Behavior Changes
-
A rate-limit Redis that is down when the gateway starts no longer stops the gateway. A gateway configured with
ratelimit.backend: redisused to bind no listener at all while that Redis accepted or dropped packets without answering, and the process then exited after several minutes./livezand/metricswere refused for that whole stretch. The gateway now binds its listeners and serves, counting per replica. It logs one warning naming the backend, the Redis host and port, and the connect budget it spent; the configured URL is never logged, because it carries the password. The degradation is restated every five minutes. A background task attaches the shared backend as soon as Redis answers. This is a temporary degradation, never a permanent switch to thememorybackend, and cluster-wide limits are not enforced while it lasts. -
A cache Redis that is down at startup no longer stops the gateway either. The listeners bind, and every
backend: rediscache policy is served as a miss until Redis answers. The warning, the five-minute restatement, and the background attach work the same way. Vector search settles only on a real probe answer, so nothing is published speculatively. While the probe has not passed, a semantic policy logs once and serves exact matches only, with no embedding call and no Redis round trip per request. An operator who wanted the boot to fail on an unreachable cache no longer gets that. -
etcd.dial_timeout_msnow defaults to 5000 ms, where the dial was previously unbounded. Only a deployment that setsetcd.useris affected, because without credentials the dial performs no I/O. The value bounds one connection attempt, and the whole dial gets it once per configured endpoint, so the budget isdial_timeout_ms × max(1, endpoints). An expired dial reports an etcd that could not be reached, and the existing path runs: warn, bind, serve from the snapshot cache, and retry in the background. That snapshot cache is read after the dial, so an unbounded dial also kept unread the one thing that exists for a control-plane outage. Writedial_timeout_ms: 0to keep the old unbounded behavior.request_timeout_msis untouched and still defaults to unbounded. -
A failing Redis cache reports an outage once instead of once per request. The
cache lookup failed,cache write failed, andcache backfill write failedmessages, and their two semantic equivalents, log the first failure of an outage at warning level and the rest at debug level. The next success re-arms them, so a later outage is reported again.aisix_redis_failures_total{operation}is untouched and still counts every failed operation. -
A Redis that answers and refuses the connection settings is told apart from an outage, and does not end the boot. A rejected credential, a
NOAUTHorDENIEDanswer, and adatabasethe server does not have were all reported as an outage. The warning therefore blamed the network for a server that had replied in milliseconds. Such a failure now takes the degraded path under its own name. The boot warning carriesreason=refusedand the driver's error, and the five-minute restatement carries the same word, so one filter finds both. An unreachable backend keepsreason=unreachable. The gateway serves degraded — per-replica counting for the limiter, misses for the cache — and keeps re-attaching. A credential corrected on the Redis side is therefore adopted without restarting the gateway. How specific the error is depends on the driver: a refuseddatabasecarries the server's own text, while a refused credential arrives as a fixed authentication message. -
What still ends the boot is configuration the gateway rejects before any network call. A Redis
urlthe driver cannot parse and TLS material that cannot be read are refused locally, for both the limiter and the cache. So are the block's own checks: a missingurl,nodes,sentinelsormaster_namefor the selected mode, andtimeout_secs: 0. A typo therefore still announces itself at startup. -
The shared Redis
username,password, anddatabasefields now apply in every mode.singlemode, which is the default, previously parsed them and then discarded them. A credential supplied throughpasswordnever reached the handshake, so the gateway loggedconnectedat boot and then failed every Redis operation for the life of the process. The explicit fields also now override a credential embedded in theurl, because a value injected through the environment is no use if a stale one in the URL outranks it. The credential is taken as a pair, so setting either half replaces both.databaseapplies insingleandsentinelmodes; Redis Cluster has only DB 0. A boot warning names the case where a credential is configured in both places at once. -
An unknown resource kind no longer reads as a failed reload. A gateway that reads a document whose
kindits build does not know now reportsaisix_config_last_reload_successful 1andstate: "synced", where it previously reported0and"degraded". The row moves fromrejected[]to the newunknown_kinds[]onGET /status/config, and fromaisix_config_rejected_resourcesto the newaisix_config_unknown_kind_resources{kind}. The two series are disjoint. A genuine rejection still flips the gauge, and the sticky last failure names it. Unknown-kind rows also get their own retention budget, so a flood of them no longer evicts real rejections from/status/configand from the heartbeat. Nothing has to be edited. An alert that countedaisix_config_rejected_resourcesacross all kinds to mean "something is unloadable" now excludes unknown kinds. -
The Metric labels editor is removed from the deploy-a-gateway page. The Dashboard now generates only the basic gateway deployment template: the mTLS bundle,
AISIX_CONFIG_PATH, the dp-manager endpoint, and the proxy port.observability.metrics.labelsis still a supported gateway startup setting, and it is now configured from the documentation rather than from a console form. The panel held no server-side state, so a gateway already running from a previously generated snippet is unaffected. -
A usage batch that fails is re-sent rather than dropped. Against a 1.4.0 control plane, a gateway re-sends the same batch unchanged, under the same batch id. It stops when the batch is accepted, or when the batch's oldest event is 30 minutes old. It also gives up after eight consecutive failures that carried a response. A control-plane outage therefore delays usage rather than losing it, and usage can appear in the console minutes late. Delivery of later batches waits behind the batch being re-sent.
aisix_usage_event_drops_totalgains tworeasonvalues:send_failed, for a batch given up on without re-sending, andretry_budget_exhausted. Against a control plane that does not advertise batch de-duplication, nothing changes and the batch is dropped as before. -
A stalled log consumer costs log lines instead of stalling the gateway. Log events go into a bounded queue of 32,768 entries, drained by a dedicated thread. When the container runtime stops draining the gateway's stderr, and a log rotation is enough to do that, the new event is dropped and counted in
aisix_log_lines_dropped_total. One summary warning follows once the sink catches up. The request worker previously blocked inwriteand the whole gateway stalled,/livezincluded. Loss is zero unless the queue fills. -
Telemetry exporters ride out a longer outage and honor
Retry-After. A transiently failing exporter keeps a batch in flight for up to 5 minutes, backing off from 200 ms to a 30-second ceiling. This covers OTLP, Datadog, SLS, and object stores. It previously gave up after four attempts spanning about 3 seconds.Retry-Afteron a429or a503is honored in both of its forms, capped at the same 30 seconds. The queue behind a sink now holds 8,192 records, up from 1,024. A full queue drops the newest record asqueue_full, where records were previously lost further upstream asretries_exhausted. A full-capture exporter owns its records, so a long outage holds more memory. Permanent errors still drop on the first attempt. -
No request pays for Redis failure detection during an outage. When the Redis circuit breaker opens, a background probe now tests recovery as the 30-second window expires. The response cache and the shared rate-limit counters share that breaker. The next business request is no longer admitted as the probe. Recovery timing is unchanged; it is simply no longer billed to a caller. A
PINGproves less than the real command the old probe used, so a server that answersPINGwhile this subsystem's own operations still time out closes the breaker. The commands behind it then each pay one timeout budget until the first failure re-opens it. -
GET /metricsis served as a chunked response. The exposition is streamed in 256 KiB pieces instead of being built whole in memory. The text itself is unchanged, and pieces break between whole series. At high cardinality, the peak memory a scrape adds fell from hundreds of megabytes to a few. There are two consequences. A render that fails after the first piece ends the body with an error rather than answering500, so a truncated scrape cannot be ingested as a complete one. And a reader that stops reading without closing the connection loses its own scrape after a bounded wait, instead of parking every later scrape. Two scrapes that overlap no longer share one render; the second waits one render and produces its own current exposition. -
Upstream host names are resolved through a short-lived shared cache. Every outbound client on the request path resolves through one process-wide cache. Concurrent lookups of a name collapse into one, answers are reused for 30 seconds, failures for one second, and the cache holds at most 1,024 names. An upstream whose address changes is therefore dialed at the previous address for up to 30 seconds. Bedrock and the
/v1/realtimedial build their own transports and are not covered. A provider key'sresolve_addressespins still apply on top and never reach the cache. -
The control plane answers
503instead of401when it cannot read the revocation list. Every/dproute a gateway calls now separates two cases. A revocation store that could not be read is503 MTLS_UNAVAILABLE, which the gateway retries. A certificate that is not acceptable is401. The routes covered are the heartbeat, certificate rotation, the budget check, and usage ingestion. Neither case fails open. The most likely transient control-plane fault previously reached the gateway as a final refusal. -
The origin allowlists accept the spellings a browser actually sends.
AISIX_CLOUD_CORS_ALLOWED_ORIGINSandAISIX_TRUSTED_ORIGINSnow accept an IPv4-mapped address in its hex form, such ashttps://[::ffff:c0a8:1], which is what anOriginheader carries. The Helm chart accepts a canonical address carrying a written-out zero hextet, such ashttps://[1::1:0], instead of refusing the install. The chart also refuses at render time the malformed IPv6 entries that previously rendered and then crash-looped cp-api. It refuses a wildcard suffix carrying more than one leading separator, which cp-api has always refused. No entry cp-api accepted before is refused now.
New Features
-
Usage ingestion is idempotent per batch. A gateway mints one id per usage batch and sends it as
X-Aisix-Usage-Batch-Id. dp-manager claims that id in the same transaction that inserts the rows. A batch that committed is therefore answered with exactly the first delivery's response, and writes nothing a second time. Every/dp/telemetryresponse carriesX-Aisix-Usage-Batch-Dedup: 1, which is how a gateway learns that re-sending is safe. The capability is judged per response and never cached, so mixed-version dp-manager replicas and a rollback are both safe. A batch the control plane can never store is answered422and dropped at once, rather than re-sent. Claims are kept at least 24 hours and swept by the existing prune daemon. -
The offline package can configure CORS.
AISIX_CLOUD_CORS_ALLOWED_ORIGINSis now forwarded by the packageddocker-compose.yaml, offered commented out in.env.example, and documented in the README besideAISIX_TRUSTED_ORIGINS. A deployment whose Dashboard is served from a different origin than cp-api previously had to hand-edit the compose file, which the next package extract overwrote. The source-tree compose stack gained the same knob, plus the notification and MCP-spec opt-ins and the CA-bootstrap override. -
New gateway metrics. The gateway adds
aisix_config_unknown_kind_resources{kind},aisix_config_apply_duration_seconds{trigger},aisix_config_apply_batch_events{trigger}, andaisix_log_lines_dropped_total. Thetriggerlabel iswatchfor a coalesced batch of watch events andfullfor a resync. All four are registered in the metric-label catalog, soobservability.metrics.labelscan select on them, andtriggeris a new label variable.
Improvements
- Background computation no longer competes with request serving. The configuration
apply, the Prometheus render, and the snapshot reclaimer run on a demoted-priority
thread, while request workers, the
/livezand/readyzlistener, and the log writer keep their priority. - The configuration digest is computed when something reads it, rather than on every
apply. On a configuration of roughly 70,000 resources, an apply fell from 0.165 to
0.0275 core-seconds, with the digests byte-identical and
apply_seqandapplied_atkeeping their documented rule. - The per-caller budget decision cache evicts in constant time, instead of scanning all 10,000 entries on every miss.
- The queue between the proxy and the usage sender now holds 16,384 events, up from
1,024. That absorbs a multi-second control-plane stall which previously dropped events
as
sink_full. A timed flush now fills from the ready queue, instead of sending an undersized batch while a backlog waits. - Environment-scoped budget totals are answered from a new partial covering index, instead of scanning charged usage from other environments. The index is built concurrently at startup under the existing migration lock, and an interrupted build is repaired.
- Usage ingestion resolves a batch's pricing in two queries, rather than one per model plus one per miss. A batch of 100 models with 50 misses now costs two reads instead of 150.
- The public
aisix-cpchart's values table now describes all 117 values, including the four an install cannot skip. The render-time message for a placeholder PostgreSQL password now saysopenssl rand -hex 24, because base64 characters corrupt thepostgres://DSN the password is embedded in.
Fixes
- cp-api no longer exhausts its connection pool when many gateways request certificates
at once.
POST /api/environments/{env_id}/gateway_certificatesheld one pooled connection while taking up to three more, so a rollout of sixteen gateway replicas could wedge the pool until the callers gave up. The certificate, its credential identity, and its audit row now commit or roll back together. A rolled-back issuance therefore leaves no live mTLS credential whose private key the caller never received. DELETE /api/environments/{env_id}now revokes certificates and deletes the environment atomically. A failure part-way could previously leave certificates revoked against an environment that survived, kicking a fleet of gateways off a working environment.- A transient database failure while reading the registered gateway fleet no longer turns
a resource save into
500with nothing stored. An unreadable fleet gates nothing, as it did before 1.3.0. The compatibility gate's own verdict,422 DP_INCOMPATIBLE, is unchanged when the fleet can be read. GET /api/data_exportanswers409 DATA_EXPORT_UNAVAILABLE, naming dp-manager as the thing to start, instead of500with a raw SQL error, on a control plane where dp-manager has never run.- The data export and import audit records are now written for attempts that fail or are
abandoned mid-download. They carry an
outcomeofcompleted,failed, orclient_disconnected, and an export also carries thebytes_deliveredcounted at the response writer. A client that took part of an organization's data and vanished previously left no record at all. - dp-manager no longer stays in gRPC graceful shutdown indefinitely behind a long-lived watch. Draining is capped within the existing five-second shutdown budget, and startup failures cancel and join the workers they created.
- cp-api and dp-manager no longer run Gin in debug mode. The 187 route-dump lines per
start, each naming an internal handler, are gone from production logs.
GIN_MODE=debugstill reaches the dump. - The Dashboard no longer warns at boot about Google and GitHub OAuth providers it was never configured with. A provider given only a client id is no longer registered with an empty secret.
- Deleting a budget while the aggregator is mid-tick no longer produces warnings from the aggregator or from the alert notifier.
- A usage event carrying an over-long value in one of four bounded fields is now clipped
at ingest, with one warning per batch. It previously failed the whole batch, taking
every other billing row in it. The four fields are
cache_status,inbound_protocol,operation, andguardrail_bypassed_reason.
API Changes
The structural comparison reports no operation added, removed, or changed. Both
documents carry 129 operations, no schema was added or removed, and there is no breaking
change. The only structural difference is the description of GET /data_export. One
change carries meaning the structural comparison cannot see.
| Endpoint | Change |
|---|---|
GET /data_export | On a control plane where dp-manager has never started, the call now answers 409 with error.code DATA_EXPORT_UNAVAILABLE, where it previously answered 500 EXPORT_FAILED with a raw SQL error. The precondition — dp-manager's first start creates the table holding the projected configuration that every bundle carries — is now stated on the endpoint, and the 409 description lists both codes. EXPORT_IN_PROGRESS is unchanged behavior, now documented: it covers both an export already running for the organization and the deployment-wide concurrent-export cap. |
The gateway-to-control-plane protocol, /dp/*, is not part of the Cloud Admin API. It
gained X-Aisix-Usage-Batch-Id on POST /dp/telemetry, X-Aisix-Usage-Batch-Dedup: 1
on every /dp response, a 422 for a batch that can never be stored, and
503 MTLS_UNAVAILABLE when the revocation list cannot be read.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 1.4.0, including an upgrade that passes over it on the way to a later release.
- Do not add the new metric families to
observability.metrics.labelsuntil every gateway in the environment runs 1.4.0. A gateway refuses an unknown metric family in that setting at startup. A snippet namingaisix_config_unknown_kind_resources,aisix_config_apply_duration_seconds,aisix_config_apply_batch_events, oraisix_log_lines_dropped_totaltherefore stops a 1.3.0 gateway from starting. - An alert on
aisix_config_rejected_resourcesnow excludes unknown resource kinds. Addaisix_config_unknown_kind_resourcesto keep the previous, wider view of "something is unloadable". - The first cp-api start after the upgrade builds a second partial index on
dpmgr_usage_events, concurrently and under the existing migration lock. On a large usage table, expect startup to do extra work once, plus the index's storage and write maintenance from then on. - A credentialed etcd whose dial legitimately takes longer than 5 seconds must now set
etcd.dial_timeout_msexplicitly. The key defaults to 5000 ms, where the dial used to be unbounded, and0restores the old behavior. Deployments withoutetcd.userare unaffected, because their dial performs no I/O. - Remove or correct a stale
redis.passwordorredis.usernamefield before upgrading. Insinglemode those fields had no effect until 1.4.0, so a deployment carrying a stale one beside a working URL credential now sends the field value. The shared backend then degrades underreason=refused, where the field was previously ignored. - Expect a boot warning on a Redis that requires authentication and has no credential
configured. Such a deployment already failed every Redis operation. It now reports
reason=refusedat startup and serves degraded, instead of loggingconnectedand failing per request. - Usage re-sending requires both planes at 1.4.0. A 1.4.0 gateway talking to an older control plane drops a failed batch exactly as before. No configuration enables or disables this.
1.3.0
Release date: September 18, 2026
A model, an MCP server and a price are now referenced by resource id rather than
by the name they happen to carry, so renaming one no longer rewrites every
document that points at it. Structured output works on Anthropic, Gemini and
Bedrock upstreams, and the Responses-to-chat bridge answers with the reasoning,
images, tool parameters and usage the caller sent or expects. A request the
caller abandons is now in the usage log, and a streamed request writes one
access-log line at the end of its stream rather than one at the start. Gateway
and control-plane images ship for linux/arm64 as well as linux/amd64, and the
control-plane chart lets OpenShift assign its own UIDs. A provider key can dial
fixed addresses for an endpoint reached over a private link, a gateway can serve
several proxy listeners with their own TLS, and an organization's whole
configuration can be exported to one file and loaded onto another deployment.
Behavior Changes
-
A request the caller abandons now files a usage row. A client that hung up before the response head was written left an access-log line and nothing in the usage log, on every metered endpoint. Such a request now files one terminal event with status
499,error_class = "client_disconnected", zero tokens and zero cost, plus one event per attempt that had already failed. The attempt that was in flight is named, so a499row says which target the request had committed to. A stream abandoned mid-flight, or dropped before its first byte, files the same way with its own message./mcp,/a2a, the passthrough namespace,/v1/realtime's pre-upgrade phase and the files, batches and fine-tuning surface file a row with empty model fields and their own attribution. An abandoned upload files one too.These rows did not exist before, so an environment's request count rises and its success rate falls — the control plane derives both from the usage events. Latency percentiles are unaffected; they already filter to successful rows. Spend is unaffected: the rows cost zero.
-
The gateway's on-disk configuration cache is now opt-in. A new
managed.snapshot_cache_enableddefaults tofalsein both managed and self-hosted etcd modes, and asnapshot_cache_pathon its own no longer enables persistence: with only the path configured the gateway writes no snapshot file at all. Disabled, it skips the reads, the encoding, the full-state copies and the background writes. In-memory last-known-good serving is unchanged. See the Upgrade Notes for what a restart looks like without it. -
A streamed request writes its access-log line when the stream ends. Six families wrote the line the moment the response head existed, often minutes early: an abandoned stream logged
200beside its own499usage event, and a delivered stream's line carried neither token counts nor the provider response id. The line now goes out with the request's terminal usage event, so the two agree on status, error class and message by construction, and the streamed line carries the token counts,provider_request_id,upstream_modelandprovider_key_id. Exactly one line per request, in all three endings. -
Every access-log line gains
duration_ms, how long the request occupied the gateway from arrival to last byte out.latency_mskeeps its meaning — what the caller waited for, time to first token on a stream — so the two differ by the length of the stream and are equal on anything buffered. The line also gainsupstream_modelandprovider_key_id, naming the target actually selected wheremodelnames the entry the caller addressed. -
A cache hit reports the cache, and never a target it did not dispatch to. The line gains
cache_statusand, on a hit,cache_hit_layer. On a hit of a routing or semantic Model Group,provider,provider_key_idandupstream_model— and the Prometheus labels built from them — now reportunknownor nothing rather than whichever candidate the strategy happened to rank first. A per-provider request panel therefore stops counting a group's cache hits under a provider that did not serve them, and filtering the Logs page by a provider-key label drops those rows. Direct-model hits are unchanged, and the hit's usage event now names the model that produced the stored body inprovider_model_version. Cost was already zero on a hit. -
A buffered
/v1/chat/completionsagainst an upstream that reports no usage now returns the gateway's own estimate instead of zeros. The usage record already carried that estimate, so a caller could not reconcile what it read with what the Dashboard billed. Per counter: a counter the upstream reported stands, one left at zero is filled, and a total beside a filled zero is recomputed. Cache hits answer the same way. Nothing billed changes, and streaming/v1/chat/completionsstill forwards the usage frame the upstream sends. -
tool_choiceis dropped when no tool survives protocol translation. An OpenAI-compatible or Anthropic upstream rejectstool_choicewithouttools, so a caller that sent an empty tool list — the shape agent CLIs send on a context-compaction call — got a400from the upstream. All three converters now emit the choice only when the converted tool list is non-empty, including a choice that forces a call, and answer as an ordinary tool-free request. A caller posting the pair directly to/v1/chat/completionsagainst an OpenAI-shaped upstream is still forwarded verbatim. -
An input guardrail on
/v1/responsesnow reads a replayed tool call's name and arguments. That API spells a model turn as a typed item carrying norole, so the scan read an agent's whole tool loop as user text and droppedfunction_callitems entirely — a block rule that fires on/v1/chat/completions, where the same replayed call has always been scanned, was bypassable by moving the payload into a tool call on this surface. An operator running a block rule whose pattern appears in a replayed tool call's arguments will start seeing refusals that previously passed. That is the gap being closed, not a regression. -
Two guardrail kinds stop screening replayed tool results on
/v1/responses.semanticandazure_content_safety_text_moderationread only user-role messages under their defaulttext_source. Afunction_call_outputused to be mislabelled as a user message, so they happened to scan it; now that it is correctly a tool result, they do not — exactly as arole: "tool"message already sits outside what they read on/v1/chat/completions. This is the two surfaces agreeing, but it is a coverage reduction for an existing rule with unchanged configuration: settext_sourceto its all-messages value to keep screening tool results. -
A URL rewrite rule now runs ahead of host-matched passthrough dispatch too. Rewriting is an entry-stage operation on every route family, so an existing rule that carries no
hostslist now also rewrites a request dispatched by host. Addhoststo a rule to scope it back. -
Bedrock, the object-store telemetry exporters and the Realtime dial now honor the configured
upstreamconnection settings. Pooled Bedrock and object-store connections expire onupstream.pool_idle_timeout_secs(30 seconds by default) rather than at 90 seconds, which is what makes lowering that setting work behind a load balancer that reaps idle sockets sooner. A guardrail's Bedrock dial usesupstream.connect_timeout_ms(5 seconds by default) instead of the 3.1 seconds built into the SDK, and a stuck/v1/realtimeupgrade fails at that budget instead of waiting out the kernel's SYN retries. A Realtime session that cannot open its upstream socket now also contributes astatus="502"sample to the request metrics, where it previously appeared in the logs and in no counter. -
Outbound requests advertise the build version. Upstream requests carried
aisix/0.1whatever release was running; the defaultUser-Agentis nowaisix/<build version>, the same sourceaisix --versionand theServerheader read. An upstream allowlist keyed on the old string has to be updated. -
An unrecognized
AISIX_*environment variable no longer refuses startup. The loader read every such variable as a configuration override and the root config rejects unknown fields, so one Kubernetes Service namedaisix-*in the namespace crash-looped every gateway pod beside it. A variable is now kept only when its first segment names a top-level setting; anything else is dropped with one WARN naming it. A misspelled key under a real section still fails the boot exactly as before.AISIX_CONFIGnow works when running the binary directly. -
A control-plane address without a scheme is accepted, and a malformed one fails the boot.
managed.cp_base_urlis normalized once at load, sodpm.example.com:7944is read ashttps://dpm.example.com:7944. Previously such a value connected to etcd and then denied every proxied request with429 budget_exceededwhile the console showed the gateway healthy.managed.cp_etcd_endpointkeeps the opposite convention and now tolerates a scheme by stripping it, where it previously doubled into an undialable address. A value that is neither shape now fails startup naming the variable. The control plane applies the identical rule to its own copy (AISIX_CLOUD_DPMGR_BASE_URL,AISIX_DPMGR_BASE_URL), so a scheme-less value now reaches the console's install snippets and dp-manager's server certificate with the scheme attached. -
Deleting a model that a rate limit policy is scoped to is refused with
409 MODEL_IN_USE. It used to succeed and strand the policy, in PostgreSQL and in etcd, counting requests against an id that resolves to nothing. Delete or re-point the policy first. A conditional policy that merely carries amodelormodel_namecondition is not a blocker — that clause simply stops matching. -
A cache policy's
model:scope now survives a rename and no longer recovers from a delete. The selector is enforced by the selected model's resource id. Deleting that model stops the policy covering anything, permanently: a later model of the same name is a different resource. The policy stays listed and enabled while matching nothing until it is saved again, which re-resolves the selector against whatever answers to that name now. -
An MCP server rename now carries the environment's anonymous ceiling. Before, the ceiling kept the old name, stopped matching, and closed anonymous access with nothing saying why. Deleting an MCP server now also rewrites and re-projects the rows that referenced it, which an operator observes as no change — the reference stays visible and inert — but those rows are written at delete time where previously nothing touched them.
-
The control-plane chart no longer pins a UID.
runAsUser,runAsGroupandfsGroupare gone from all three components'podSecurityContext;runAsNonRoot: trueand the RuntimeDefault seccomp profile stay. On a cluster that assigns one — OpenShift'srestricted-v2SCC — the pods run as the namespace's UID; everywhere else they run as the image's user, which is the same UID the chart used to pin. The images'USERis numeric for the same reason (10001for cp-api and dp-manager,1001for the Dashboard,10001for the gateway): the kubelet cannot prove a named user is non-root and refuses such a container once the chart stops supplyingrunAsUser. -
A
PUT /model_pricingupdate now applies every rate column. The update path omittedduration_cents_per_1m_seconds, so from the second save onward an audio-priced model kept the first rate it was ever given while the console showed the correction, and the telemetry path billed at the stored value. The audit event for that endpoint had the same gap and recorded a byte-identicalbeforeandafterfor a change to any of four rates. -
Deleting an environment that holds intra-environment references now succeeds.
DELETE /api/environments/{id}answered500naming a foreign key whenever a routing group's targets, an ensemble's panel, a semantic router's models, a cache policy's embedder or a passthrough route's anonymous key lived inside the environment being deleted, and the environment could only be removed by unpicking it by hand. -
A CHECK constraint rendered from a Go list now rebuilds itself after that list changes, on upgraded databases as well as fresh ones. An upgraded database could keep a constraint that refused a legal
kind=customguardrail, surfacing as a500. The other side of the same keying: a constraint an operator dropped by hand is no longer re-added by the next boot. -
AISIX_TRUSTED_ORIGINSentries are validated against the origin grammar cp-api applies to its own allowlist. An entry that cannot match a browser'sOriginheader —*, a wildcard pattern, an uppercase scheme or host, a trailing dot or slash, a written-out default port — is dropped with an error in the server log rather than trusted, and sign-in from that origin is refused. The deployment's own origin and its loopback twin are trusted as before.
New Features
-
Structured output on Anthropic, Gemini and Bedrock upstreams. A
/v1/chat/completionscaller asking for JSON — and a/v1/responsescaller throughtext.format— got prose back from these three, because none of their wires has a top-levelresponse_format. Each now gets the shape its own API defines: Claude 4.5 and later takeoutput_config.format, Gemini takesgenerationConfig.responseJsonSchema(2.x and later) orresponseSchema(1.x, with the older dialect's conversions), and Bedrock takesoutput_config.formaton the Messages route oroutputConfig.textFormaton Converse. Everything else — older Claude families, Anthropic-compatible third parties, the Converse publishers that support tool use — carries the schema on a synthetic forced tool and the reply is translated back into ordinary JSON content. Schemas are narrowed to each provider's documented keyword subset, with each removed constraint folded into the property's description, and sealed withadditionalProperties: falsewhilerequiredis left exactly as the caller wrote it. A caller's own tools and an explicitly statedtool_choicealways win over the synthetic one. The tool route runs its upstream leg non-streaming and fake-streams the result, so such a request pays first-byte latency it would not otherwise. -
The Responses-to-chat bridge reaches parity with the native path. The bridge serves a
/v1/responsesclient whose model has no native Responses endpoint. It now returns the model's reasoning as areasoningoutput item with the matching streaming sequence; forwardsinput_image,input_fileandinput_audioparts instead of dropping them; turnstext.formatintoresponse_format; forwardsparallel_tool_calls, translates acustom(freeform) tool so the model can call it at all, normalizes everytool_choiceform to the provider-neutral shape, and serializes a JSONfunction_call_outputrather than sending the model an empty string. A bridged custom tool call comes back as acustom_tool_callitem carrying the freeforminput, streamed through its own event pair, and a replayed one goes back upstream correctly so multi-turn works. The usage a bridged client reads now equals the usage recorded. On the Anthropic converter,parallel_tool_calls: falseis re-expressed astool_choice.disable_parallel_tool_useinstead of being flattened onto a body that rejects unknown keys. -
A guardrail can read only the latest turn. A new
input_messagessetting (all, the default, orlatest_turn) on every guardrail kind. IDE and agent clients replay the whole conversation on every call, so a rule that matched one message kept refusing the rest of the session even though the new prompt was clean.latest_turnreads only the messages after the last assistant message, system messages excluded — this turn's user messages and the tool results answering them — so a trailing assistant prefill stays inside the current turn. It governs the input hook only;hook_point: outputtogether withlatest_turnis refused rather than accepted and ignored. A masking guardrail underlatest_turnleaves the conversation history exactly as the caller sent it. The Dashboard's guardrail forms carry an "Input scan" picker and a list badge. -
A rename no longer breaks what points at a model, an MCP server or a price. Every reference the control plane projects now travels as the referenced resource's id beside the display name it has always carried, and the gateway resolves the id against the live table on every request. That covers an API key's model allowlist, a routing group's targets, an ensemble's panel and judge, a semantic router's embedder, default, routes and failure target, a cache policy's scope and its similarity embedder, a semantic guardrail's embedder, an MCP tool grant or denial, a per-MCP-server rate limit, and the environment's anonymous MCP ceiling. Renaming a resource takes effect on the next request with no edit to the referring document and no rewrite fan-out across the configuration. Nothing an operator configured has to change, and the name is still written beside the id so gateways older than this release keep reading the reference they always did.
-
Prices live in their own documents. A model takes its per-token price from a shared pricing document — the environment's own overrides first, the deployment-wide catalog second, the model's inline
costlast — instead of carrying the price inline. Repricing is now one write to the price rather than a rewrite of every model that bills at it, and embedding models carry a price the gateway can resolve for the first time. The catalog is published under a new deployment-wide etcd prefix that gateways may read and never write. -
A provider key can dial fixed addresses for its endpoint. An upstream reached over a private link often has no DNS entry, while the vendor behind it still answers
404to any request that does not carry its own hostname — andHostis not a forwardable header. A new optionalresolve_addresseson a provider key takes an ordered list of IPv4 or IPv6 address literals and connects to them for the hostname the key'sapi_basenames. Only the connection target moves: theHostheader, the HTTP/2:authority, the TLS server name and the certificate check all keep using that hostname, and the scheme and port keep coming from the base URL. The addresses are tried in the order written, as a DNS answer would be, so a link terminating on one address per availability zone stays reachable when one is down. It is scoped to theapi_basehost, so anapisentry serving a second protocol from the same host is covered and one naming a different host resolves normally. Omitting the field resolves through DNS as before, andnullclears an override. Every surface that dispatches through the provider key honors it; Bedrock and/v1/realtimedo not, because they build their own transport and do not honor the key'stlseither. It has no effect when the gateway reaches its upstreams through a forward proxy, which is handed the hostname and resolves it itself. The Dashboard carries an "Endpoint addresses" field beside the endpoint URL. -
Reserved entries in a direct model's
effort_mapping. The map answered one question — "the caller asked for X, send Y" — and was silent about the two cases operators actually hit. The empty-string key now matches a request that sets no reasoning effort at all (absent,nullor empty) and supplies one; the*key matches any other present value with no entry of its own; and anullvalue removes the effort from the outbound request so the provider's own default applies. On the Anthropic messages and token-counting endpoints onlyoutput_config.effortis read: athinkingblock is not an effort setting for this mapping, so a client that sendsthinkingand no effort takes the empty-string entry. A request that turned reasoning off withthinking.type: disabledis never given a tier. The Dashboard renders each side of a rule as a picker, since neither reserved key nor a removal is typable. -
Custom webhook bodies, headers and captured responses. A webhook notification channel could send exactly one payload shape with exactly one header, so a receiver expecting anything else could not be connected at all, and a rejection recorded only
webhook returned 400. A channel now takes an optional Gotext/templatebody (alert-event variables plus a one-linemessage, ajsonfunction, rendered and validated at save time) and up to 16 extra request headers, whose values are write-only the way the channel URL is. A failed delivery records the destination's status and response body, with the channel's own URL and header values redacted out of it; the list carries the first 8192 bytes and a new single-delivery endpoint returns the whole thing. The Test button reports the exact body sent and the response received. -
Organization data export and import.
GET /api/data_exportstreams everything one organization owns — environments, models, credentials, policies, teams, members, the projected configuration the gateways read, and a window of the audit trail — as one gzip-compressed SQL file, andPOST /api/data_importapplies it to another deployment of the same control-plane version in one transaction. The default is a backup: credentials stay encrypted under the deployment's own master key, an unredacted file additionally carries the certificate authority so a restore keeps the gateways it had, and it requires the owner role.redact=trueproduces a support bundle instead, where every credential becomes a known synthetic value and every account signs in with a published fixed password, so the configuration can be reproduced by someone who should not see the real secrets. Restoring onto a deployment that has already issued certificates keeps that deployment's authority and says so. The Dashboard carries the export card. -
URL rewrite rules can be scoped by inbound host. A new optional
hostslist restricts a rule to named hosts — case-insensitive exact names or single-label wildcards, port ignored. Host and path must both match, and the first matching rule applies once. -
The
/mcpaccess log says what the request was. Every MCP operation tunnels through onePOST, so a handshake, a tool call and an ACL-emptiedtools/listrendered identically. The line now carriesmcp_method,mcp_toolon atools/call, andtools_totalandtools_returnedon atools/list. Atools/listthat ends up empty although the upstreams returned tools emits one WARN naming which of the two misconfigurations it was. -
The MCP anonymous allowlist and every key or policy can name a server by resource id, which is what makes the rename survival above work on the gateway side.
-
The control plane exposes optional Prometheus metrics on their own listener, off unless
AISIX_CLOUD_METRICS_LISTENis set (api.metrics.enabledin the chart, which also renders a metrics Service and an optional ServiceMonitor). They report write volumes and configuration-fan-out cardinality under anaisix_cp_prefix, so one Prometheus can hold both planes. -
Several proxy listeners, each with its own TLS.
proxy.addrplusproxy.tlsdescribed the only listener there was, so configuring a certificate made that single port HTTPS-only and a deployment that also needed plain HTTP had no way to get it. A newproxy.listenersblock takes the complete set of proxy listeners, each with an address and its own optional TLS, so one gateway serves HTTPS and plaintext HTTP at the same time. All listeners share one router and one application state, TLS and ALPN negotiation are per listener, and every listener takes part in graceful shutdown and the drain the way the single listener always has.proxy.addrandproxy.tlsremain the single-listener shorthand and are unchanged whenlistenersis absent or empty; when it is set,proxy.addris not bound and the gateway logs one INFO line saying so, andproxy.tlsbeside a non-emptylistenersis a configuration error rather than a certificate that applies to nothing. A duplicate address is refused at startup, naming both entries, becauseSO_REUSEPORTwould otherwise let two entries co-bind one port and answer TLS on some connections and plaintext on others. An environment-only deployment sets the whole set as one JSON array inAISIX_PROXY__LISTENERS. Theaisixchart 1.3.0 carries the matchinglistenersvalues, one entry per port, each with its own container port, Service port, optional NodePort and optional TLS Secret. -
Gateway and control-plane images ship for
linux/arm64. Every tag the pipeline publishes —dev,sha-*, a release candidate and a release — is now a manifest list carryinglinux/amd64andlinux/arm64, for the gateway and foraisix-cp-api,aisix-cp-dpmandaisix-cp-ui. Each architecture builds natively and is smoke-tested on its own runner, and a tag is not published unless both are present. The offline installation package is built per architecture and named for it (aisix-self-hosted-offline-<version>-linux-{amd64,arm64}.tar.gz);run.shrefuses a package built for another architecture instead of loading images that cannot execute. The unsuffixed filename keeps serving the amd64 package. -
cp-api can serve a Dashboard hosted on another origin.
AISIX_CLOUD_CORS_ALLOWED_ORIGINS(api.corsAllowedOriginsin the chart) lists the browser origins allowed to call/api/*cross-origin. Empty is the default and writes no CORS header at all, which is what an installed control plane wants: it serves the API and the Dashboard from one origin, so nothing is cross-origin. Populated, it permits cross-origin/api/*requests, including Better Auth routes under/api/auth/*, and echoes the concrete origin rather than*; the Dashboard proxy is excluded. The chart validates entries at render time, and cp-api validates them again at startup. The released Docker Compose package does not forward the environment variable from.env; use the Helm chart when this release needs cross-origin access. The chart and image also disagree on browser-canonical IPv4-mapped IPv6 origins, so use a DNS hostname or IPv4 address instead of that form. -
The Dashboard Deployment can mount a private database CA.
ui.extraVolumesandui.extraVolumeMountsaccept a ConfigMap or Secret, withNODE_EXTRA_CA_CERTSpointed at it throughui.extraEnvVars, so database-backed authentication can trust a private PostgreSQL chain.
Improvements
-
A bulk configuration change no longer competes with the request path. Watch events are now coalesced on a timed window rather than on whatever happened to be buffered, with the window adapting to how expensive the previous apply was; the apply runs off the async I/O workers, so an unrelated socket no longer waits behind it; the configuration digest resumes from a checkpoint instead of rehashing every row; the resource index shares its strings across snapshots; and a replaced configuration is reclaimed on a shared background thread rather than synchronously on whichever request thread held the last reference. On a 70,000-row configuration that last one alone moved the request thread's destructor cost from a 7 to 8 ms p99 to under a microsecond. A 1,507-row burst now costs 12 whole-configuration passes instead of 131.
The cost is that a single isolated configuration write becomes visible one quiet period later: measured against a local etcd, the median time from the write to
applied_revisionmoving goes from 21 ms to 42 ms. Nothing has to be reconfigured and no configuration is served differently. -
High-cardinality metric scrapes are substantially cheaper. The recorder keeps per-series storage with lock-free sample buffers, caches escaped labels and the sorted series directory per metric kind, preallocates the output from the previous render, and renders on blocking workers with overlapping scrapes sharing one render. On a 53,260-series, 723 MB exposition, warmed render CPU falls from roughly 6.8 s to 0.57 s and empty upkeep from 1.11 s to 0.011 s. Metric names, types, default labels, histogram edges and rolling quantiles are unchanged. Label values carrying a raw backslash, quote or newline are now escaped without conflating distinct series.
-
Rate-limit evaluation and JWT authentication no longer scan unrelated rows. Policy candidates are indexed by their required conditions and JWT API-key bindings by the API-key table's generation, both invalidated by the table they read rather than by any configuration write.
-
The control plane's outbox delivers faster. Transactions reach the embedded Kine in process instead of through its own gRPC listener, up to four independent keys drain concurrently while per-key order is preserved, the watermark read no longer lists the whole resource range, large etcd Range and Watch responses are encoded without temporary buffers, and a telemetry flush persists up to 100 events per statement instead of one (the embedded Kine also moves to v0.16.3, which corrects prefix reads for resource kinds whose names carry an underscore). Budget checks stop scanning zero-cost usage rows, backed by an additive partial index built concurrently after boot migration, and the gateway-compatibility lookup for a configuration write reuses the same transaction rather than borrowing a second connection from a pool it may have exhausted.
-
The Dashboard adopts API7 Console's color tokens. Every surface, text, border, status and action color moves to the Console value, with card edges drawn by a shadow. Visual only — typography, spacing, control sizes and layout are unchanged.
Fixes
-
A repeatedly excluded routing target could block a request worker indefinitely. The first exclusion after the 60-second log throttle expired held a read guard while taking the same shard's write lock, so the request never reached the upstream timeout and blocked workers accumulated during an outage.
-
Concurrent JWT requests could fail while a valid key fetch was in flight. The refresh interval was recorded before a result existed, so cold-cache callers were rejected and a request using a newly rotated key could miss the refresh already running. Fetches are now shared per JWKS URL or issuer, and stale trust material is served during a refresh or an outage.
-
Fixed a connection-pool deadlock that could stall the environment model list and the members list under concurrent load.
-
A gateway could hang on shutdown. An initial configuration read or watch creation that never answered, with no etcd request timeout configured, was not cancelled by the shutdown signal.
-
GET /api/config/publicand the console's install snippets carry a scheme. See the control-plane address note under Behavior Changes; a bare value pasted from the console previously produced a gateway that rejected every proxied request. -
Gateways can read the shared pricing catalog. The etcd authorization interceptor scoped every gateway certificate to its own environment prefix, so the new deployment-wide prefix came back
PermissionDenied— tolerated by the gateway, but warned about on every start. Reads under/aisix/global/are now granted; writes are not, and a gateway still cannot reach another environment's keyspace. -
A semantic router could be projected with the wrong embedding-failure target. Unreachable through the API, which rejects the shape three ways; the projector now refuses the document and says why rather than naming whichever model happened to sit in that slot.
-
The Dashboard's redaction switch no longer renders checked while the role is loading, which silently downloaded an unredacted backup for an owner who left it alone. Rapid navigation no longer lets a late response from the previous page replace the content of the one now in the URL.
API Changes
The structural comparison reports 42 changes: three operations added, none
removed, and six new schemas with none removed (DataImportResponse,
GuardrailInputMessages, NotificationDeliveryResponse,
ProviderKeyResolveAddresses, WebhookBodyTemplate, WebhookHeaders). Six of
the 42 are flagged for caller impact, all on one field — effort_mapping, whose
map values may now be null on four model operations and whose minimum value
length rose from 0 to 1 on two. Everything else is an addition.
| Endpoint | Change |
|---|---|
GET /data_export | Added. Streams one organization's whole configuration as a gzip-compressed SQL file. Query parameters redact (default false), audit_since (default 30 days ago), include_usage_events (default false) and usage_since (default 7 days ago). 200 is application/gzip with Content-Disposition and X-Aisix-Export-Redacted; 409 when an export for the organization is already running. Requires the new data_export read permission, and an unredacted export additionally requires the owner role — an admin token is accepted only when its owner holds that role and the token carries the write scope. |
POST /data_import | Added. Takes the gzip bundle as the request body and applies it in one transaction. Unauthenticated while the control plane holds no users at all; afterwards the caller must be a signed-in person, and a personal access token is never accepted. 200 returns DataImportResponse (imported, redacted, rows, ca as one of unchanged, replaced or kept_target, warnings, owner_user_id). 409 carries MIGRATION_VERSION_MISMATCH, ORG_EXISTS, USER_EXISTS, OUT_OF_SCOPE_ROWS, PROJECTION_TABLE_MISSING, MASTER_KEY_MISMATCH or MASTER_KEY_UNAVAILABLE. A ca of replaced means cp-api and dp-manager must be restarted. |
GET /notification_deliveries/{delivery_id} | Added. Returns one delivery with the destination's complete response body, up to the 1 MiB captured. Organization scoped, 404 for another organization's delivery. |
POST and PATCH /environments/{env_id}/guardrails[/{guardrail_id}], and the guardrail read views | Add the optional input_messages (all or latest_turn, default all). Omitting it is unchanged behavior. hook_point: output sent together with input_messages: latest_turn is refused 400 INVALID_REQUEST naming both settings, on create and on either half of a patch. |
POST and PATCH /environments/{env_id}/models[/{model_id}], and the model read views | Changed. effort_mapping values may now be null — a removal entry, meaning "send no effort upstream" — and a value's minLength rises from 0 to 1. A caller deserializing the map into non-nullable strings must accept null. Two shapes that used to be accepted are now 400: the empty-string key mapped to null, and any entry mapped to "". Automation that generates a mapping must not serialize an absent value as "". |
| The same two model operations | The reserved keys "" and *, and a null value, are additionally refused 422 DP_INCOMPATIBLE while a registered gateway in the target environment is older than the release that honors them. The 422 and its envelope are unchanged from 1.2.0; the set of configurations that can trigger it is what grew. |
POST and PATCH /notification_channels[/{channel_id}] | Add the optional body_template (Go text/template, 64 KiB or less rendered, must render to valid JSON against a sample event at save time, range, template and block rejected) and headers (16 entries or fewer, RFC 7230 token names of 128 characters or fewer, values of 4096 characters or fewer with no control character; Content-Type, Content-Length, Host, Transfer-Encoding and Connection refused). Both are refused 400 on a slack channel, including on an update that would move a webhook channel carrying them onto slack. A masked *** header value sent back on update keeps the stored value; a masked value under a name with nothing stored is 400. Sending an empty object removes every header. |
GET /notification_channels and GET /notification_channels/{channel_id} | Added: the channel representation carries body_template (empty string when the channel sends the fixed payload) and headers (names only, values ***, null when none). Both are always present. Additive for a reader; a client generated from a strict schema regenerates. |
POST /notification_channels/{channel_id}/test | Adds optional request_body, response_status and response_body. response_body is returned whenever a response arrived, including a successful one, with the channel's own URL and header values masked out. Where the deployment allows private destinations, this hands the destination's response body to the caller. |
GET /notification_deliveries | Added: delivery representations carry last_response_status, last_response_body (first 8192 bytes, cut on a UTF-8 boundary) and last_response_body_truncated, always present and null or false on a delivery that has not failed or never got a response. Additive for a reader; a client generated from a strict schema regenerates. |
POST /provider_keys, PATCH /provider_keys/{provider_key_id} and GET /provider_keys/{provider_key_id} | Add the optional resolve_addresses: an array of IPv4 and IPv6 address literals (minItems: 1, no port, no brackets), absent or null meaning the api_base host is resolved through DNS. PATCH replaces the list whole and null clears it; the detail response echoes it, since it is network topology rather than a credential. Refused 400 INVALID_REQUEST for an entry that is not a bare IP literal (a hostname, a bracketed address, a CIDR block, a host:port pair, an out-of-range octet, an IPv6 zone), for an api_base that is already an address literal or has no readable host — checked on the resulting pair, so a PATCH that only moves api_base is caught, and the error names both fields — and for a Bedrock adapter key, which dispatches over its own transport. Refused 422 DP_INCOMPATIBLE while any gateway registered in a target environment predates 1.3.0: such a release ignores the field and resolves the host through public DNS, handing the key's credential to whatever answers. |
POST /roles and PATCH /roles/{role_name} | The read-permission resource enum gains data_export. There is no write grant for it. |
Six changes carry meaning the structural comparison cannot see.
DELETE /environments/{env_id}/models/{model_id}answers409 MODEL_IN_USEin one more case: a rate limit policy whosescopeismodeland whosescope_refnames the model. Same status, same envelope, a condition that used to return200. A conditional policy carrying amodelormodel_namecondition is deliberately not a blocker.- A cache policy's
applies_to: "model:<name>"is now enforced by the selected model's resource id. Same type, same value, different binding: a rename keeps the policy on the model, and a delete stops it covering anything permanently, including after a model of that name is created again. Re-saving the policy with an unchanged selector re-resolves it. - An MCP tool grant, denial, per-server rate limit or anonymous ceiling entry
whose server segment is the exact name of a registered server is now bound to
that server and is read back under its new name after a rename. An entry
carrying a
*, or naming no registered server, still matches by name. No shape change on any of the four carriers. PUT /model_pricingnow storesduration_cents_per_1m_secondson an update. The endpoint is documented as a full replacement and its response echoes the stored row, so an organization that tried to change an audio price received a response contradicting the request it had just sent.PATCH /environments/{env_id}/models/{model_id}no longer promises that every reference follows a rename: a rate limit policy'smodel_namecondition keeps the old name and stops matching. That was already true and is now stated.DELETE /environments/{env_id}stops returning500for an environment whose resources reference each other. No shape change; a call that failed now succeeds.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 1.3.0, including an upgrade that passes over it on the way to a later release.
- Upgrade the control plane before the gateways. That is the only supported order.
- Set
managed.snapshot_cache_enabled: trueif you rely on disk recovery across restarts. The setting is new and defaults tofalse, and asnapshot_cache_pathon its own no longer enables persistence — with only the path configured, 1.3.0 writes no snapshot file at all. A gateway restarted while dp-manager is unreachable therefore logswaiting for the first configuration before binding the proxy listenerand does not serve until the control plane is reachable again; 1.2.0 restored from disk and served (snapshot restored from on-disk cache). The remedy ismanaged.snapshot_cache_enabled: true, orAISIX_MANAGED__SNAPSHOT_CACHE_ENABLED=truefor a chart or container deployment; the path keeps its default/var/lib/aisix/config_cache.json. In-memory last-known-good serving is unchanged. - Expect an environment's request count to rise and its success rate to fall.
Requests the caller abandoned now file usage rows and previously filed none. The
rows carry zero tokens and zero cost, are filterable by
error_class = "client_disconnected", and change nothing about spend. Latency percentiles are unaffected. - Re-point log processing at the end-of-stream access-log line. A streamed
request writes exactly one line, at the end of its stream, carrying the final
status and the token counts; it used to write one at head time carrying
200and no counts. Every line also gainsduration_msbesidelatency_ms, and a buffered/v1/chat/completionsline gainscache_status. - Check any address the gateway or control plane is given for a scheme. A
value that is not an
httporhttpsURL now fails startup instead of failing at runtime:managed.cp_base_urlandmanaged.cp_etcd_endpointon the gateway,AISIX_CLOUD_DPMGR_BASE_URLon cp-api andAISIX_DPMGR_BASE_URLon dp-manager. A scheme-lesshost:portis now accepted and read ashttps://, and an existing lower-casehttp://orhttps://value is unaffected. - Pin the UIDs back if you depended on them. The control-plane chart no longer
sets
runAsUser,runAsGrouporfsGroupon any component. A PodSecurityPolicy or Gatekeeper rule matching on UID, or a pre-chowned external mount, needs the three keys set again through values. The bundled PostgreSQL subchart keeps its own UID and is disabled separately on OpenShift —--set postgresql.primary.podSecurityContext.enabled=false --set postgresql.primary.containerSecurityContext.enabled=false. - Download the offline package for your architecture. The package is now built
per architecture and named
aisix-self-hosted-offline-<version>-linux-{amd64,arm64}.tar.gz;run.shrefuses one built for another architecture. The unsuffixed filename keeps serving the amd64 package. - Expect a pre-1.3.0 gateway to report one rejected pricing row during the
upgrade window. While a gateway older than 1.3.0 is still registered, the
1.3.0 control plane's projection of a Model Pricing override — an existing one
is re-projected at upgrade, a new one on save — makes that gateway report
aisix_config_rejected_resources{kind="pricing"} 1andaisix_config_last_reload_successful 0. Traffic, readiness and later configuration updates are unaffected, and the control plane's Rejected Resources view stays empty, because the row is an unknown kind to that gateway. The two series clear as soon as the gateway is upgraded to 1.3.0. Upgrade gateways promptly after the control plane, and do not page onaisix_config_last_reload_successfulfor pre-1.3.0 gateways during the window. - Re-save any organization price override that carries an audio duration rate.
A
duration_cents_per_1m_secondsset on any save after the first was discarded, so the stored value is the first one ever written. Saving the price again stores the current rate. - Re-save any cache policy whose
applies_toselects a model that has since been deleted and recreated. The selector is now resolved to a resource id at save time, and the recreated model is a different resource. - Repair or clear an
effort_mappingthat carries{"": null}or an entry mapped to"". Both shapes are now400. A model that stored one keeps serving, but its edit form shows the rule as incomplete and aPATCHresending the unchanged mapping is refused. Neither shape was reachable from the Dashboard. - Update anything that matches on the gateway's outbound
User-Agent. It now reportsaisix/<build version>instead of the fixedaisix/0.1. - Review
upstream.pool_idle_timeout_secsandupstream.connect_timeout_msif you use Bedrock, object-store telemetry export or/v1/realtime. Those three stacks now honor the configured values (30 seconds and 5 seconds by default) where they previously used 90 seconds, 3.1 seconds or nothing at all.
1.2.0
Release date: September 8, 2026
This release rebuilds gateway compatibility on a fixed support floor. The control plane now refuses a configuration that a registered gateway could not load, instead of writing it and warning afterward. The gateway no longer pays whole-configuration costs on every watch event, which is what let a bulk edit starve its own request path. Operators can now choose the label set of each metric. The raw OpenAI cache-write count is carried end to end, from the gateway's own response through the usage logs and their export.
Behavior Changes
-
A token window now refuses the request that arrives with its budget exactly consumed.
tpmandtpdadmitted one more request while the committed token count sat exactly on the cap, and refused only once it was strictly over. Both backends, in-memory and shared Redis, now compare against the cap the same way. Swapping one for the other does not change the observable limit. This applies to every token window, policy-derived and key-level alike. A window explicitly configured to0now refuses every request rather than admitting one. That matches how a0request window, such asrpm, has always behaved. Request counters are pre-paid and were already exact, so they are unchanged. -
A save whose configuration a registered gateway could not load is now refused, not warned about. The control plane validates every projected document against the read contract of every gateway release registered in the target environment. It answers
422 DP_INCOMPATIBLEwhen one of those releases would reject the document. Previously the write went through and the response carried arow_rejectedwarning. By that point the resource had already stopped serving on part of the fleet. Theerror.messagefield is a complete sentence, naming the gateway release, the affected gateways and the offending part of the document. Theerror.dp_compatobject carries the same facts as fields. Therow_rejectedwarning code is gone. A newbelow_floorwarning reports gateways older than the oldest release this control plane supports, which are not checked at all rather than guessed about.A gateway keeps its registration for five minutes after it stops reporting. So shortly after a fleet upgrade, a refusal can still name a version the fleet has already left behind. That is the fail-closed direction and is deliberate. The refusal carries
last_heartbeat_age_seconds, and retrying once the stale registration is reaped resolves it. -
The control plane refuses to start against a database last run by a release older than 0.12.0. The API service records its own version during boot migration. It stops if the recorded version is below the supported floor, naming the recorded version and the way out. The offline installer's
run.shrefuses the same upgrade before starting any container. Upgrade through a supported release first, or setAISIX_ALLOW_UNSUPPORTED_UPGRADE=1after taking a backup. Both honor that variable, and the chart accepts it throughapi.extraEnvVars. A fresh install is not refused, and neither is a database last run by a build that predates this ledger. -
An empty
ignore_statuseslist is now honored instead of the default. A model saved with an explicit empty list was accepted. It was then silently stored, read back and projected as[408, 429]. An operator who asked the background health probe to treat408and429as real failures kept a gateway that ignored them. Nothing on any surface said the input had been overridden. Empty now round-trips as empty on the stored value, theGETread-back and the document the gateway reads. Omitting the field is unchanged and still means[408, 429]. A model saved before this release keeps the substituted default until it is next saved. -
The Anthropic billing-attribution line is dropped from the system prompt for non-Anthropic upstreams. Anthropic-native clients prepend an
x-anthropic-billing-headerline to the system prompt. One of its segments varies per request in some deployments. Because the line sits at the very front of the prompt, every other provider saw a different prefix on every turn and cached none of it. OnPOST /v1/messagesandPOST /v1/messages/count_tokens, the line is now removed before the upstream request is built, whenever the resolved target is not the first-partyanthropiccatalog provider. That covers the attribution block in an array-formsystem, and the first line of a string-form one. The rest of the prompt survives byte for byte,cache_controlmarkers included, andmessagesis never touched. The gate is the catalog vendor id. A first-party key pointed at a regional or proxied Anthropic endpoint keeps the line. An OpenAI-chat bridge, a third-party Anthropic-compatible passthrough and the Bedrock, Vertex and Azure platform adapters all drop it. Nothing has to be reconfigured, and callers that do not send the line are unaffected.
New Features
-
Per-metric label selection. A new
observability.metrics.labelsblock states the complete label list for each metric family. The environment variableAISIX_OBSERVABILITY__METRICS__LABELScarries the same thing as a JSON map. A metric family you leave alone keeps its existing defaults, so an upgraded gateway emits exactly what it emitted before. Selection is applied before observations accumulate. Removing a counter or histogram label therefore aggregates its observations rather than dropping them, and gauges keep their required identity labels. An unknown metric name, an unsupported label variable or a duplicate label fails startup, and a change takes effect on restart. Time-to-first-token and request latency can now carry provider-credential and caller attribution across native and bridged request paths. Both are available to select, and neither is on by default. -
The Dashboard's gateway installation page offers the label selection. Operators can add supported labels, remove default ones and restore the defaults. The Docker, Compose, Helm and systemd snippets carry the complete selection as the startup environment variable. This is a gateway process setting, so it adds no resource field and changes no projection.
-
The raw OpenAI cache-write count is carried end to end. The
cache_write_tokensvalue now survives from the upstream through the gateway's own response, atusage.prompt_tokens_details.cache_write_tokens. It also survives into the usage event, the Admin API usage-log list and export, and the Dashboard's log details. It is carried on native OpenAI, the protocol bridges, legacy non-streaming Completions and recognized passthrough usage. An explicit zero stays distinguishable from an absent value, and an omitted value remains absent. It is a raw count, kept separate from the additive Anthropiccache_creation_tokens, and it changes no token total and no billing. Existing usage rows stay null.
Improvements
-
A bulk configuration edit no longer stalls the gateway's own request path. Every accepted configuration write used to pay costs proportional to the whole configuration rather than to the write. That meant a deep copy of the entire snapshot. It also meant a re-parse and canonical re-serialization of every stored document, to recompute both hashes. Finally it bumped the single global version, which was the invalidation key of every derived cache. The next request on each worker thread then rebuilt the whole guardrail index synchronously, building one runtime and one HTTP client per enabled attachment, each reloading the CA store. The rebuild restarted without bound if the snapshot moved meanwhile. On that worker,
/livezwas queued behind the whole thing.Derived caches now key on the tables they read rather than on the global version. One guardrail instance is built per configured guardrail and shared across its attachments, and it is reused across rebuilds unless that row itself changed. The index build is single-flighted and the retry loop is gone. The snapshot clone is structural, digests are computed once per entry version, and queued configuration events are applied as one copy-on-write cycle. The aggregating
/mcpendpoint caches each server's tool set per row, instead of re-walking every registered OpenAPI document per call.No configuration, wire shape or reported value changes. Both
config_hashandsource_hashstay byte-identical, the guardrails in force for a request are unchanged, and the rejected-row reporting is unchanged. One newinfolog line,guardrail index rebuilt, reports the row counts and how many instances were built versus reused. -
Three metric families are now actually emitted. The
aisix_redis_failures_total,aisix_otlp_fanout_failures_totalandaisix_otlp_fanout_drops_totalfamilies had emit methods and no callers. Querying any of the three returned an empty result, whether the subsystem was healthy or failing constantly. Every failure they describe is fail-open by design, so these series are the only place the degradation shows. Redis failures are now counted for the rate-limit store, the response cache and the semantic cache. They are labeled by subsystem-qualified operation, such asratelimit_acquire,cache_getandsemantic_lookup, so an operator pointing all three at one Redis can tell which one is degraded. The OTLP fan-out counts one failure per failed export attempt, retries included. It counts dropped records by reason:queue_full,worker_stopped,retries_exhaustedandpermanent_error. A failed batch counts as many records as it carried. -
A Dashboard page whose data fails to load now says so, instead of loading forever. A null list meant both "still loading" and "the load failed". A failed fetch therefore left an error card above a spinner that never resolved, and in a few places stated nothing at all. Every list page now settles: teams and team detail, budgets, A2A agents, admin tokens, MCP servers, provider keys, pricing, audit, notification channels and deliveries, and the three usage lists. The per-environment pages settle too: models, API keys, rate limits, cache policies, claim mappings, OIDC providers, passthrough routes, observability, logs and MCP policy. The organization settings cards and the team MCP entitlement card settle the same way. The settings and notifications pages now show the standard banner when the organization list itself fails, rather than rendering nothing. The flag is also raised again on a refresh, so a retry after a failure does not show the failure text for its whole in-flight window.
Fixes
-
An unreachable Redis no longer hangs every request that touches it. With
ratelimit.backendset toredis, or with a cache policy on the Redis backend, a Redis that failed at runtime left such requests waiting for minutes instead of degrading. Theaisix_redis_failures_totalcounter recorded the failure, but no request outcome followed. Every Redis round-trip and connection attempt is now bounded by a newtimeout_secssetting on eachredis:block, which defaults to 5 seconds. The existing fail-open path takes over inside that budget: rate limiting falls back to the per-process counters, and caches miss. A failure then short-circuits that subsystem for 30 seconds, so only about one probe request per 30 seconds pays the full budget during an outage. The breaker is shared per subsystem: the cache's exact and vector connections count as one, and the rate limiter is another. Once Redis recovers, the gateway notices within up to 30 seconds, and rate limiting counts per replica and caches miss until it does. A request whose upstream leg runs longer than 30 seconds can still pay a second budget on its cache write. Shared counting then resumes on its own, with no restart and no configuration change. -
A write that the control plane could not commit no longer answers
2xx. The transaction was committed after the response had already gone out. A caller that read the status line and disconnected cancelled the context. The commit then failed and the insert was rolled back, while that caller kept a201for a row that no longer existed. A mutating request's response is now held until the commit succeeds. A2xxthe transaction did not keep is discarded and answered500with the standard error envelope, with the row rolled back as before. Only2xxis withheld, so a409, a400or a499is delivered exactly as before. A caller that stays connected sees no difference. -
A streaming frame spread over several
data:lines kept the upstream provider's model id. The event-stream format lets one frame spell its payload over severaldata:lines joined with a newline. The model-id rewrite read only the first line. Such a frame did not parse as JSON, and was forwarded untouched. The caller was answered with the provider's own model id instead of the alias they addressed. This affected the/v1/messagesmessage_startframe and the/v1/responsessnapshot frames. The rewrite now reads the whole joined payload. It writes each rewritten line back into the line it came from, so the framing survives byte for byte. -
A failing token mint is no longer retried forever by the Dashboard. A failed mint round left no trace. With the auth plane unreachable, every open tab therefore started a fresh three-attempt round every 30 seconds, silently, driven by the budget banner's poll. A failed round is now remembered for a doubling backoff, starting at 30 seconds and capped at five minutes. Requests inside that window fail fast off the remembered error. The backoff resets on the next successful mint and on sign-out. A refresh that fails while the previous token is still valid keeps using it, as before.
-
Audit records for routing models no longer carry
nullfield values. A routing model that lefthash_onandfallback_on_statusesunset recorded them as JSONnullin the audit event, a value the API neither accepts nor returns. The keys are now omitted when unset, matching the gateway-facing document. Existing rows are not rewritten.
API Changes
The structural comparison reports no operations added or removed, no request shape
changed, and one new schema, DataPlaneIncompatibleError. The breaking check
reports no errors. Its 36 warnings share one kind: a response enum value added.
| Endpoint | Change |
|---|---|
The 32 projecting write operations. POST and PATCH on models, API keys (including /rotate), guardrails and guardrail attachments. POST and PATCH on rate limits, cache policies (including /purge), claim mappings and passthrough routes. POST and PATCH on observability exporters, OIDC providers, MCP servers (including /approve) and MCP server submissions. POST and PATCH on provider keys and A2A agents. Plus PATCH /environments/{env_id}, PUT /environments/{env_id}/mcp_policy and PUT /model_pricing. | Add a 422 response, DataPlaneIncompatibleError. It is returned when a registered gateway in the target environment could not load the configuration being saved. error.code is DP_INCOMPATIBLE, and error.message is a complete sentence safe to show an operator. error.dp_compat carries kind, dp_version, path, reason, affected_dp_count, affected_dp_nodes, affected_dp_versions and last_heartbeat_age_seconds. A caller must handle 422 on every write that reaches a gateway. The remedy is to upgrade the named gateways or remove the setting. |
The 27 create and update operations above that return warnings. Plus GET on a single model, API key, guardrail, rate limit or cache policy. Plus GET on a single claim mapping, passthrough route, MCP server, provider key or A2A agent. | The warnings[].code enum removes row_rejected and adds below_floor. The row_rejected code no longer occurs, because that outcome is the new 422. The below_floor code reports gateways older than the oldest release this control plane supports. Its field is empty, because it is about the gateway rather than one field. Automation that branches on row_rejected must be updated. |
| Usage-event representations add the optional cache_write_tokens. It is absent on rows recorded before this release, and on upstreams that report no cache write. An explicit 0 is distinguishable from absent. It is a raw count and is not additive to prompt tokens. |
Three changes carry meaning the structural comparison cannot see.
CompatibilityWarning.min_dp_versionkeeps its type and itsunreleasedsentinel, but its meaning moved. It is now the oldest supported gateway version that reads the field, and forbelow_floorit is the oldest gateway version the control plane supports. It used to be the oldest gateway version that fully executes the field, derived from a hand-maintained table. The companionaffected_dp_countmoved likewise, from "registered gateways running a version older thanmin_dp_version" to "registered gateways this warning applies to". Same shape, different number in some fleets.background_model_check.ignore_statusesaccepted[]before this release and stored[408, 429]. It now stores, returns and projects the empty list. There is no shape change, but the value a caller reads back after sending[]is different. The field's description now states what an empty array means, and how that differs from omittingbackground_model_checkon create versus onPATCH.- Every mutating operation under tenant scoping now answers
500where it previously answered a2xxwhose row had been rolled back. This is reachable only when the caller disconnects mid-write, in which case it receives nothing at all. There is no shape change, and no change for a caller that stays connected.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 1.2.0, including an upgrade that passes over it on the way to a later release.
- Upgrade through 0.12.0 or later before upgrading to this release. The control
plane API service and the offline installer both refuse a database last run by an
older release. Setting
AISIX_ALLOW_UNSUPPORTED_UPGRADE=1overrides the refusal after you take a backup, and the chart accepts it throughapi.extraEnvVars. This control plane supports gateways 0.12.0 and newer. - Update automation that branches on the
row_rejectedwarning code. That outcome is now a422 DP_INCOMPATIBLE, and every projecting write can return it. The codebelow_flooris new in the same enum, and reports gateways older than the oldest supported release. - Re-save any model that was saved before this release with an explicit empty
background_model_check.ignore_statuses. Such a model was stored with the substituted[408, 429], and it keeps that value until it is next saved. Saving it again stores the literal empty list.
1.1.0
Release date: September 7, 2026
This release hardens the path between the control plane and the gateway. A gateway now opens its proxy port only once it has a configuration to serve, recovers on its own from an etcd outage, an expired etcd token, or a watch that is never confirmed, and loads configuration sets that previously exceeded an internal message-size ceiling. A2A agents complete the client-header forwarding contract, and the On-Premises control-plane chart gains fixed NodePorts and stops waiting on a bundled database it was told not to use.
Behavior Changes
-
The gateway binds its proxy listener only after it has applied a configuration. In etcd mode a cold-booting instance keeps the proxy port closed until its first configuration read succeeds, instead of accepting connections immediately and answering every request
401 invalid_api_keybecause it knew no API keys yet. An orchestrator that treats "the port accepts" as "this instance is ready" no longer routes traffic to an instance that has nothing to serve. There is no configuration knob and no opt-out; this is the startup order now. The wait only ever applies to an instance that has never applied a configuration. File mode passes the gate immediately, and so does a restart that finds an on-disk snapshot cache: such an instance restores the snapshot and binds in well under a second even while its configuration source is unreachable, so a pod that has run before comes straight back up during a control-plane outage. A first boot with no cached snapshot is the only case that waits, and against a reachable control plane that wait is tens of milliseconds at realistic configuration sizes./livezand/readyzare served on the proxy listener, so a probe aimed at that port gets a refused connection rather than a response during the window; the same two paths on the admin listener, and/status/readyand/status/configon the metrics listener, are bound before the gate and answer throughout. Helm users should take the 1.1.0 chart alongside the 1.1.0 image: the chart's startup budget was widened to match this order. -
The
etcd.dial_timeout_msandetcd.request_timeout_mssettings now take effect. Both were parsed and then read by nothing in earlier releases, so every deployment ran with no etcd timeouts whatever the file said. Both keys are optional and unset means unbounded; there is no implicit5000.request_timeout_msbounds each single request and response etcd call — the configuration range read at boot and on every watch reconnect, creating the watch, and the admin API's reads — and never the established watch stream.dial_timeout_msbounds the whole dial, including the TLS handshake and the authentication exchange. A configuration file copied from the previous release's shipped examples already carries both keys at5000, so review it before upgrading; see Upgrade Notes. -
A gateway with etcd credentials configured no longer exits when etcd is unreachable at startup. Previously, setting
etcd.userandetcd.password_envmade an unreachable etcd fatal: the process worked through a fixed ladder of five connection attempts five seconds apart and then exited, roughly 20 to 25 seconds in, without ever binding a listener. Such an instance now behaves like an unauthenticated one — it starts, holds the proxy listener closed, retries on its existing backoff, and binds as soon as a configuration arrives. Credentials that etcd actively refuses — a wrong user or password, a user without the required permission, or credentials sent to a cluster with authentication disabled — remain fatal, and now stop the boot on etcd's first answer rather than after a fixed retry ladder. Anything that relied on the process exiting to signal "etcd is down" should read/status/readyinstead.
New Features
-
A2A agents can forward inbound client headers upstream. A new
forward_client_headerslist on an A2A agent names the client headers the gateway relays to that agent, as single-*glob patterns matched case-insensitively. It applies to every JSON-RPC method served at/a2a/<name>—message/send,message/stream, and every task operation — and to the agent-card fetch at/a2a/<name>/.well-known/agent-card.json. This completes the forwarding contract across all four proxy faces, alongside Provider Keys, passthrough routes, and MCP servers. Empty is the default and forwards nothing, so no existing agent changes behavior.Naming the credential slot the agent's
auth_typefills —authorizationforbearer,x-api-keyforapi_key— hands the agent the caller's own credential in place of the gateway's, never both, which lets an internal agent that already authorizes on the end user's identity keep doing so. A credential slot and thetraceparentandtracestateheaders are forwarded only when a pattern names them exactly; a broad glob such as*orx-*never sweeps them in. Headers whose relay would break the exchange are refused however broad the pattern:host, hop-by-hop headers, thex-aisix-*namespace, the headers describing a body the gateway re-serializes, anda2a-version, which is the gateway's own announcement of the version pinned inprotocol_version.One limitation is worth knowing before you forward
cookie. The gateway terminates inbound HTTP/2 and performs no cookie reassembly, so an HTTP/2 caller that splitscookieacross several header fields — an HPACK compression option that not every client exercises — has only the first of them forwarded. This is true of A2A agents, MCP servers, and Provider Keys alike; a passthrough route relays every value.The setting is available in the Dashboard on the A2A agent create and edit forms under Advanced, and through the Admin API. Saving a non-empty list while some gateways in scope still run a version that predates it returns a compatibility warning; those gateways relay nothing until they are upgraded, which is the fail-closed direction.
Improvements
-
The gateway loads configuration sets larger than 4 MiB. The gateway reads its whole prefix in a single etcd range response, and a 4 MiB gRPC decode ceiling meant an environment that grew past that size never loaded — a limit crossed by how many resources an environment holds, not by any one of them being large. It did not self-heal: a running instance silently stopped seeing configuration changes, and a restarted or newly scheduled one never applied a configuration at all. The ceiling is lifted on both the configuration read path and the admin read surface. Deployments below the old limit are unaffected.
-
The control plane no longer caps the size of a resource it writes to the gateways. Two message-size ceilings on the control plane's own write path — a 2 MiB send limit on the outbox client and a 4 MiB receive limit on the embedded store — could reject a single oversized projected resource on every poll, forever. Per-key isolation meant the rest of the queue kept draining, so the visible result was one resource whose configuration never reached the gateways, with nothing surfaced to whoever saved it. Neither ceiling was chosen for this path and both are removed. Reaching either required an unusually large projected value, such as an API key granting many thousands of models.
-
A gateway waiting on its configuration source now says so. While the first configuration read is outstanding, the gateway logs when the wait starts and repeats a warning every ten seconds. An endpoint that accepts TCP and then answers nothing previously produced a process with no port and no log line at all.
-
The On-Premises control-plane chart accepts fixed NodePorts for the API and Dashboard services.
api.service.nodePortandui.service.nodePortjoin the existingdpm.service.nodePort, each rendered only when its Service type isNodePortand a value is set; both default to empty, so an installation that does not set them keeps dynamic allocation. This lets an operator setapi.publicBaseURLto a known endpoint before installation. NodePort access to the API and Dashboard is plain HTTP: the Dashboard NodePort is not a standalone entry point but the upstream of a same-origin proxy that routes/api/*to the API, and that proxy should terminate TLS unless it is on a trusted private network.
Fixes
-
An expired or invalidated etcd authentication token no longer requires a gateway restart. The etcd client authenticated once at connect and never again, so a token whose lifetime elapsed while the connection was idle — the normal state of a gateway with stable configuration — or one invalidated by a change to etcd's authentication store left every later call refused until the process was restarted. The gateway now discards the connection, re-authenticates, and retries the call once, on both the configuration read path and the admin read surface. Credentials etcd genuinely refuses are still reported as such and are not retried in a loop.
-
The gateway no longer loses its last configuration snapshot when stopped shortly after an update. Snapshot-cache writes were spawned detached and nothing waited for them, so an instance stopped soon after applying a change could exit with the write unfinished and restart without its last-known-good snapshot. In-flight writes are now drained at shutdown, bounded at five seconds. Snapshot writes also commit in apply order: a write for an older revision no longer overwrites a newer one, so a gateway restarted after a burst of updates cannot come back onto a snapshot it had already moved past. The graceful-drain sequence, the drain window, and
preStopbehavior are unchanged. -
A configuration watch that is never confirmed is now detected. An etcd that answered range reads but never confirmed the watch left the gateway serving its first snapshot forever, blind to every later change, while
/status/configstill reported it connected. Whenrequest_timeout_msis set, an unconfirmed watch creation is aborted and retried on the existing backoff. -
MCP access-control reprojection now covers every affected row. The reprojection that refreshes MCP access control on upgrade paginated in a way that could skip rows, leaving those rows projected in the retired shape the flattening was meant to replace. It now walks every API key and MCP policy explicitly. Upgrading the control plane performs one additional projection write per current API key and MCP policy. No API, configuration, or resource shape changes, and no operator action is required.
-
The On-Premises control-plane chart no longer waits for the bundled PostgreSQL when an external database is configured. Setting
postgresql.builtin=falsedisabled the bundled server, but the API and Data Plane Manager still ran a PostgreSQL readiness init container built frompostgresql.image.*, so in a private network an unavailable PostgreSQL image could block startup even with a healthy external database. The readiness init is now rendered only whenpostgresql.builtin=true. Bundled deployments are unchanged and remain the default. Operators using an external database must ensure it is ready before the control plane starts.
API Changes
The OpenAPI structural comparison reports no breaking changes. All additions are optional and confined to the A2A agent surface.
| Endpoint | Change |
|---|---|
| A2A agent representations add optional forward_client_headers. |
| Accept optional forward_client_headers, an array of header-name glob patterns; [] clears an existing list. Entries are validated against the same rules as the other forwarding surfaces — RFC 7230 token grammar, at most one * per entry, and a 64-entry cap — and a pattern that could only ever name a header this surface never relays is rejected with 400. A PATCH carrying only forward_client_headers is a valid update. |
| Successful save responses may include warnings when gateways in the environments the agent is exposed to run a version that will not execute the saved forward_client_headers. The warning shape is the existing CompatibilityWarning; this is the first release in which an A2A save can carry one. |
One guarantee narrowed without any shape changing, and it is worth reading if you
built against the A2A agent's auth_type. Through 1.0.0 the resources-file schema for
that field said the gateway's credential is never forwarded from or exposed to the
calling client. It now says the credential is not exposed to the caller unless
forward_client_headers names the slot auth_type fills, in which case the agent
receives the caller's credential in place of the gateway's. The Admin API states the
same rule, which it did not describe before this release. The field's shape and enum
values are unchanged on both surfaces.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 1.1.0, including an upgrade that passes over it on the way to a later release.
- Check your
etcd:block before upgrading.etcd.dial_timeout_msandetcd.request_timeout_mstake effect for the first time in this release, and the previous release shipped both of them active at5000inconfig.example.yamlandconfig.managed.yaml. A configuration file copied from either therefore acquires a live 5-second bound on the configuration range read. A deployment whose configuration set does not load within that bound fails the read and retries it, and — because the gateway now binds its proxy listener only after a successful apply — does not begin serving. Removerequest_timeout_msunless you specifically want a slow etcd to fail fast, and note the trade-off: the unconfirmed-watch abort described under Fixes only takes effect whilerequest_timeout_msis set. If you keep the key, size it against your own configuration set rather than the number that used to be in the example.dial_timeout_ms: 5000carries no comparable risk. The shipped examples now leave both keys inactive, and unset means unbounded. - Check
api.service.nodePortandui.service.nodePortif your control-plane Helm values file already sets either. Both keys were ignored until this release and are now rendered, so upgrading the chart can move a port that Kubernetes had been allocating dynamically. Remove the key to keep dynamic allocation.
1.0.0
Release date: September 4, 2026
This release makes guardrail behavior visible and configurable at the point where operators need to tune it. Semantic guardrails report the similarity they measured, the console can test a saved rule before it is attached to production traffic, and fail-open decisions are recorded consistently across the gateway. The release also applies one client-header forwarding contract to model routes, MCP servers, passthrough routes, and Realtime, improves reasoning controls across provider protocols, and makes routing and configuration failures easier to diagnose.
Behavior Changes
-
Model cooldown is now opt-in. A direct model with no
cooldownblock, or with a block whoseenabledfield is omitted, is no longer taken out of rotation after request failures. Setcooldown.enabled: trueto retain the previous behavior; the default timeout and trigger settings inside an enabled block are unchanged. This is a breaking behavior change for existing models that relied on cooldown without enabling it explicitly. -
Guardrails no longer scan the Files API as one opaque blob. Upload bodies, upload responses, and file downloads under
/v1/filesnow pass without input or output guardrail evaluation. The former whole-file scan could neither evaluate individual JSONL records nor write masked content back to the multipart body; it also decoded binary files with replacement characters. The request envelopes on/v1/batchesand/v1/fine_tuning/jobsremain guarded; per-record file screening is not part of this release. -
A body the gateway cannot scan now follows the guardrail that governs that side. The gateway refuses unreadable content only when at least one guardrail in scope both reads that side of the exchange and fails closed on it. A chain that does not read that side does not cause a refusal, and the request continues with an
unscannable_bodyrecord when every relevant guardrail is fail-open. Thefail_opensetting now applies tokeywordandpiiguardrails as well as remote guardrails. Held-back streamed output remains fail-closed when nothing scannable can be recovered, because the alternative would release buffered bytes that no guardrail evaluated. -
Audio transcription and translation responses that cannot be decoded now honor the output failure policy. With an output guardrail that fails closed, such a plain-text transcript returns
422 content_filterwithguardrail_unavailable; with a fail-open guardrail, the original bytes are relayed and the bypass is recorded. Transcript content that can be decoded is still scanned. -
Unsupported streaming requests fail before reaching the provider. A request with
stream: trueto/v1/completionsor/v1/images/generationsnow returns400 invalid_request_error. These endpoints do not relay streaming responses; previously they sent the request upstream, could repeat billable work through the retry budget, and eventually returned a decode-related502. -
Guardrail scope now treats reasoning by who supplied it. Reasoning content replayed by the caller is input and is scanned on Chat Completions, Responses, and Anthropic Messages, while reasoning generated by the model remains outside output scanning. Anthropic signed thinking blocks are inspected by block actions but are not rewritten by mask actions, because clients must replay those signed bytes unchanged.
-
Streaming error events now use their endpoint's native envelope. Guardrail errors on
/v1/messagesstreams use Anthropic'sinvalid_request_errortype, and/v1/responsesemits flat Responses API error events with top-levelcode,message,param, andsequence_numberfields. HTTP error envelopes are unchanged. -
New release images use immutable full-version tags. Stable releases publish
:X.Y.Z,:latest, and:sha-*; release candidates publish their full RC tag and SHA tag. New:X.Yand:Xaliases are no longer created. Existing abbreviated tags remain in the registries but are not advanced by this or later releases.
New Features
-
Semantic guardrails can be measured before and after deployment. The edit form has a dry-run panel that embeds probe text and shows the deny/allow score, configured threshold, closest example, and resulting verdict. Requests evaluated by a semantic guardrail add
guardrail_scoresto usage events, including requests that pass. The Logs page shows the score, threshold, direction, closest-example index, and embedding model; the usage API and CSV export carry the same record. Durable usage data contains the example index, never the example or screened text. -
Client-header forwarding now spans Provider Keys, MCP servers, passthrough routes, and Realtime. MCP servers and passthrough routes gain
forward_client_headers; the existing Provider Key setting now also reaches Realtime and all translated provider paths. On MCP and standard model endpoints it is an allowlist; on passthrough routes it restores a header the route would otherwise strip. Because the field can pass caller credentials and trace context, credential slots, including the AWS SigV4 request-identity headers, andtraceparent/tracestaterequire an exact name and cannot be selected by a glob. An exactly forwarded credential replaces the credential the gateway ordefault_headerswould otherwise put in that slot, never adds a second value. Amazon Bedrock remains the exception: its signer ownsauthorizationand the SigV4 headers and drops supplied values before signing. -
A direct model can normalize reasoning-effort values for its upstream. The new
effort_mappingobject performs one exact, case-sensitive string-to-string lookup after the final direct target is selected. Unlisted or absent values pass through unchanged, the mapping never adds a missing effort, and a mapped result is not looked up again. It covers Chat Completions, Responses, Anthropic Messages and token-count requests, native and cross-provider paths, streaming requests, Bedrock invocation modes, and direct targets selected through routing, semantic, or ensemble models. The setting is accepted only on direct models and is editable in the console.
Improvements
-
Semantic guardrails require a threshold chosen for their embedding model. A non-empty
deny_exampleslist now requiresdeny_threshold, and a non-emptyallow_exampleslist requiresallow_threshold; a direction with no examples requires no value. Existing rows that omitted a threshold are filled with0.75, preserving what they already enforced. The Dashboard test panel and the per-request scores provide the measurements needed to tune a replacement value. -
Fail-open outcomes are visible on every guarded proxy surface. Success and error usage events carry
guardrail_bypassed_reason, including a bypass that occurs before an individual guardrail executes. Theaisix_guardrail_bypasses_totalmetric now includesreason="unscannable_body";aisix_guardrail_blocks_totalalso includes fail-closed refusals raised before a timed guardrail execution. A2A and usage-less rerank or unsupported-capability paths emit zero-token usage events when needed to preserve guardrail attribution. -
Guardrail configuration failures are reported as configuration state. Rows that deserialize but cannot build at runtime now appear in
/status/config, in rejection metrics, and in managed heartbeat status, and disappear after the row is repaired. Writes reject JSONnullin PII, Presidio, keyword, and semantic configuration where the gateway could otherwise drop the whole guardrail. The upgrade migration repairs only stored nulls and missing values with safe defaults, validates the complete projected row, and leaves other malformed rows unchanged. -
Buffered output guardrails cover complete SSE frames. The Messages and Responses paths now handle CRLF framing, multi-line
data:payloads, missing final terminators, and non-SSE JSON replies to streaming requests. Content that cannot be structurally masked is removed or refused instead of being released unread, while maskable multi-line frames retain their complete payload. -
Reasoning controls survive protocol translation. Anthropic
output_config.effortand adaptive/disabled thinking map to the corresponding OpenAIreasoning_effort; OpenAI effort values map to Anthropic's currentoutput_config.effortform. Anthropic structured-output declarations are also translated to OpenAIresponse_formatwith a strict JSON schema instead of being discarded. -
Routing and metrics expose more of the work the gateway performed. A routing target removed for cooldown or background health now produces a throttled warning naming the target and reason, and failed-attempt warnings are consistent across Chat Completions, Messages, token counting, and Responses. Native and translated Responses streams now report both TTFT metric families, and ensemble requests use one consistent usage estimate for quota, usage events, and token metrics.
-
The console preserves resource references instead of asking operators to retype them. Existing-resource fields use searchable selectors, fields that may legitimately contain a new value keep editable suggestions, and a stale reference remains visible and is not silently cleared by an unrelated edit. In Logs, typing a requested-model filter remains a case-insensitive substring search, while selecting a known model performs case-sensitive equality in both the feed and CSV export.
Fixes
-
Response model names are consistent on rerank and Realtime. A Jina-shaped
/v1/rerankresponse andsession.created/session.updatedRealtime events now report the gateway model name the caller addressed rather than the provider's model id. A full Realtime session object sent back insession.updateis translated to the upstream id again without changing a separately configured transcription model. -
Unsupported provider capabilities no longer spend the retry budget. Missing completions, embeddings, or image-generation support is classified explicitly and returns the existing
501response without retrying an adapter capability that cannot change between attempts. -
Wildcard model references are validated before they become inert or widen access. Renaming a referenced model into a wildcard alias is refused when it would corrupt an exact-name reference or silently turn an API key's single-model grant into a namespace grant. A semantic guardrail cannot newly select a wildcard alias as its embedding model. A cache policy may select a concrete model name served by a wildcard alias, but cannot select the wildcard pattern itself. Previously stored references are validated again only when the referenced value changes.
API Changes
The OpenAPI structural comparison reports no breaking shape changes. However, several
validation changes below are semantic breaking changes: a request that was previously
accepted can now return 400. Callers should review them as well as the additive
fields.
| Endpoint | Change |
|---|---|
| Model representations add optional effort_mapping and document cooldown.enabled: false as the default. |
| Direct models accept optional effort_mapping, and successful responses return it when configured; null or {} clears it on update, while non-direct kinds reject it. An omitted cooldown block or enabled value means cooldown is off. PATCH rejects a rename into a wildcard alias while an exact-name reference or API-key grant would be corrupted. |
| A semantic example list requires its matching threshold; semantic embedding models cannot newly name a wildcard alias; JSON null is rejected in the affected semantic, PII, Presidio, and keyword config fields. A full replacement config on PATCH must include every threshold required by its example lists. |
| A model:<name> selector may name a concrete address served by a wildcard alias, but the selector itself cannot be a wildcard pattern. Existing values are validated again only when changed. |
| Existing request.forward_client_headers and request.default_headers validation now accepts credential slots, rejects transport-breaking and x-aisix-* names, and applies the exact-match and collision rules described above. Some formerly accepted but inert header entries must be removed before a full request-overrides block can be saved again. |
| Requests and responses add optional forward_client_headers; null clears it on update. |
| Requests and responses add optional forward_client_headers. POST /mcp_servers/{mcp_server_id}/approve and /reject also return the field as part of the MCP server representation. |
| Adds optional boolean query parameter requested_model_exact; response items may include guardrail_scores. |
| Adds optional boolean query parameter requested_model_exact; exported items gain guardrail_scores. |
Upgrade Notes
These notes apply when you upgrade from any release earlier than 1.0.0, including an upgrade that passes over it on the way to a later release.
- Cooldown opt-in is a breaking behavior change. Before upgrading, set
cooldown.enabled: trueon every direct model that should retain the pre-1.0 implicit cooldown behavior. - Treat
/v1/filesas outside guardrail coverage in this release. Do not rely on a whole-file keyword, PII, or remote-guardrail decision for uploads or downloads. - Review the hooks and failure policy on every guardrail. A body that AISIX cannot
scan and that a relevant fail-closed guardrail governs is still refused, but one
that no relevant fail-closed guardrail governs can now continue. In particular,
fail_open: truenow takes effect forkeywordandpiiguardrails and can newly forward a request or response that AISIX cannot scan. - Clients that replay reasoning content can now be blocked by input guardrails. Model-generated reasoning is no longer evaluated by output guardrails, and a mask action does not rewrite Anthropic signed thinking blocks.
- Handle
422 content_filterwhen transcription or translation response bytes cannot be decoded and an output guardrail fails closed. With fail-open, the original bytes can now be relayed without content evaluation. - A
stream: truerequest to/v1/completionsor/v1/images/generationsnow returns400 invalid_request_errorwithout contacting the provider, instead of eventually returning a decode-related502. Update status-code handling and retry policies. - Update custom stream parsers for native guardrail error envelopes: Messages streams
use Anthropic's
invalid_request_error, while Responses streams putcode,message,param, andsequence_numberat the top level. - Review stored guardrails containing JSON
nullbefore upgrade. A safely repairable row that the previous gateway skipped can begin enforcing after the migration; a semantic row missing only a threshold keeps its existing0.75behavior. - API clients that create or replace semantic guardrail configs must include the threshold corresponding to every non-empty example list. The threshold and null checks are semantic breaking Admin API changes even though the OpenAPI shapes are not structurally breaking.
- Review automation that renames models or writes model references. A rename into a wildcard alias is rejected when it would corrupt an exact reference or widen an API key grant; semantic guardrails cannot newly use a wildcard alias as their embedding model; cache policies may name a concrete model served by an alias but not the wildcard pattern itself.
- Review Provider Key
forward_client_headerspatterns used with Realtime: 1.0.0 starts honoring the setting on that surface. Credential slots andtraceparent/tracestatemust be named exactly; broad globs do not select them. Remove rejected transport,x-stainless-*,anthropic-version, orx-aisix-*entries before saving a full overrides block. On Bedrock-compatible endpoints, a forwarded caller value now wins a collision withdefault_headersin thex-api-key,api-key,x-goog-api-key,proxy-authorization, andcookieslots. Bedrock's signer still ownsauthorizationand the AWS SigV4 headers and discards caller-supplied values. - Update image references or automation that expected a newly published
:X.Yor:Xalias to use the full version or:latestinstead. - If an alert is based on
aisix_guardrail_bypasses_total, account for the newreason="unscannable_body"series and the additional events now included in the total.
0.13.0
Release date: September 1, 2026
This release is about telling traffic apart and reaching an upstream on its own terms. A Provider Key can now declare which API surfaces its endpoint serves natively and where each one lives, so one credential reaches both the OpenAI-compatible path and the Anthropic-compatible path an upstream serves, without a second key or a second model. Every usage record now names the work that was asked for, so an image generation and a video submission are no longer the same undifferentiated OpenAI stream in an exporter or in Logs. A caller the gateway rate-limits gets a response that says what the cap was and when to come back. And a response now reports the gateway model name the caller addressed on every endpoint that serves one, rather than the provider's own id on some of them.
Behavior Changes
-
Existing DeepSeek provider keys reach DeepSeek's own routes after the upgrade, instead of being translated. AISIX now carries a verified declaration of the API surfaces DeepSeek serves — its native
/v1/responsesand its Anthropic-compatible/anthropic/v1/messages— and applies it once, at upgrade, to provider keys that have no declaration of their own, use thedeepseekprovider, and still point athttps://api.deepseek.com. Those keys reportapis_source: catalog. What changes for traffic: a Responses API request is forwarded to DeepSeek's own/v1/responsesrather than translated to chat completions, so reasoning output items survive; and an Anthropic-wire request reaches the Anthropic-compatible path, so prompt-cache breakpoints and thinking blocks survive. A key you declared yourself is left untouched. -
A client reading the
modelfield off a response now sees the gateway model name it asked for, on/v1/messages,/v1/responses,/v1/completionsand/v1/embeddings— streamed and buffered alike — and on/v1/videospolls. These paths previously returned the provider's own id, so a model aliasedgpt4o-minicame back asgpt-4o-mini-2024-07-18, while/v1/chat/completionsalready returned the alias. Passthrough routes are unchanged and still relay the provider's response as-is. -
A gateway rate-limit rejection now carries response headers. A 429 the gateway itself produces describes the single limit that refused it:
x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset,x-ratelimit-scopeandRetry-After.x-ratelimit-resetandRetry-Afterare both a delta in seconds. A concurrency rejection now carries a retry hint where it previously carried none. These headers are added only when the gateway refuses the request; a 429 relayed from an upstream is passed through as the upstream sent it. The per-dimensionx-ratelimit-limit-requests/-tokens/-concurrentheaders on successful responses are unchanged. -
A Prometheus gauge whose key no longer exists stops reporting its last value.
aisix_budget_limit_usd/_spent_usd/_remaining_usd/_reset_seconds/_details_presentandaisix_ratelimit_remaining_{requests,tokens}are written only from the request path, so deleting an API key used to freeze its series at whatever it last read — and an alert on budget exhaustion kept firing for a credential that no longer exists. Such a series is now retired toNaN, which Prometheus treats as no value rather than as zero. An alert that compares these gauges no longer matches a deleted key. If you built a dashboard panel that reads a raw value, expect a gap rather than a flat line after a key is deleted.
New Features
-
Native API surfaces on a Provider Key. One upstream account often exposes more than one protocol, on different paths of the same host, under one credential — DeepSeek, Zhipu and Kimi each front an OpenAI-compatible path and an Anthropic-compatible one.
api_basecan name only one of them, so everything else was translated, losing what the target protocol carries and the canonical chat shape does not. A Provider Key now takes anapisblock naming each surface and, optionally, the base URL it lives at:{"apis": {"responses": {},"messages": { "base": "https://api.deepseek.com/anthropic" }}}The two surfaces resolve on different terms, because the evidence for them differs.
messagesis additive: a key whose adapter is alreadyanthropickeeps serving/v1/messagesnatively whatever the map says, and listing it here adds the route to a key whose adapter is something else.responsesis authoritative: once the block exists,/v1/responsesis served natively only if it is listed — which is how an operator says "this endpoint has no Responses route" and gets the request translated to chat completions instead of 404'd upstream. With no block at all, both fall back to what the gateway inferred before:/v1/messagesfrom the vendor id or theanthropicadapter,/v1/responsesfrom the vendor id alone. Surfaces the block has no key for — embeddings, audio, images, videos, files, batches, fine-tuning, rerank — always useapi_base, exactly as before. Set the block to{}to declare that the endpoint serves nothing beyond its adapter's own surface, or tonullto clear the declaration. Configurable in the console on the Provider Keys page, and over the Cloud Admin API onPOSTandPATCH /provider_keys. -
Usage records name the operation that was asked for. Every usage event now carries
operation— the kind of work a request asked for, chosen by the route it matched and never by caller text:chat,completions,messages,count_tokens,responses,embeddings,rerank,realtime,image_generation,image_edit,transcription,translation,speech,video_generation,files,batches,batch_completion,fine_tuning,mcp,a2aandpassthrough.inbound_protocolreportsopenaifor every OpenAI-shaped route, so nothing could previously separate a text chat from an image generation or a video submission short of a regular expression over a captured prompt — which ametadata_onlyexporter has no prompt for, and which a zero-token video submission does not answer at all. The field reaches your own exporters (OTLP, Datadog, SLS) and the console: Logs filters on the operation and marks each request with it — the conversational endpoints (chat,messages,responses,completions) are left unmarked as the common case — andGET /usage_eventsandGET /usage_events/exporttake anoperationquery parameter and return the field on each item.
Improvements
- Usage-event and budget metrics name the member, not just their id.
aisix_usage_events_emitted_total,aisix_usage_event_drops_totaland the fiveaisix_budget_*gauges now carryuser_namebesideuser_id, matchingaisix_proxy_requests_totaland theaisix_llm_*families. A usage-event alert or a budget dashboard can name the person it is about without a lookup outside Prometheus.
Fixes
aisix_budget_remaining_usdis now published. The gateway could only emit this gauge when the control plane stated the remaining amount in its budget decision, and the decision never carried it — so a series the metric reference documents existed in no 0.12.0 or earlier deployment, and a dashboard panel or alert built on it matched nothing. It now appears alongsideaisix_budget_limit_usdandaisix_budget_spent_usd, on the same labels and at the same moments. Upgrading the control plane is enough; no gateway change and no configuration.- The sign-in error for a rejected origin no longer misstates what the server
trusts, or points you at the wrong component to restart. It said the server
trusts only the one address it is configured to serve on; it also trusts that
address's
localhost/127.0.0.1twin and every origin inAISIX_TRUSTED_ORIGINS, and the message now offers that second remedy. It also told the operator to restart cp-api, where the check that rejected them runs in the dashboard. - The Helm chart's post-install notes no longer name an address the port-forward they tell you to run does not serve, and carried the same overstated claim about trusted origins.
API Changes
No breaking changes. All additions are optional.
| Endpoint | Change |
|---|---|
| New optional request property apis. The 201 response may now carry warnings when a data plane in scope runs a gateway too old to execute a declared surface. |
| New optional request property apis; null clears the declaration, {} declares that the endpoint serves nothing beyond its adapter's surface. Response adds apis, apis_source and warnings. |
| Response adds apis, apis_source (catalog or operator) and warnings. |
| New optional operation query parameter; each item gains operation. |
| New optional operation query parameter; each exported item gains operation. |
Note that apis is not echoed by the create response, the same as api_base —
read it back with GET /provider_keys/{provider_key_id}, whose apis_source
reports catalog when AISIX supplied the declaration rather than you.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 0.13.0, including an upgrade that passes over it on the way to a later release.
- The control plane applies the curated API-surface declaration once, on the first
boot after the upgrade, to the provider keys described under Behavior Changes.
It is best-effort and idempotent: a boot that cannot complete it retries on the
next one, and a key that already carries a declaration is never touched. Set
apisyourself on any key whose endpoint you do not want reached natively. - If you alert on
aisix_budget_*oraisix_ratelimit_remaining_*, review the expressions against the gauge-retirement change above before upgrading. - A gateway still running a release before 0.13.0 records no
operation, because the gateway is what derives the value from the matched route. While such a gateway is in the fleet, filtering by operation — in Logs, onGET /usage_events, or on an export — excludes its traffic from the results. Its requests are proxied and recorded as before; only the operation filter cannot see them. Upgrading the gateway resolves it for traffic from that point on; rows already recorded without an operation stay outside the filter.
0.12.0
Release date: August 30, 2026
This release makes two contracts say what they mean. The Cloud Admin API now covers everything the console does, ships as an immutable document per release, and validates what it documents. And a guardrail now governs exactly what you attached it to — the old "attached to nothing means everywhere" rule is gone from the gateway, the console, and the upgrade backfill alike, without changing what any existing guardrail enforces. Alongside that, token counts are reported in the protocol the caller asked in, thinking tokens on Gemini models are counted at last, usage records name the person behind the credential, and several ways a delete could leave a live reference behind are closed.
Behavior Changes
- A guardrail's scope is now exactly its attachments; one attached to nothing inspects no traffic. A guardrail with no attachment rows used to be applied to the entire environment at the lowest priority. Keying on the absence of rows is what made that dangerous, because narrow scoping is expressed by deleting the environment attachment: a guardrail scoped to a single model silently widened to every request in the environment the moment that model was deleted. Upgrading preserves what your guardrails enforce today — the control plane converts the old implicit environment scope into an explicit environment attachment as part of the upgrade, before the gateway stops honoring the implicit one. It also returns guardrails that a previous boot had wrongly widened to environment scope back to the narrow scope you configured. What changes from here on is that a new guardrail governs nothing until you attach it. Declaring an unattached guardrail is still not an error: its scope target may simply have been deleted. In the console such a guardrail now reads Not attached — "Attached to nothing, so it inspects no traffic" — where it used to read Global / "Applies to all models", and the gateway logs a warning naming any enabled guardrail that nothing attaches. In the same vein, opening a narrowly scoped guardrail in the console to change something unrelated and pressing Save no longer writes an environment-wide attachment: the editor now represents "attached to nothing" as its own state instead of defaulting to environment scope.
- Twenty-one control-plane routes the dashboard already used are now part of the Admin
API contract, and requests to them are validated. They were reachable with an admin
token but had no published schema; they now appear in the API reference. Bringing a live
route into the contract also turns request validation on for it, so a request body
carrying a field the schema does not declare is rejected with
400instead of being accepted and ignored —POST /teamswith a stray field returned201before this release and returns400 additional properties not allowednow. If you have automation calling the usage feed, its export, the usage summary or request-level metrics, teams or team members, model price overrides, invitations, member removal, or the notification delivery log, confirm it sends only documented fields. The routes are listed under API Changes. - Token counts in a response are now reported in the accounting the caller's own
protocol uses. The two protocols disagree on exactly one thing — whether prompt-cache
counters live inside the input count or beside it. On the OpenAI protocol
prompt_tokensis the full input with cached tokens included; on the Anthropic protocol the input count excludes them andcache_creation_input_tokens/cache_read_input_tokenssit beside it. The gateway used to copy through whichever shape the upstream reported, so a client speaking one protocol in front of an upstream speaking the other was answered in the other protocol's accounting: an OpenAI-protocol client over an Anthropic upstream sawprompt_tokens: 40alongsidetotal_tokens: 2104, arithmetic that does not add up and a cache hit reported nowhere. The same request now reports2088and2104with the hit underprompt_tokens_details.cached_tokens. Where that conversion happens,total_tokensis recomputed from the projected input and output counts rather than echoing a total that was computed under the other accounting; where no conversion is needed, the total the upstream reported is passed through unchanged. Streaming, non-streaming and/v1/responsesare all covered. The recorded usage event is unchanged, so billed and historical cost are unaffected — only the counts reported back to the caller move. - OpenAI-protocol responses can now carry a cache-write count.
prompt_tokens_details.cache_creation_tokens(andinput_tokens_detailson/v1/responses) reports prompt tokens the provider wrote into its cache this turn. It is not an OpenAI field — OpenAI has no cache-write concept — but without it an Anthropic or Bedrock cache write is an unexplained rise inprompt_tokensthe caller cannot price, and a write is billed above the plain input rate. It is emitted only when non-zero, so a strict client that rejects unknown fields should be checked. - Thinking tokens on Gemini models are now counted, and billed. Gemini reports
thoughtsTokenCount, which the gateway parsed nowhere — so a thinking model'scompletion_tokenswas recorded short by its thinking tokens andreasoning_tokensread0on every protocol. Both are now carried, withreasoning_tokensnaming the subset ofcompletion_tokens, and inclusiveness decided from the total the upstream itself reports, so nothing is double-counted where Gemini already folds thinking tokens into its own candidate count. Cost chargesreasoning_tokensat the reasoning rate, or the completion rate where no reasoning rate is set. If you run thinking Gemini models, recorded completion tokens and cost will rise — to match what Google already bills you for and the gateway was not recording. Unlike the protocol conversion above, this one does change the usage record. /v1/messages/count_tokensnow emits a usage record. The route emitted none on any outcome, so a request that an input guardrail refused was correctly refused and then impossible to find in the logs — including in the Guardrail blocks view. It still bills nothing:prompt_tokensandcompletion_tokensare zero on both the answered and the refused path. "Not metered" and "not recorded" were being treated as one statement; they are two.- A delete that would leave a live reference behind is now refused, repaired, or
reported. Deleting a model named as a semantic guardrail's embedding model returns
409, and renaming that model rewrites the guardrail rather than orphaning it. The stakes were high, because guardrails fail closed: before this release that delete succeeded, and every request in the guardrail's scope then answered422 semantic_embed_unresolved. Deleting an API key that a claim mapping resolves to returns409as well, matching how a passthrough route's anonymous key is already protected — a dangling mapping fails closed but is not free, because matching takes the highest-priority rule without falling through, so a broken one masks a lower-priority rule that still works. Deleting a team or removing a member now clears the ownership it leaves on their caller keys and publishes that to the gateway. Both columns were cleared by a database foreign key, which emits no projection event, so the gateway kept reading a document naming a team that no longer exists — and a rate limit scoped to the deleted team went on throttling those keys with nothing in the console to explain the429s. Deleting an MCP server no longer strips the per-key rate limits that name it: it used to drop those while keeping the tool grants, so re-creating a server under the same name — a supported path — restored the permissions and silently lost the limits. Deleting an OIDC provider still succeeds — both of its carriers hold a name, a rename is a delete plus a re-create, and dangling fails closed and self-heals — but the response now reports how many API keys and claim mappings still name it.
New Features
- The Cloud Admin API is published per release. Every release tag now publishes an immutable OpenAPI document generated from that tag's own source, and the API reference serves a page per released version together with a generated changelog between adjacent versions. You can read the API your deployment exposes and diff it against the version you are upgrading to, rather than against a document that tracks development.
- Usage records and logs are attributed to the org member, not only to the credential. A usage record named the API key a request arrived on but never the person behind it, so "show me this member's errors in the last 24 hours" meant enumerating every key they own and merging the results by hand — and a member calling through both an API key and OIDC was split across two identities no single filter could join. Usage records now carry the member and the Logs page filters by member. The attribution is a snapshot taken when the request is served: re-assigning or deleting a key does not rewrite the history it already produced.
- Logs can be filtered by an exact status code. Filtering previously offered status
families only, so there was no way to ask for
429without also getting every other4xx. Exact codes and ranges such as500-599are now accepted. aisix_usage_events_emitted_totalandaisix_usage_event_drops_totalcarry the member and the exact status. Both gain auser_idlabel and astatuslabel holding the response code itself, so "how many of this member's requests were rate limited" and "did this member lose any usage records" are answerable from metrics. The existingstatus_codefamily label is unchanged, so alerts built on it keep working — but a dashboard that aggregates these two counters without naming its labels will see the new dimensions.
Improvements
- The usage page no longer reports zeros while it is still loading. Before the first
response arrived,
/usagerendered$0.00,0requests and0tokens under two tables reading "No usage in the last 30 days.", then replaced it all seconds later. On a slow connection an operator was told, definitely, that their gateway had seen no traffic. It now shows—with a "Loading…" caption, matching the overview page. - The offline installer reports the version it installed and the port it actually published. An offline deployment had no way to confirm on the way out which release it had just installed, and when the port came from the environment rather than the default, the closing banner still printed the default — so an install listening on one port told the operator to open another.
- The reference now spells out how a custom PII rule's replacement text behaves.
$1is not expanded as a capture-group reference, and a pattern that has capture groups replaces only the first group rather than the whole match — soreplacement: "ACCT-$1-****"againstACCT-([0-9]{4})-([0-9]{4})yieldsACCT-ACCT-$1-****-5678, which looks redacted while leaving the original in place. The console now warns when a replacement contains$.
Fixes
- Two control-plane replicas booting at the same time no longer crash each other. Schema migration was check-then-act throughout — probe a catalog, then create or alter — which PostgreSQL does not serialize, so a high-availability install could fail a pod on startup across several distinct error codes. Boot-time schema work is now serialized; eight replicas started simultaneously against a fresh database all come up, where roughly seven in eight used to fail.
- The upgrade backfill no longer re-widens a model-scoped guardrail. The backfill that gives pre-attachment guardrails an environment-scoped attachment ran on every control-plane boot and identified its targets as "has no environment attachment" — a predicate written when that could only mean "predates attachments". Once scoping a guardrail to a model began deleting its environment attachment, every restart quietly widened it back to the whole environment.
- Deleting a guardrail now retracts its attachments from the gateway, and upgrading cleans up the ones earlier deletes left behind. The attachment rows were removed by a database cascade, which emits no projection event, so the attachment documents stayed live and every gateway logged an unknown-guardrail warning on each index rebuild, indefinitely. Upgrading retracts those leftovers, and the warning stops on its own — no manual cleanup is needed.
API Changes
Twenty-one routes enter the contract this release. They are newly documented, not newly added — each was already reachable with an admin token; what changes is that they now have a published schema and their requests are validated (see Behavior Changes).
| Route | Change |
|---|---|
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Adds 409 — the key is resolved by a claim mapping |
| 200 response adds an optional warnings property reporting API keys and claim mappings that still name the provider |
| 409 was already documented; it now also covers a model named as a semantic guardrail's embedding model |
No previously documented route changed its request or response shape in a breaking way. Four semantics that are easy to guess wrong are now stated explicitly, and are worth checking your integration against:
usage_summarycounts distinct requests, not upstream attempts — a request that retried across three attempts counts once.ModelPricing.sourcecan besnapshoton an offline deployment, not only the values a connected deployment produces.- Six of the usage filters are case-insensitive substring matches;
provider_labelandrequest_idmatch exactly. - Caller API key ownership takes the membership identifier from
GET /members, not the member's user identifier — a correction to what the previous reference said. The user identifier is rejected.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 0.12.0, including an upgrade that passes over it on the way to a later release.
- Guardrails need no action. The scope change preserves what your guardrails enforce today: the upgrade writes the implicit environment scope out as an explicit environment attachment, and returns any guardrail a previous boot had wrongly widened to its configured narrow scope. It also retracts attachment records that earlier deletes left behind, so the gateway's unknown-guardrail warnings stop on their own. Guardrails created after the upgrade govern only what you attach them to.
- If you call the control-plane API directly, check the twenty-one newly contracted
routes. Requests to them are now validated against a published schema, so an
undocumented field that used to be accepted and ignored is rejected with
400. - Expect recorded cost to rise on thinking Gemini models. Their thinking tokens were
not being counted at all; they now appear in
completion_tokensandreasoning_tokensand are charged. Budgets and cost alerts calibrated against the previous, understated figures should be reviewed. No other model or provider is affected. - Check dashboards that aggregate
aisix_usage_events_emitted_totaloraisix_usage_event_drops_total. Both gained auser_idand astatuslabel, which changes the cardinality of an unlabelledsum by (...). Thestatus_codefamily label is unchanged.
0.11.0
Release date: August 28, 2026
This release is about guardrails. Screening gains two new kinds — one that decides by meaning rather than by pattern, and one that runs a script you write — and the whole subsystem becomes accountable: a guardrail that refuses now says whether your policy matched or your rule could not run, and every enforced hit reaches the logs and the usage record. Alongside that, a gateway rolls over without dropping traffic, prompt-cache usage is billed and measured correctly on the Anthropic protocol, and several ways an operator could quietly lose data or ship a non-upgrade are closed.
Behavior Changes
- Input guardrails now run on every request that reaches a provider. Previously the
chain was consulted only where a request carried text the gateway had already extracted,
so a request with no text — an argumentless MCP tool call, a transcription with no
prompt, an image edit with no prompt, an embedding of an empty string — reached the
upstream without being screened, and
/v1/messages/count_tokensand/a2a/:agentwere never screened at all. What varies per route is the text offered to the chain, not whether the chain is consulted. If you rely on a guardrail to gate egress, it was not covering these paths before this release. A body the Anthropic parser cannot read is now refused rather than forwarded, when a guardrail is attached. - A guardrail refusal is now recorded on the usage event that reports it. The flag the Logs Guardrail blocks view filters on was set by only two of the fourteen surfaces that can refuse, so that view could read empty while callers were being refused — which looks like the gateway recording no guardrail activity at all. Every surface now sets it.
- A guardrail that cannot run no longer claims your content was blocked. When a
screening rule fails — an unreachable provider, a script that returned something that
is not a verdict, a timeout — the refusal is now reported as one. The caller gets
422with"code": "guardrail_unavailable"and a message naming the guardrail and the cause (request rejected: guardrail 'x' could not evaluate it (...)), instead of therequest blocked by content policya real match returns. The gateway logs a warning, andaisix_guardrail_latency_secondscarrieserror_typeset to the cause. The HTTP status anderror.typeare unchanged, so an alert on those keeps counting both. This applies to every guardrail kind on every endpoint.aisix_guardrail_blocks_totalcounts both, as it always has — it carries no labels, so it cannot separate them, and it is recorded on the chat-completions path only. To count policy blocks, useaisix_guardrail_latency_seconds_count{result="blocked", error_type="none"}; the same series witherror_type != "none"gives the rule failures. Do not select onerror_typealone — that histogram records every guardrail execution, allowed requests included. - Guardrails fail closed by default.
fail_opennow defaults tofalseon both planes: a rule that cannot reach its provider refuses the request rather than passing it through unscreened. Setfail_open: trueper guardrail to keep the old behavior. The same default governs a script fault. - Content larger than a provider's limit is split, not clipped. An over-limit body used to be truncated and screened partially, which is a silent hole in a control that exists to see everything. It is now split into chunks and screened whole — every chunk is submitted, with no cap on how many, because a chunk budget would be unscanned content through the back door.
- The
mandatoryflag is gone. It duplicated whatfail_openalready expresses. Remove it from stored configurations; a guardrail that must not be bypassed is one withfail_open: false. - The removed
/passthrough/*tunnel now answers a plain404. 0.10.0 replaced the implicit tunnel with explicit passthrough routes and left a410 Gonemigration pointer for one release. That release has shipped, so unclaimed/passthrough/*paths take the router's ordinary miss path and the namespace is entirely yours to claim with explicit routes. The accompanying warning log and theprovider="unresolved"metric series are gone with it. /v1/messagesreports a prompt-cache hit from an OpenAI upstream. When an OpenAI-compatible upstream serves an Anthropic-protocol request, the cached prefix is no longer folded intoinput_tokens: the client receivesinput_tokens= non-cached input andcache_read_input_tokens= the hit. The usage record does not convert: it keeps the OpenAI shape, whereprompt_tokensis still the whole prompt andcached_prompt_tokensis the cached subset of it rather than a separate amount to add — and that is what lets Logs, the Usage API, CSV export and billing price the cached portion at the cache-read rate. A streamed response reports it in the closingmessage_delta. The counter is emitted only when non-zero. Previously the whole prompt billed at the uncached rate.- Twelve metric families gained an
upstream_protocollabel. It names the protocol that actually served the request, which a cross-protocol conversion made invisible before. Aggregating queries are unaffected; dashboards, alerts and recording rules that match labels exactly will need updating. - Stopped gateways read as offline. Node liveness is now decided by the control plane
rather than in the browser, and
DpNodecarries astatusfield (healthy/warning/offline) derived from heartbeat age. A stopped instance no longer renders as healthy until the page is reloaded, and the Data planes view updates on its own. GET /environments/{env_id}/mcp_policyreturns200with a null policy when an environment has none, instead of404. A404from that endpoint now means only that the environment does not exist.- A notification channel's URL can no longer be destroyed by writing it back. Read
responses mask the URL, because the path of a webhook is its credential. That masked value
used to be accepted as a write, so any read-modify-write silently replaced the real
webhook and returned
200. An echoed mask is now a no-op, a mask naming a different host is rejected, and creating a channel with a masked URL is refused. Delivery errors no longer echo the remote response body, which could contain the URL.
New Features
- Semantic guardrails. A screening rule can now decide by meaning instead of by pattern: give it example phrases to allow and to deny, and it scores each message against them with an embedding model you nominate, with separate thresholds per side. It screens each message individually and can be pointed at user messages or the whole conversation.
- Custom script guardrails. For a policy the built-in kinds cannot express, write the
check yourself: a script the gateway runs in a sandbox, with a timeout you set and
secrets you register by name rather than inline. The hook returns
{action: "none"}(or"allow") to pass,{action: "block", reason, reason_code}to refuse, or{action: "mask", segments}to rewrite. Those four are the whole vocabulary — anything else is a script fault, reported as one. - PII patterns can scope to a capture group and choose their replacement. A custom pattern can redact just the part of a match that is sensitive, and name the string that replaces it, instead of masking the whole match with a fixed token.
- Enforced guardrail hits reach the logs and the usage record. Every LLM handler now records which guardrail acted on a request, so a blocked or masked call is attributable after the fact rather than only visible as a refusal. The Logs view distinguishes a policy block from a fail-closed outage.
validatereports guardrails that load but cannot run. A row whose configuration parses but does not build — an invalid regular expression, an unknown detector or action value, akind: customscript that does not compile — used to be dropped from the chain with only a warning in the log, leaving the screening it describes silently absent. It is now reported at validation time instead of at the first request. The check builds the configuration in process and makes no network calls, so it reports nothing about whether a provider is reachable, and it skipskind: semanticrows entirely — the embedding model such a row names is not checked.- A gateway retires HTTP/2 connections when it starts draining. On
SIGTERMthe gateway sendsGOAWAYto HTTP/2 downstreams as the drain window opens, so a client stops sending on that connection and reconnects elsewhere instead of racing the shutdown. HTTP/1.1 already receivedConnection: close. A draining gateway also stays live to its health probe, so an orchestrator does not kill it mid-drain. POST /v1/images/edits. Multipart image editing is now a typed endpoint with the same authentication, guardrails and usage accounting as the rest of the proxy.- Prompt-cache token counters and
upstream_protocolon the metrics. Cache reads and cache writes are now their own Prometheus counters, so the cached share of input is answerable from metrics instead of only from usage exports. - The gateway's trace id is stored with each request and can be interpolated into a
trace-viewer link via
{trace_id}intrace_ui_url_template, so a log line leads to the trace it belongs to.
Improvements
- Renaming an environment, and editing an embedding model, are now possible from the Dashboard.
- Every list footer accepts a page number, instead of only step-by-step paging.
- An invitation is now something a new user can actually accept: the link previews the organization before joining, joining is an explicit action rather than a side effect of opening the page, and the invitation is bound to the invited address.
- A read-modify-write of a guardrail's configuration round-trips. The fields describing a stored secret are accepted unchanged and rejected when altered, so scripting and infrastructure-as-code no longer have to strip them by hand. Editing a Bedrock guardrail no longer demands the AWS secret the API never returns.
- The offline package refuses to start over an existing data volume when
.envis missing, naming what would be lost, rather than regenerating secrets and crash-looping. AISIX_CLOUD_NOTIFY_ALLOW_PRIVATE_URLS,AISIX_CLOUD_MCP_SPEC_ALLOW_PRIVATE_URLS,AISIX_CLOUD_ALLOW_FRESH_BOOTSTRAPandAISIX_CLOUD_PRICESYNC_URLcan now be set in the offline package's.env; previously they were documented but unreachable.- Usage is bucketed by UTC day whatever timezone the session is in, request totals are exact rather than sampled, and estimated usage is disclosed as estimated.
- Ordinary Dashboard browsing no longer exhausts its own authentication budget. A throttled token refresh now reads as "retry in a moment", not as being signed out — previously a signed-in user was told to sign in, and several other flows reported a failed session read as a lost session.
- Overview, Logs, Budgets and Observability no longer render partial or misleading figures while data is still loading.
- The documented on-premises upgrade now actually moves the version.
AISIX_VERSIONis owned by the package and reconciled on every start; previously it stayed pinned in.env, so following the documented steps left the old images running and reported success. Pin a version with the image variables instead. A downgrade is refused unlessAISIX_ALLOW_DOWNGRADE=1. - A control plane that fails to start now names the cause — wrong database password, wrong master key, mismatched key id, missing CA root — reading both the api and dpm logs, instead of leaving only "container is unhealthy".
Fixes
/v1/realtimeworks against OpenAI again. The gateway sent a beta opt-in header on every realtime connection; OpenAI's now-GA endpoint rejects it and closes the session before the client sends anything. The header is now forwarded only when the caller asks for it, so a legacy client still gets the beta shape and everyone else reaches the GA API.kind: customguardrails can be created from the Dashboard. The gateway did not advertise the kind in its capability list, so the Dashboard offered it as unsupported even though both planes could run it.- An OpenAI provider key with no
api_basefalls back to the vendor default on every route, not just some. - A no-op update to a provider key — sending back what is already stored — succeeds instead of failing with an internal error.
- Non-UTF-8 multipart prompt fields are rejected on every surface that accepts them.
- Per-item PII action overrides survive the Dashboard's edit form.
- API key rows written before the layered MCP rule keep loading. The retired
mcp_access.modeselector is ignored as an ordinary unknown field, so the row does not fail and the rest of the key'smcp_accessblock is read normally — a rejected API key row would stop that key authenticating all of its traffic, not just its MCP traffic. - Static OTLP exporter blocks that were never wired no longer log a false success.
Upgrade Notes
- Upgrade the control plane first, then your gateways. This is the supported order.
During the window, a gateway still on 0.10.0 will not run the new guardrail kinds
(
semantic,custom), the PIIreplacementfield, or the newtext_sourcevalues: it cannot parse the row and drops it whole, so the rule screens nothing there. The control plane still saves such a configuration — it does not refuse it — and returns awarningsarray on the write naming the field, the version that first reads it, and how many of your gateways are affected. Read those warnings: until every gateway is upgraded, the rule is not in force on the ones that are behind. - Re-enter any notification channel webhook URL that was destroyed before this upgrade. If you ever updated a channel through the API by reading it and writing the object back, the real webhook URL was replaced by its masked form and cannot be recovered — it exists only at the provider. After upgrading, a channel in that state reports it in its delivery error; re-enter the URL from Slack (or your provider) to restore delivery. Channels only edited through the Dashboard are unaffected.
- Re-check what your guardrails were actually covering. Two fixes in this release change the answer: the input chain now runs on paths it previously skipped, and a refusal is now recorded on every surface that can refuse. If a guardrail looked quiet before, that may have been the gap rather than the traffic.
- Update metric queries that match labels exactly. Twelve families gained
upstream_protocol. - A query that reads
aisix_guardrail_blocks_totalas policy volume is counting rule failures too. That counter has no labels and never separated the two, and it is recorded on the chat-completions path only — as isaisix_guardrail_bypasses_total. Withfail_opennow defaulting tofalse, a guardrail whose provider is unreachable refuses the request rather than passing it, so failures that used to land in the bypasses counter land in the blocks counter instead. For policy-block volume across the gateway, useaisix_guardrail_latency_seconds_count{result="blocked", error_type="none"}.
0.10.0
Release date: August 20, 2026
This release is about agent traffic. Forward-proxy traffic — an IDE or coding agent pointed at the gateway — is now carried by an explicit route resource you can audit instead of an implicit tunnel that guessed which credential to borrow. MCP gains the 2026-07-28 revision of the protocol, anonymous access for clients that carry no credential, OAuth 2.1 discovery, and a single coherent rule for which tools a caller may reach. Model groups gain consistent hashing and priority tiers. The gateway keeps serving through a drain window instead of refusing connections a load balancer has not yet stopped sending, and traces exported over OTLP form a real hierarchy.
Behavior Changes
- The implicit passthrough tunnel is replaced by explicit routes.
/passthrough/<provider>/<rest>no longer resolves by provider name; an unclaimed/passthrough/*path answers410 Gonewithendpoint_removedand logs a warning naming the caller, so un-migrated clients are findable. To migrate, create a passthrough route withpath_prefix: /passthrough/<provider>,target_urlset to the old API base, andprovider_key_idset to the key it used to borrow — client URLs then keep working byte for byte. Two behaviors differ from the old tunnel: a route is a single-attempt relay, so upstream 5xx and transport retries are gone, and because a route resolves no model, model cooldown is no longer cross-marked from this traffic. Access is an explicit grant on the caller key (allowed_routes); a key without one reaches no route. - MCP tool access is now one rule across three layers. The environment policy, the caller's team policy, and the key's own
mcp_accessblock each carryallowanddenylists. Allow lists intersect and deny lists combine, so no layer can widen what another restricts; a layer that is not set imposes no constraint, and a caller with no layer at all has no MCP access.modeis gone from both policies and keys,allowed_toolsis gone from the key — the key's layer ismcp_access.allow— and the legacy-key migration flow is removed. If you have configured MCP tool policies, re-check them after upgrading: the resulting access is recomputed under these rules and can differ from what the old settings produced. Two changes matter most: a team policy now narrows the environment grant instead of replacing it, so it can no longer grant a tool the environment does not; and a key with nomcp_accessblock now follows the policy layers instead of standing outside them.allowis required on every layer, so a layer that only means to subtract must spell its allow side["*"]. - A guardrail that blocks an MCP tool call now answers as a tool error. The call returns success at the protocol level with
isError: trueon the result and a message naming the guardrail — never the matched content — instead of a JSON-RPC-32600. A calling agent sees the refusal as tool output it can adapt to, rather than a broken transport. A client matching onerror.code == -32600must readresult.isErrorinstead. - The
weightedrouting strategy and thestickyflag are gone. Weight is now a per-target attribute available under every strategy:round_robinis smooth weighted round robin, so equal or absent weights reproduce the previous declaration-order cycle exactly and unequal weights hold exact proportions. Session affinity is now its own strategy,consistent_hash. Deployments managed through AISIX Cloud are migrated automatically. Declarative configurations must be updated:weightedbecomesround_robinwith weights kept, andweightedwithstickybecomesconsistent_hash. A stored configuration still namingweightedfails to load rather than changing meaning silently. Cohort assignments reshuffle once when moving to consistent hashing, since it builds its ring differently. - An unsupported MCP protocol version is now refused inside the JSON-RPC envelope. A request whose
MCP-Protocol-Versionheader names a revision the endpoint does not serve gets400with a JSON-RPC error carrying the supported list, instead of a baretext/plainresponse outside every gateway envelope. The endpoint serves2025-03-26,2025-06-18,2025-11-25and2026-07-28; it no longer advertises2024-11-05, which only existed for the HTTP+SSE transport. - Traces exported over OTLP now form a hierarchy. A model request that reaches an upstream produces an HTTP SERVER span, a logical client span covering every retry and failover, and one child span per upstream attempt, replacing one flat span per usage event. An exported request trace with no upstream dispatch has only the SERVER span; protocols without per-attempt tracking have the SERVER span and one client span. Anything keyed on "one span per request" — span-count alerts, name-based counts — will see structural spans appear. The attempt span carries the full attribute set and is identifiable by
aisix.attempt_index; structural spans carry a smaller correlation attribute set that includesaisix.request_id, and the SERVER span is span kind 2. Separately, the caller'straceparentandtracestateare no longer forwarded to model providers; an operator's explicitdefault_headersentry for a trusted upstream still works. - Text-to-speech latency is now measured to the first byte.
/v1/audio/speechstreams the synthesized audio as it arrives instead of buffering it, so a player can start on the first bytes and the reported latency is time to first byte rather than time to last byte. This is the convention every other streaming surface here already follows.
New Features
- Passthrough routes. A route binds a gateway entry — a path prefix, an inbound
Hostallowlist, or both — to one upstream target, so an IDE or coding agent can be pointed at the gateway and have its traffic audited without changing the client. Host matching runs before routing, so forward-proxy traffic delivered with its originalHostcan claim a path the gateway also uses; path matching runs as the router's fallback, so a route can never shadow the gateway's own API. Callers authenticate with a gateway key, with the key in a header you name soAuthorizationstays free for the upstream credential, or anonymously as a bound principal gated by a source-CIDR allowlist. The upstream credential is either injected from a provider key or forwarded from the caller after the route removes its gateway credential and fixed header set. An optional device-injected identity header is recorded as the request's end-user identity for per-employee attribution. The request envelope is detected per request — chat, Responses, completions, or opaque — and drives guardrail text extraction, audit capture and token accounting; traffic that is not an LLM call records no phantom tokens. SSE responses relay incrementally. See Passthrough Routes and Forward Proxy for IDE AI Traffic. - Consistent hashing and priority tiers for model groups.
strategy: consistent_hashpins each session to one target by hashing a request key over a ring of targets: the same key keeps landing on the same target, weights scale each target's share, and when a target fails only its own sessions move to their ring successors — every other key keeps its mapping. The key comes from a source chain you configure (hash_on: a header, a cookie, the caller's API key, or the client IP), defaulting to the routing-key header falling back to the API key. Independently, every target takes apriority: targets partition into tiers, higher is preferred, and a lower tier only receives traffic once every target above it has failed or been taken out by health tracking — so a backup pool can sit idle behind an active one and a request that discovers a fully dead tier still succeeds by spilling within itself. - MCP 2026-07-28.
/mcpand/mcp/{server}serve the final revision of the specification, including the handshake-free discovery lifecycle and the stateless transport. A registered MCP server takes an optionalprotocol_versionto pin the revision the gateway uses to open its upstream session, which is the only way to reach a server that no longer answers the older handshake; omitted, the gateway keeps negotiating as before, so existing configurations are unchanged. Selection is explicit in both directions — the gateway never probes or silently falls back, so a mismatch fails visibly instead of quietly negotiating something you ruled out. - Anonymous MCP access. An environment can let MCP clients that present no credential reach chosen entries, running as an API key you nominate — that key's tool grant, rate limits, budget and usage attribution all apply. It is gated by a mandatory source-CIDR allowlist, names the entries it opens, and treats the aggregate
/mcpentry as opt-in. A client that does present a credential is still authenticated normally; an invalid one is rejected rather than served anonymously. - OAuth 2.1 resource-server discovery for
/mcp. With a canonical resource URL configured and an identity provider enabled, the gateway publishes protected-resource metadata and answers unauthenticated MCP requests with aWWW-Authenticatechallenge pointing at it, so a standard MCP client can discover where to get a token. Bearer tokens must carry that URL in their audience. - Guardrails can be scoped to one MCP server, so a rule can guard a single registered server instead of all MCP traffic in the environment. Guardrails now also scan a tool's structured output, not only its text blocks.
- A drain window on shutdown. On
SIGTERMthe gateway reports itself not-ready immediately, then keeps accepting new connections for at leastshutdown.min_drain_secs(default 30s) before closing its listener, so connections a load balancer routes inside its own detection window are still served. HTTP/1.1 responses carryConnection: closeduring the window so pooling clients retire connections as they use them. The window is a minimum, not a deadline: the listener closes only once it has elapsed and nothing is in flight. - W3C trace context is now honored. A valid inbound
traceparentmakes the gateway's spans children of the caller's trace; a malformed or duplicated header is ignored in favor of a local root rather than failing the request. Span and trace ids are minted once per request, so a delivery retry re-sends byte-identical ids instead of fresh ones. Usage events carry the trace id for correlation. - Streamed audio transcription is relayed live.
/v1/audio/transcriptionswithstream=truenow forwards frames as they arrive instead of answering from a fully read body, while still recording the request's token usage. A blocking or masking output guardrail keeps the buffered path, since it must see the whole transcript before any of it reaches the caller. - Role management in the Cloud Admin API. Custom role CRUD, organization-role assignment, and environment-scoped role bindings are now part of the published API contract, with generated bindings and request validation.
Improvements
- Release images are now built with profile-guided optimization, and the allocator runs a background purge thread that returns freed memory to the operating system after load subsides.
- Strict validation now names the offending setting when it rejects configuration a model kind never reads, instead of reporting only that the document failed.
- Passthrough traffic records every usage dimension its detected envelope carries.
Fixes
- Streamed audio transcription was billed as free. The gateway's SSE decoder ended an event only on a bare line-feed pair, while providers frame transcription streams with carriage returns, so the terminal event carrying the token counts was never decoded and every streamed transcription recorded zero tokens. Events now end on any pair of line terminators. The decoder is shared by every streaming bridge, so any upstream framing with carriage returns was affected, not only audio.
- Caller API keys whose MCP access was written before the layered rule keep loading rather than being dropped.
- The deployment and fallback counters are now emitted; pre-dispatch failures no longer count toward them, and failed requests are attributed to the caller that was resolved.
- Passthrough traffic is attributed correctly in traces and on rejected-request events, and a
401from header-based authentication names the header it expected. - Forward-proxy routes matched on host may claim path prefixes the gateway reserves, and mirror the whole request path.
- Audio metadata parsing no longer logs warnings about ordinary uploads.
- In the Dashboard: the create-model picker no longer reads as "unsupported," the Dimensions field is no longer mislabelled optional, route transitions show a loading boundary, and the MCP authentication settings live on the MCP Access page with the rest of the MCP configuration.
Upgrade Notes
- Upgrade the control plane first, then your gateways. This is the supported order, and the mixed-version window can be as long as you need. While it lasts, a gateway still on 0.9.x will stop serving a model group that used
weightedwithsticky(its migrated configuration names a strategy that release does not know) and will not apply MCP tool policies (their stored shape changed). Caller API keys keep authenticating throughout — their MCP access is simply off on the older gateway until it is upgraded. Both are visible in the Dashboard's data-plane compatibility view. - Deployments managed through AISIX Cloud migrate stored routing configurations automatically on the first control-plane start after the upgrade. Declarative configurations are not migrated and must be updated by hand, as described under Behavior Changes.
0.9.0
Release date: August 13, 2026
This release makes the gateway substantially faster and makes agent traffic legible. The proxy now serves from thread-per-core workers, roughly doubling throughput and halving p99 latency. Caching learned to match requests that mean the same thing rather than only those spelled the same way. A2A calls now record which task they advanced and what they cost, and a verified JWT can stand in for a caller API key, so an identity provider's claims decide which caller a request runs as.
It also completes the Admin API deprecation announced in 0.4.0: the gateway's own Admin API is now read-only, and resources are managed declaratively or through AISIX Cloud.
Behavior Changes
- The gateway's Admin API no longer writes resources. The admin listener keeps its whole read surface — lists and gets for every resource kind, model status, health, the OpenAPI reference, and the playground — but
POST,PUT, andDELETEon/admin/v1/<kind>now return405withAllow: GET, and both spellings of the API key rotate route return404. This completes the deprecation announced in 0.4.0. Manage resources through a resources file (validate withaisix validate --resources <file>, reload withSIGHUP) or by writing to etcd; gateways connected to AISIX Cloud never exposed this listener and are unaffected. To rotate a caller key declaratively, write the same resource id with a newkey_hash— the old secret stops authenticating as soon as the write propagates. The published Admin API reference no longer documents write operations. See Resources File. - Cached responses are isolated per caller API key by default. A cache policy's new
scopefield defaults toapi_key, so one caller's answers are never served to another. Deployments that relied on environment-wide sharing must setscope: envexplicitly. Either way the cache key changes shape, so expect a one-time full miss after upgrading. See Caching. - Time to first token is now measured at the first streamed frame of any kind. It previously waited for a frame carrying generated output, so a model that thinks silently before answering recorded the end of its thinking phase — a figure that could exceed the request's own reported latency and could not be compared with what a fronting proxy measures. Time spent thinking remains visible in upstream latency. Dashboards and alerts calibrated against the old figure will shift. See Metrics and Logs.
- A request id supplied by the caller is now adopted as the gateway's own. The gateway accepts
x-aisix-request-idby default: the value is echoed back, recorded in the access log and usage events, and forwarded upstream. Ids must be 1–256 visible-ASCII characters; anything else is ignored and the gateway generates its own id, so a malformed header never fails a request. Addx-request-idtoproxy.request_id.accept_headersto honor that convention too, or set the list to empty to restore the previous behavior. Because the id is now caller-controlled it is neither unique nor trusted, and it is never used as a metric label. See Access Logs and Request Correlation. - Configuration a model kind never reads is now rejected instead of stored and ignored.
retries,auto_prompt_caching, andcoston a model group, the generic call settings on an ensemble, andauto_prompt_cachingandcoston a semantic router are refused at write time with400. A resources file carrying one fails to load and names the offending entry. A model already stored with such a setting keeps working: the gateway drops the field and reports it as partially compatible rather than taking the model out of service. In the other direction, semantic routers and embedding models now accepttimeout,stream_timeout, andretries, which they previously refused. - Several configurations that were silently accepted are now refused. Creating a model with a configuration block belonging to a different kind, referencing a wildcard alias as an ensemble member or judge, as a semantic router target or default, or as a cache policy's target, renaming a referenced model into a wildcard, and pointing a cache policy at a model that does not exist all return
400. Each was previously accepted and then quietly ignored. - Traffic through a wildcard model is reported under the wildcard's own name. Metrics, rate-limit buckets, and health tracking now key on the configured wildcard row rather than the alias each caller happened to send, so one model is one identity instead of a new series per spelling. A dashboard filtered on a caller-minted alias will show its series end at this release.
- Upgrade note. The control plane widens the
request_idcolumn on its usage table fromuuidtotextso a caller's own id can be stored. On a large table this rewrites it — about six minutes per 10 million rows — and the control plane does not serve traffic until it finishes. The Helm chart's startup probe budget has been raised to 30 minutes to accommodate this; if you run your own manifests, raise yours before upgrading.
New Features
- The proxy serves from thread-per-core workers. Each worker has its own runtime, listener, and upstream connection pool, so a request is accepted, dispatched, and answered on a single thread instead of being handed between threads twice. On four cores this raises throughput by 54–88% depending on concurrency and roughly halves p99 latency, cutting system calls per request from 11.9 to 5.0. It is on by default on Linux. Two settings apply at startup:
proxy.thread_per_coreandproxy.workers, which defaults to the parallelism available to the process and follows a cgroup CPU limit. Below roughly four client connections per worker the kernel spreads connections unevenly and this mode is slower than the shared runtime; setproxy.thread_per_core: falsefor very low concurrency. - Semantic caching. A cache policy can now match on meaning: a request that misses exact matching is embedded and served from the nearest stored entry at or above a cosine similarity threshold you set. Only fully textual requests take that path — anything carrying images, audio, or tool calls always matches exactly. Entries can live in each gateway's memory or, with
backend: redis, be shared across replicas using Redis vector search. Sharing requires Redis 8 or later, or the search module; the gateway checks at startup and, when it is missing, keeps serving exact matches and says so in the log rather than failing traffic. A policy can also be emptied without deleting it. See Semantic Caching. - JWT claim mappings. A new resource resolves verified OIDC claims to an existing caller API key, so an identity provider can decide which caller a request runs as without provisioning a key per user. Rules are evaluated in priority order and the first whose claim conditions all hold selects the key; the request then inherits that key's model and tool access, rate limits, and budget unchanged. Conditions support exact string matches and array membership over nested claim paths. A token that matches no rule is denied. Usage events record the subject, the provider, and the mapping that matched — note that the subject is an end-user identifier and reaches any configured observability exporter. See Claim Mappings.
- A2A calls are now observable at the protocol level. Every call records its operation, task id, context id, and final task state, with both wire vocabularies normalized so one figure is not split in two. Streamed calls additionally record time to first event, how many events arrived, and whether the caller was still listening at the end, so a caller that hangs up mid-task is no longer counted as a success. Because the protocol carries no usage block, the gateway estimates tokens from the message text that passed through and marks them as estimated; cost is left at zero, since what an agent charges is not something the gateway can know. Metrics are sliced by agent and operation. See Agent Gateway.
- The provider's own response id is now recorded. It appears in the access log and in a dedicated log line emitted once per upstream attempt, which covers streamed responses and retried or failed-over attempts that a single access-log line structurally cannot. The Dashboard shows it on the log detail panel, so a provider-side problem can be traced to the exact upstream call without joining records by hand.
- Rate-limit policies accept a
daywindow, counted against the UTC day. See Rate Limit Policies. - The Dashboard's request log is split by gateway surface. LLM, MCP, and A2A traffic each get their own tab with the filters that can actually narrow it, rather than one feed with filters that apply to only part of it.
- The Dashboard's dropdowns are searchable. Every selector that can grow long — models, provider keys, API keys — now filters as you type instead of requiring a scroll through the full list.
- The Dashboard reports gateway configuration compatibility. When a setting you save is not understood by every gateway in the environment, the Dashboard says so at save time and on the data-planes view, naming the field and the version that introduced it, instead of leaving the setting silently inert on older gateways.
Improvements
- Beyond thread-per-core, the per-request path was reworked throughout: a jemalloc allocator and link-time optimization, fast paths that skip machinery a zero-configuration deployment does not need, a per-worker metrics cache,
TCP_NODELAYon downstream connections, cached upstream endpoint URLs, and a single configuration snapshot load per request. - A rate-limit condition on a model now matches the entry the caller addressed as well as the one the request was dispatched to, so a condition naming a model group applies to every request addressed to that group. Previously only the dispatched target was compared, and such a condition could never match.
- A semantic router now honors its members' own access rules when it selects a route, falling through to another target instead of dispatching to a member the caller is not permitted to use.
- Per-model rate limits now apply to every model kind, and the Dashboard offers the rate-limit form on every one.
- A cache policy's target is now validated when the policy is created, and renaming a model that a policy references updates the reference.
Fixes
- An invitation that has expired no longer keeps its email address reserved, so the same person can be invited again. Pending invitations still block a duplicate.
- Creating a resource with a boolean field explicitly set to
falsenow storesfalse. A column default was substitutingtruefor the zero value, so an OIDC provider created as disabled was stored as enabled and the API reported it as enabled. - Post-authentication refusals on the realtime endpoint are now attributed to the caller that was resolved, instead of being recorded without an identity.
- Gateways that have stopped sending heartbeats no longer count toward the Dashboard's configuration-compatibility banner, so a decommissioned instance cannot make a healthy fleet look partially incompatible.
- A gateway built from source without version stamping reports a placeholder version; that placeholder is now treated as unknown rather than as a real old version, so such a fleet no longer warns forever about features it may well support.
0.8.2
Release date: August 11, 2026
This is a maintenance release for the A2A gateway. The gateway now announces the wire version each agent is pinned to, finds an agent card published under a path prefix, keeps every address on the card pointed at itself, and relays message/stream as it arrives instead of buffering it. It also applies the agent and MCP server naming and credential rules to every configuration path, not only the gateway's own Admin API.
Behavior Changes
- A2A agent and MCP server definitions supplied through a resources file are now checked against the same rules the Admin API enforces: the name shape, the credential each
auth_typerequires, and the fields an OpenAPI-backed MCP server needs. A file that breaks one of them no longer loads, and the gateway reports the offending entry and the missing property by name rather than starting with a definition that cannot work — an agent named with a/splits its own/a2a/<name>route in two, andauth_type: bearerwithout a secret authenticates upstream with nothing. Deployments configured through the control plane are unaffected, since those rules already applied when the resource was created; a gateway managed by the control plane rejects only the offending entry and keeps serving the rest. Check a file before rolling it out withaisix validate --resources <file>. See Agent Gateway.
Fixes
- The gateway now announces an agent's pinned protocol version in
A2A-Versionon every call it makes to that agent, including the agent-card fetch. Nothing sent the header before, and the A2A specification tells an agent to read its absence as version 0.3 — so an agent pinned to 1.0 answeredVersionNotSupportedErrorand the configuredprotocol_versionhad no effect. A version supplied by the caller does not override the registered pin. - An A2A agent whose card is published under a path prefix now resolves. The well-known card URI was built at the origin, discarding the path the agent was registered with, so any agent behind an ingress path or on a platform that multiplexes tenants under a prefix was asked for a card it does not serve there — and the platform's catch-all
405came back as the agent's own answer. - The agent card the gateway serves now points every advertised address back at the gateway. Only the top-level
urlwas rewritten before, leaving the upstream address insupportedInterfaces— and a 1.0 caller reads its endpoint from there, so it could bypass the gateway, and with it the caller authentication and per-agent access control, and reach the agent directly. message/streamandtasks/resubscribenow relay the agent's event stream as it arrives. Both were served through the single-response path, which buffered the whole body and parsed it as one JSON document; an event stream is not valid JSON, so the two methods whose purpose is watching a long-running task both failed with502and could not be used at all. Events are reassembled across chunk boundaries, and a malformed event ends the stream with an error rather than being skipped, so a truncated task cannot read as a completed one.
0.8.1
Release date: August 7, 2026
This is a maintenance release. It corrects how the gateway decides that an upstream speaks the Anthropic protocol, so a model pointed at a self-managed Anthropic endpoint is served on the whole /v1/messages family with its request body passed through unchanged. It also gives authentication denials enough context to investigate, carries tool grants through an MCP server rename, and fixes several control-plane admission paths that had no workable request shape or no actionable error.
Behavior Changes
- Enabling
auto_prompt_cachingnow requires a model whose provider key speaks the Anthropic protocol, eitherprovider: anthropicor a BYO key whose adapter is Anthropic. The setting is implemented by injecting Anthropiccache_controlmarkers, so on any other upstream it was stored but never took effect, and nothing told the operator that their prompt caching was off. Enabling it elsewhere now returns400. Disabling remains valid on every provider, so a model configured before this release can still be cleaned up, and a key whose adapter cannot be determined is allowed rather than rejected on a guess. See Prompt Caching. - A model configured as
provider: byowith the Anthropic adapter is now served on/v1/messagesby passing its request body through unchanged, instead of re-encoding it through the cross-provider bridge. Acache_controlmarker set by the client reaches the upstream as written; it was previously dropped, or rewritten to the model's configured TTL whenauto_prompt_cachingwas enabled. This also changes which upstream cache-write price applies, since a one-hour write costs twice the base input price and a five-minute write costs 1.25 times. Authentication is unchanged.
Improvements
- Authentication denials now carry the request context needed to act on them: the caller address resolved through the trusted-proxy configuration, the HTTP method and path, and the request id. Denials for a disabled or expired key also carry the key id. A
401is refused before any handler runs and therefore never reaches the access log, which left the denial counter as the only record and it cannot say who, when, or against which route. Log levels are unchanged, so scanner traffic stays at debug and the default-level output is exactly as before. See Metrics and Logs. - The AISIX Cloud Admin API reference now describes provider request headers and conditional rate-limit counter semantics correctly. It previously said that changing a policy's
conditionsresets the current windows; counter keys are built from the policy id and the selectedgroup_byvalues, so changing onlyconditionsorlimitspreserves existing keys and their counts, while changing the grouping dimensions moves requests to different keys.
Fixes
/v1/messages/count_tokensnow serves a model whose provider key speaks Anthropic through an adapter rather than by vendor id. Aprovider: byomodel pointed at an Anthropic-protocol endpoint was rejected with400on this endpoint while the sibling/v1/messagesserved it normally.- Renaming an MCP server now carries the tool grants with it. Tool names are
<server>__<tool>, so every grant references the server by name; the per-key rate limit entries already followed a rename but the access control did not, which silently revoked access to the whole server and made its tools disappear fromtools/listwith no explanation. The caller key'sallowed_toolsandmcp_access, and the environment- and team-level access policies, are all rewritten. Wildcards that match a shape rather than one server, such as a bare*, are left alone. See MCP Access Control. - Provider keys for Amazon Bedrock and Google Vertex can now be created by supplying
configalone. Their credentials do not fit in a singleapi_keystring, butapi_keywas required by the schema and a non-empty value was rejected by the handler, so the only accepted request was an empty string that no documentation mentioned. A request missing its credential now names the field that provider actually accepts. See Provider Keys. PATCHwith"tls": nullnow clears a provider key's TLS settings, matching howrate_limitand the other optional blocks behave. It previously returned400and the setting could only be cleared with an empty object.- The usage feed and its CSV export now include
audio_duration_seconds. A transcription priced by duration showed a cost whose basis could not be read back from the API. The field is omitted for requests that carry no audio. See Audio. - The offline package no longer lets two installations become one. The Compose project name came from the directory the package was unpacked into, so unpacking a second copy elsewhere and running
run.shtook over the running installation, rebuilding its containers against the second copy's configuration while keeping the original data volume. The project name is now fixed to the package, and starting it while another directory holds the same stack is refused with both paths and the ways forward. Upgrading in place is unaffected, and a second independent installation can still be started with an explicitCOMPOSE_PROJECT_NAME. See On-Premises Quickstart. - The Dashboard container in the offline package now has a writable Next.js cache directory, matching the Helm chart.
- Trusted-proxy CIDRs and the User-Agent client-type rules can now be set from the environment.
proxy.real_ip.trusted_proxiesandobservability.metrics.client_type_rulescould only be supplied in a configuration file; setting either through itsAISIX_*variable failed to start the gateway. Deployments configured purely through environment variables — the Helm chart and the Dashboard'sdocker runsnippet — therefore could not declare their load balancer as a trusted proxy, so every request appeared to come from it. See Metrics and Logs. - Two admission errors now say what to change. A
weighton a target in a group that is notweightednames the field and the strategy instead of reporting a generic invalid-fields error, and an unrecognizedprovidernames the rejected value and explains that the catalog route needs a Dashboard session rather than an admin token. - The Dashboard now lets one API key allow several model groups. Checking one group greyed out the rest, so a key could only ever reference one, although the API always accepted a longer list and the edit dialog never enforced the limit. The two dialogs now share one picker, which also fixes model groups, ensembles, embedding models, and semantic routers rendering with an empty arrow in the edit dialog.
0.8.0
Release date: August 6, 2026
This release makes rate limiting expressive enough to describe a real quota policy. A single policy now decides which traffic it applies to through a condition tree, splits its counters along any dimension of that traffic, caps seven different quantities, and can suspend itself on a recurring schedule such as weekday off-peak hours, whole weekends, or named holidays. MCP servers gain an endpoint that serves one server and keeps its original tool names, so a client written against that server works unchanged. Token, spend, and request metrics now cover every endpoint that reports usage rather than only chat and messages, and audio transcription is priced by the duration of the audio.
Behavior Changes
- The gateway now treats
api_baseas the upstream root on every OpenAI-family endpoint. Previously/v1/chat/completionsappended the endpoint to the configured base verbatim, while/v1/responses,/v1/rerank,/v1/audio/*,/v1/realtime,/v1/files,/v1/batches, and/v1/fine_tuning/jobsinserted a/v1segment unless the base already ended in one, so a key whose upstream root was not/v1served chat correctly and returned404everywhere else. Both paths now read the base the same way. A base carrying a path but no version segment buildshttps://proxy.corp/openai-shim/responseswhere it previously built.../openai-shim/v1/responses; move the/v1intoapi_baseif you relied on the old behavior. A bare host still has/v1added, a base ending in/v1is unchanged, and Anthropic is unchanged in every base shape. See OpenAI-Compatible Vendors. - Histogram bucket boundaries changed for two metrics.
aisix_request_ttft_secondsdrops its two edges below 50 ms, because it measures the upstream time to first token and never observes values there, andaisix_request_e2e_latency_secondsgains edges at 420 and 600 seconds sohistogram_quantile()interpolates past 300 seconds instead of pinning to it. Dashboards and recording rules that depend on the previous edges need updating. Both sets are now configurable per metric underobservability.metrics.buckets. See Metrics Reference. - Time to first token now counts a reasoning model's first reasoning delta. Only ordinary content stamped the measurement before, so a model that reasons before it answers reported a later first token than the caller observed. Reported values for those models drop accordingly. See Metrics and Logs.
GET /v1/modelsnow lists model groups alongside direct models, semantic routers, and ensembles. A model group was the only virtual alias filtered out of the listing, so a caller key scoped to a group as its public entry point received an empty list. No authorization changed, since those names were always callable by those keys. Only clients that assert an exact model list are affected. See Model Aliases.
New Features
Rate Limiting
- A rate limit policy can now be written in a conditional form that carries a tree of conditions instead of a single scope. Conditions match on team, member, caller API key, model, model name, and provider, combine through explicit
andandorgroups up to three levels deep, support negation, and match strings by equality, list membership, or regular expression. One resource can therefore hold different quotas for different traffic instead of one flat limit. See Rate Limit Policies. - A conditional policy splits its counters with
group_by, which buckets the counters per team, per member, per caller key, per model, or any combination of those. A single policy now enforces a per-tenant quota that previously required one policy per tenant. - A policy can cap seven quantities in any combination: requests per second, minute, hour, and day, tokens per minute and per day, and concurrent requests.
- A policy that matches on a model property reserves its quota where the concrete model is known, which is per target for a routing or ensemble parent. An over-limit target is treated as a failed attempt and fails over to the next one, rather than consuming the parent alias's own budget.
- A rate limit rejection now identifies the policy that produced it. The
429response body carries the policy's id and name, andaisix_ratelimit_rejections_totalcounts rejections on every endpoint and is labeled by the enforcing layer and policy. With several policies live, an unattributed rejection could not be traced to its cause. - A policy can carry recurring suspension windows during which it is not enforced. Each window selects days either by weekday or by explicit dates, takes wall-clock start and end times in its own IANA time zone, and treats an end at or before the start as crossing midnight and belonging to the day it starts on. Multiple windows form a union, enforcement resumes automatically, and the counters are not reset by the transition, so suspending and resuming inside one rate window cannot be used to clear consumed quota. In AISIX Cloud, the Dashboard exposes these as a schedule list on the rate limit policy form. See Rate Limit Policies.