AISIX 1.3.0
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.
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 ever cross-origin. Populated, it grants a credential-free read of/api/*with the origin echoed concretely rather than*;/api/auth/*and the Dashboard proxy are excluded, and a malformed entry is refused by the chart at render time and by cp-api at startup. -
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, and rollback is the same order reversed.
- 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.