Docs

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 a 499 row 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_enabled defaults to false in both managed and self-hosted etcd modes, and a snapshot_cache_path on 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 200 beside its own 499 usage 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_model and provider_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_ms keeps 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 gains upstream_model and provider_key_id, naming the target actually selected where model names the entry the caller addressed.

  • A cache hit reports the cache, and never a target it did not dispatch to. The line gains cache_status and, on a hit, cache_hit_layer. On a hit of a routing or semantic Model Group, provider, provider_key_id and upstream_model — and the Prometheus labels built from them — now report unknown or 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 in provider_model_version. Cost was already zero on a hit.

  • A buffered /v1/chat/completions against 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/completions still forwards the usage frame the upstream sends.

  • tool_choice is dropped when no tool survives protocol translation. An OpenAI-compatible or Anthropic upstream rejects tool_choice without tools, so a caller that sent an empty tool list — the shape agent CLIs send on a context-compaction call — got a 400 from 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/completions against an OpenAI-shaped upstream is still forwarded verbatim.

  • An input guardrail on /v1/responses now reads a replayed tool call's name and arguments. That API spells a model turn as a typed item carrying no role, so the scan read an agent's whole tool loop as user text and dropped function_call items 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. semantic and azure_content_safety_text_moderation read only user-role messages under their default text_source. A function_call_output used 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 a role: "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: set text_source to 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 hosts list now also rewrites a request dispatched by host. Add hosts to a rule to scope it back.

  • Bedrock, the object-store telemetry exporters and the Realtime dial now honor the configured upstream connection settings. Pooled Bedrock and object-store connections expire on upstream.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 uses upstream.connect_timeout_ms (5 seconds by default) instead of the 3.1 seconds built into the SDK, and a stuck /v1/realtime upgrade 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 a status="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.1 whatever release was running; the default User-Agent is now aisix/<build version>, the same source aisix --version and the Server header 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 named aisix-* 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_CONFIG now 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_url is normalized once at load, so dpm.example.com:7944 is read as https://dpm.example.com:7944. Previously such a value connected to etcd and then denied every proxied request with 429 budget_exceeded while the console showed the gateway healthy. managed.cp_etcd_endpoint keeps 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 a model or model_name condition 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, runAsGroup and fsGroup are gone from all three components' podSecurityContext; runAsNonRoot: true and the RuntimeDefault seccomp profile stay. On a cluster that assigns one — OpenShift's restricted-v2 SCC — 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' USER is numeric for the same reason (10001 for cp-api and dp-manager, 1001 for the Dashboard, 10001 for the gateway): the kubelet cannot prove a named user is non-root and refuses such a container once the chart stops supplying runAsUser.

  • A PUT /model_pricing update now applies every rate column. The update path omitted duration_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-identical before and after for a change to any of four rates.

  • Deleting an environment that holds intra-environment references now succeeds. DELETE /api/environments/{id} answered 500 naming 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=custom guardrail, surfacing as a 500. 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_ORIGINS entries are validated against the origin grammar cp-api applies to its own allowlist. An entry that cannot match a browser's Origin header — *, 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/completions caller asking for JSON — and a /v1/responses caller through text.format — got prose back from these three, because none of their wires has a top-level response_format. Each now gets the shape its own API defines: Claude 4.5 and later take output_config.format, Gemini takes generationConfig.responseJsonSchema (2.x and later) or responseSchema (1.x, with the older dialect's conversions), and Bedrock takes output_config.format on the Messages route or outputConfig.textFormat on 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 with additionalProperties: false while required is left exactly as the caller wrote it. A caller's own tools and an explicitly stated tool_choice always 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/responses client whose model has no native Responses endpoint. It now returns the model's reasoning as a reasoning output item with the matching streaming sequence; forwards input_image, input_file and input_audio parts instead of dropping them; turns text.format into response_format; forwards parallel_tool_calls, translates a custom (freeform) tool so the model can call it at all, normalizes every tool_choice form to the provider-neutral shape, and serializes a JSON function_call_output rather than sending the model an empty string. A bridged custom tool call comes back as a custom_tool_call item carrying the freeform input, 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: false is re-expressed as tool_choice.disable_parallel_tool_use instead of being flattened onto a body that rejects unknown keys.

  • A guardrail can read only the latest turn. A new input_messages setting (all, the default, or latest_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_turn reads 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: output together with latest_turn is refused rather than accepted and ignored. A masking guardrail under latest_turn leaves 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 cost last — 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 404 to any request that does not carry its own hostname — and Host is not a forwardable header. A new optional resolve_addresses on a provider key takes an ordered list of IPv4 or IPv6 address literals and connects to them for the hostname the key's api_base names. Only the connection target moves: the Host header, 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 the api_base host, so an apis entry 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, and null clears an override. Every surface that dispatches through the provider key honors it; Bedrock and /v1/realtime do not, because they build their own transport and do not honor the key's tls either. 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, null or empty) and supplies one; the * key matches any other present value with no entry of its own; and a null value removes the effort from the outbound request so the provider's own default applies. On the Anthropic messages and token-counting endpoints only output_config.effort is read: a thinking block is not an effort setting for this mapping, so a client that sends thinking and no effort takes the empty-string entry. A request that turned reasoning off with thinking.type: disabled is 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 Go text/template body (alert-event variables plus a one-line message, a json function, 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_export streams 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, and POST /api/data_import applies 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=true produces 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 hosts list 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 /mcp access log says what the request was. Every MCP operation tunnels through one POST, so a handshake, a tool call and an ACL-emptied tools/list rendered identically. The line now carries mcp_method, mcp_tool on a tools/call, and tools_total and tools_returned on a tools/list. A tools/list that 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_LISTEN is set (api.metrics.enabled in the chart, which also renders a metrics Service and an optional ServiceMonitor). They report write volumes and configuration-fan-out cardinality under an aisix_cp_ prefix, so one Prometheus can hold both planes.

  • Several proxy listeners, each with its own TLS. proxy.addr plus proxy.tls described 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 new proxy.listeners block 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.addr and proxy.tls remain the single-listener shorthand and are unchanged when listeners is absent or empty; when it is set, proxy.addr is not bound and the gateway logs one INFO line saying so, and proxy.tls beside a non-empty listeners is a configuration error rather than a certificate that applies to nothing. A duplicate address is refused at startup, naming both entries, because SO_REUSEPORT would 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 in AISIX_PROXY__LISTENERS. The aisix chart 1.3.0 carries the matching listeners values, 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 carrying linux/amd64 and linux/arm64, for the gateway and for aisix-cp-api, aisix-cp-dpm and aisix-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.sh refuses 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.corsAllowedOrigins in 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.extraVolumes and ui.extraVolumeMounts accept a ConfigMap or Secret, with NODE_EXTRA_CA_CERTS pointed at it through ui.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_revision moving 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/public and 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.

EndpointChange
GET /data_exportAdded. 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_importAdded. 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 viewsAdd 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 viewsChanged. 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 operationsThe 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}/testAdds 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_deliveriesAdded: 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} answers 409 MODEL_IN_USE in one more case: a rate limit policy whose scope is model and whose scope_ref names the model. Same status, same envelope, a condition that used to return 200. A conditional policy carrying a model or model_name condition 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_pricing now stores duration_cents_per_1m_seconds on 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's model_name condition keeps the old name and stops matching. That was already true and is now stated.
  • DELETE /environments/{env_id} stops returning 500 for 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: true if you rely on disk recovery across restarts. The setting is new and defaults to false, and a snapshot_cache_path on 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 logs waiting for the first configuration before binding the proxy listener and 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 is managed.snapshot_cache_enabled: true, or AISIX_MANAGED__SNAPSHOT_CACHE_ENABLED=true for 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 200 and no counts. Every line also gains duration_ms beside latency_ms, and a buffered /v1/chat/completions line gains cache_status.
  • Check any address the gateway or control plane is given for a scheme. A value that is not an http or https URL now fails startup instead of failing at runtime: managed.cp_base_url and managed.cp_etcd_endpoint on the gateway, AISIX_CLOUD_DPMGR_BASE_URL on cp-api and AISIX_DPMGR_BASE_URL on dp-manager. A scheme-less host:port is now accepted and read as https://, and an existing lower-case http:// or https:// value is unaffected.
  • Pin the UIDs back if you depended on them. The control-plane chart no longer sets runAsUser, runAsGroup or fsGroup on 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.sh refuses 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"} 1 and aisix_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 on aisix_config_last_reload_successful for 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_seconds set 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_to selects 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_mapping that carries {"": null} or an entry mapped to "". Both shapes are now 400. A model that stored one keeps serving, but its edit form shows the rule as incomplete and a PATCH resending 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 reports aisix/<build version> instead of the fixed aisix/0.1.
  • Review upstream.pool_idle_timeout_secs and upstream.connect_timeout_ms if 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.