AISIX 0.12.0
This release makes two contracts say what they mean.
Release date: August 30, 2026
This release makes two contracts say what they mean. The Cloud Admin API now covers everything the console does, ships as an immutable document per release, and validates what it documents. And a guardrail now governs exactly what you attached it to — the old "attached to nothing means everywhere" rule is gone from the gateway, the console, and the upgrade backfill alike, without changing what any existing guardrail enforces. Alongside that, token counts are reported in the protocol the caller asked in, thinking tokens on Gemini models are counted at last, usage records name the person behind the credential, and several ways a delete could leave a live reference behind are closed.
Behavior Changes
- A guardrail's scope is now exactly its attachments; one attached to nothing inspects no traffic. A guardrail with no attachment rows used to be applied to the entire environment at the lowest priority. Keying on the absence of rows is what made that dangerous, because narrow scoping is expressed by deleting the environment attachment: a guardrail scoped to a single model silently widened to every request in the environment the moment that model was deleted. Upgrading preserves what your guardrails enforce today — the control plane converts the old implicit environment scope into an explicit environment attachment as part of the upgrade, before the gateway stops honoring the implicit one. It also returns guardrails that a previous boot had wrongly widened to environment scope back to the narrow scope you configured. What changes from here on is that a new guardrail governs nothing until you attach it. Declaring an unattached guardrail is still not an error: its scope target may simply have been deleted. In the console such a guardrail now reads Not attached — "Attached to nothing, so it inspects no traffic" — where it used to read Global / "Applies to all models", and the gateway logs a warning naming any enabled guardrail that nothing attaches. In the same vein, opening a narrowly scoped guardrail in the console to change something unrelated and pressing Save no longer writes an environment-wide attachment: the editor now represents "attached to nothing" as its own state instead of defaulting to environment scope.
- Twenty-one control-plane routes the dashboard already used are now part of the Admin
API contract, and requests to them are validated. They were reachable with an admin
token but had no published schema; they now appear in the API reference. Bringing a live
route into the contract also turns request validation on for it, so a request body
carrying a field the schema does not declare is rejected with
400instead of being accepted and ignored —POST /teamswith a stray field returned201before this release and returns400 additional properties not allowednow. If you have automation calling the usage feed, its export, the usage summary or request-level metrics, teams or team members, model price overrides, invitations, member removal, or the notification delivery log, confirm it sends only documented fields. The routes are listed under API Changes. - Token counts in a response are now reported in the accounting the caller's own
protocol uses. The two protocols disagree on exactly one thing — whether prompt-cache
counters live inside the input count or beside it. On the OpenAI protocol
prompt_tokensis the full input with cached tokens included; on the Anthropic protocol the input count excludes them andcache_creation_input_tokens/cache_read_input_tokenssit beside it. The gateway used to copy through whichever shape the upstream reported, so a client speaking one protocol in front of an upstream speaking the other was answered in the other protocol's accounting: an OpenAI-protocol client over an Anthropic upstream sawprompt_tokens: 40alongsidetotal_tokens: 2104, arithmetic that does not add up and a cache hit reported nowhere. The same request now reports2088and2104with the hit underprompt_tokens_details.cached_tokens. Where that conversion happens,total_tokensis recomputed from the projected input and output counts rather than echoing a total that was computed under the other accounting; where no conversion is needed, the total the upstream reported is passed through unchanged. Streaming, non-streaming and/v1/responsesare all covered. The recorded usage event is unchanged, so billed and historical cost are unaffected — only the counts reported back to the caller move. - OpenAI-protocol responses can now carry a cache-write count.
prompt_tokens_details.cache_creation_tokens(andinput_tokens_detailson/v1/responses) reports prompt tokens the provider wrote into its cache this turn. It is not an OpenAI field — OpenAI has no cache-write concept — but without it an Anthropic or Bedrock cache write is an unexplained rise inprompt_tokensthe caller cannot price, and a write is billed above the plain input rate. It is emitted only when non-zero, so a strict client that rejects unknown fields should be checked. - Thinking tokens on Gemini models are now counted, and billed. Gemini reports
thoughtsTokenCount, which the gateway parsed nowhere — so a thinking model'scompletion_tokenswas recorded short by its thinking tokens andreasoning_tokensread0on every protocol. Both are now carried, withreasoning_tokensnaming the subset ofcompletion_tokens, and inclusiveness decided from the total the upstream itself reports, so nothing is double-counted where Gemini already folds thinking tokens into its own candidate count. Cost chargesreasoning_tokensat the reasoning rate, or the completion rate where no reasoning rate is set. If you run thinking Gemini models, recorded completion tokens and cost will rise — to match what Google already bills you for and the gateway was not recording. Unlike the protocol conversion above, this one does change the usage record. /v1/messages/count_tokensnow emits a usage record. The route emitted none on any outcome, so a request that an input guardrail refused was correctly refused and then impossible to find in the logs — including in the Guardrail blocks view. It still bills nothing:prompt_tokensandcompletion_tokensare zero on both the answered and the refused path. "Not metered" and "not recorded" were being treated as one statement; they are two.- A delete that would leave a live reference behind is now refused, repaired, or
reported. Deleting a model named as a semantic guardrail's embedding model returns
409, and renaming that model rewrites the guardrail rather than orphaning it. The stakes were high, because guardrails fail closed: before this release that delete succeeded, and every request in the guardrail's scope then answered422 semantic_embed_unresolved. Deleting an API key that a claim mapping resolves to returns409as well, matching how a passthrough route's anonymous key is already protected — a dangling mapping fails closed but is not free, because matching takes the highest-priority rule without falling through, so a broken one masks a lower-priority rule that still works. Deleting a team or removing a member now clears the ownership it leaves on their caller keys and publishes that to the gateway. Both columns were cleared by a database foreign key, which emits no projection event, so the gateway kept reading a document naming a team that no longer exists — and a rate limit scoped to the deleted team went on throttling those keys with nothing in the console to explain the429s. Deleting an MCP server no longer strips the per-key rate limits that name it: it used to drop those while keeping the tool grants, so re-creating a server under the same name — a supported path — restored the permissions and silently lost the limits. Deleting an OIDC provider still succeeds — both of its carriers hold a name, a rename is a delete plus a re-create, and dangling fails closed and self-heals — but the response now reports how many API keys and claim mappings still name it.
New Features
- The Cloud Admin API is published per release. Every release tag now publishes an immutable OpenAPI document generated from that tag's own source, and the API reference serves a page per released version together with a generated changelog between adjacent versions. You can read the API your deployment exposes and diff it against the version you are upgrading to, rather than against a document that tracks development.
- Usage records and logs are attributed to the org member, not only to the credential. A usage record named the API key a request arrived on but never the person behind it, so "show me this member's errors in the last 24 hours" meant enumerating every key they own and merging the results by hand — and a member calling through both an API key and OIDC was split across two identities no single filter could join. Usage records now carry the member and the Logs page filters by member. The attribution is a snapshot taken when the request is served: re-assigning or deleting a key does not rewrite the history it already produced.
- Logs can be filtered by an exact status code. Filtering previously offered status
families only, so there was no way to ask for
429without also getting every other4xx. Exact codes and ranges such as500-599are now accepted. aisix_usage_events_emitted_totalandaisix_usage_event_drops_totalcarry the member and the exact status. Both gain auser_idlabel and astatuslabel holding the response code itself, so "how many of this member's requests were rate limited" and "did this member lose any usage records" are answerable from metrics. The existingstatus_codefamily label is unchanged, so alerts built on it keep working — but a dashboard that aggregates these two counters without naming its labels will see the new dimensions.
Improvements
- The usage page no longer reports zeros while it is still loading. Before the first
response arrived,
/usagerendered$0.00,0requests and0tokens under two tables reading "No usage in the last 30 days.", then replaced it all seconds later. On a slow connection an operator was told, definitely, that their gateway had seen no traffic. It now shows—with a "Loading…" caption, matching the overview page. - The offline installer reports the version it installed and the port it actually published. An offline deployment had no way to confirm on the way out which release it had just installed, and when the port came from the environment rather than the default, the closing banner still printed the default — so an install listening on one port told the operator to open another.
- The reference now spells out how a custom PII rule's replacement text behaves.
$1is not expanded as a capture-group reference, and a pattern that has capture groups replaces only the first group rather than the whole match — soreplacement: "ACCT-$1-****"againstACCT-([0-9]{4})-([0-9]{4})yieldsACCT-ACCT-$1-****-5678, which looks redacted while leaving the original in place. The console now warns when a replacement contains$.
Fixes
- Two control-plane replicas booting at the same time no longer crash each other. Schema migration was check-then-act throughout — probe a catalog, then create or alter — which PostgreSQL does not serialize, so a high-availability install could fail a pod on startup across several distinct error codes. Boot-time schema work is now serialized; eight replicas started simultaneously against a fresh database all come up, where roughly seven in eight used to fail.
- The upgrade backfill no longer re-widens a model-scoped guardrail. The backfill that gives pre-attachment guardrails an environment-scoped attachment ran on every control-plane boot and identified its targets as "has no environment attachment" — a predicate written when that could only mean "predates attachments". Once scoping a guardrail to a model began deleting its environment attachment, every restart quietly widened it back to the whole environment.
- Deleting a guardrail now retracts its attachments from the gateway, and upgrading cleans up the ones earlier deletes left behind. The attachment rows were removed by a database cascade, which emits no projection event, so the attachment documents stayed live and every gateway logged an unknown-guardrail warning on each index rebuild, indefinitely. Upgrading retracts those leftovers, and the warning stops on its own — no manual cleanup is needed.
API Changes
Twenty-one routes enter the contract this release. They are newly documented, not newly added — each was already reachable with an admin token; what changes is that they now have a published schema and their requests are validated (see Behavior Changes).
| Route | Change |
|---|---|
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Newly documented |
| Adds 409 — the key is resolved by a claim mapping |
| 200 response adds an optional warnings property reporting API keys and claim mappings that still name the provider |
| 409 was already documented; it now also covers a model named as a semantic guardrail's embedding model |
No previously documented route changed its request or response shape in a breaking way. Four semantics that are easy to guess wrong are now stated explicitly, and are worth checking your integration against:
usage_summarycounts distinct requests, not upstream attempts — a request that retried across three attempts counts once.ModelPricing.sourcecan besnapshoton an offline deployment, not only the values a connected deployment produces.- Six of the usage filters are case-insensitive substring matches;
provider_labelandrequest_idmatch exactly. - Caller API key ownership takes the membership identifier from
GET /members, not the member's user identifier — a correction to what the previous reference said. The user identifier is rejected.
Upgrade Notes
These notes apply when you upgrade from any release earlier than 0.12.0, including an upgrade that passes over it on the way to a later release.
- Guardrails need no action. The scope change preserves what your guardrails enforce today: the upgrade writes the implicit environment scope out as an explicit environment attachment, and returns any guardrail a previous boot had wrongly widened to its configured narrow scope. It also retracts attachment records that earlier deletes left behind, so the gateway's unknown-guardrail warnings stop on their own. Guardrails created after the upgrade govern only what you attach them to.
- If you call the control-plane API directly, check the twenty-one newly contracted
routes. Requests to them are now validated against a published schema, so an
undocumented field that used to be accepted and ignored is rejected with
400. - Expect recorded cost to rise on thinking Gemini models. Their thinking tokens were
not being counted at all; they now appear in
completion_tokensandreasoning_tokensand are charged. Budgets and cost alerts calibrated against the previous, understated figures should be reviewed. No other model or provider is affected. - Check dashboards that aggregate
aisix_usage_events_emitted_totaloraisix_usage_event_drops_total. Both gained auser_idand astatuslabel, which changes the cardinality of an unlabelledsum by (...). Thestatus_codefamily label is unchanged.