AISIX 0.11.0
This release is about guardrails.
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"}.