v1.3.0 — Private-link upstreams, multiple proxy listeners, and a faster configuration path
🚀 Get started
New to AISIX? Get the gateway running and route your first LLM request in minutes:
- 📖 Documentation — https://docs.api7.ai/ai-gateway/
- ⚡ Gateway quickstart (one container, declarative
resources.yaml) — https://docs.api7.ai/ai-gateway/getting-started/gateway-quickstart
📦 Download
Pull the container image from the GitHub Container Registry:
docker pull ghcr.io/api7/aisix:1.3.0The rolling latest tag and the docker.io/api7/aisix mirror for private/offline deployments are listed on the package page.
AISIX 1.3.0 reaches the upstreams that are hard to reach: a Provider Key can dial fixed addresses for an endpoint that lives behind a private link, one gateway can serve several proxy listeners with their own TLS, and structured output now works on Anthropic, Gemini and Bedrock. Underneath, the configuration, metrics, authentication and rate-limit paths were rebuilt so a bulk configuration change stops competing with the request path.
- A Provider Key can dial fixed addresses for its endpoint (#1205) — for an upstream reached over a private link with no DNS entry, while the vendor behind it still requires its own hostname.
- Several proxy listeners, each with its own TLS (#1206) — one gateway serving HTTPS and plaintext HTTP at the same time.
- Structured output on Anthropic, Gemini and Bedrock upstreams (#1188), and a Responses-to-chat bridge that answers with the reasoning, images, tool parameters and usage the caller sent or expects (#1183, #1185, #1187).
- A request the caller abandons now files a usage row, and a streamed request writes exactly one access-log line, at the end of its stream (#1190, #1191, #1192, #1193).
- A model, an MCP server and a price are referenced by resource id, so renaming one no longer rewrites every document that points at it (#1148, #1151, #1152, #1153, #1156).
- Configuration, metrics, authentication and rate-limit evaluation are substantially cheaper — a 1,507-row configuration burst now costs 12 whole-configuration passes instead of 131, and a warmed 53,260-series scrape renders in 0.57 s instead of 6.8 s.
linux/arm64images for every tag (#1194, #1197), and a gateway that starts unprivileged beside anaisix-*Kubernetes Service (#1195).
🔌 Providers & API surface
response_formaton Anthropic, Gemini and Bedrock upstreams (#1188). A caller asking for JSON — through/v1/chat/completionsor/v1/responses'text.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), 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 written. A caller's own tools and an explicittool_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 (#1185, #1187). 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; normalises everytool_choiceform; and serialises 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. tool_choiceis dropped when no tool survives protocol translation (#1183). 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.- A buffered
/v1/chat/completionsagainst an upstream that reports no usage returns the gateway's own estimate instead of zeros (#1187). The usage record already carried that estimate, so a caller could not reconcile what it read with what it was 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 upstream's own usage frame. - Reserved entries in a direct model's
effort_mapping(#1184, #1186). 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 — and a request that turned reasoning off withthinking.type: disabledis never given a tier. Two shapes that used to be accepted are now refused: the empty-string key mapped tonull, and any entry mapped to"". - Outbound requests advertise the build version (#1161). The default
User-Agentis nowaisix/<build version>— the same sourceaisix --versionand theServerheader read — instead of the fixedaisix/0.1. See Breaking changes.
🧭 Routing & declarative configuration
-
Several proxy listeners, each with its own TLS (#1206).
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. 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, unchanged whenlistenersis absent or empty. 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:AISIX_PROXY__LISTENERS='[{"addr":"0.0.0.0:3443","tls":{"cert_file":"/etc/aisix/tls/proxy.crt","key_file":"/etc/aisix/tls/proxy.key"}},{"addr":"0.0.0.0:3000"}]'Two coexistence rules apply once
listenersis non-empty:proxy.addris not bound (the field stays required, and the gateway logs one INFO line saying it is ignored), andproxy.tlsbeside it is a configuration error rather than a certificate that applies to nothing — move the certificate onto theproxy.listenersentry that should serve it. -
A Provider Key can dial fixed addresses for its endpoint (#1205). 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_addressestakes 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 resolver's 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. Every surface that dispatches through the Provider Key honours it — the provider bridges and the whole passthrough family. Bedrock and/v1/realtimedo not, because they build their own transport and do not honour the key'stlseither. It also has no effect when the gateway reaches its upstreams through a forward proxy, which is handed the hostname and resolves it itself. -
URL rewrite rules can be scoped by inbound host, and now run ahead of every route family (#1165). 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. Absolute-form requests use their URI authority ahead of a conflictingHostheader; origin-form requests useHost. An empty list or an invalid pattern fails startup. Rewriting is now an entry-stage operation on every route family, including host-matched passthrough dispatch — see Breaking changes. -
A rename no longer breaks what points at a model, an MCP server or a price (#1148, #1151, #1152, #1153, #1156). 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. 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 (#1152). 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 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. -
The declarative
resources.yamlnames its resources, and says so when handed an id. The id spellings above are a control-plane projection: a file derives its own ids from its entry names, so an id a file carried would resolve to nothing — silently, with the name spelling ignored on top. The file source therefore refuses each one by name and points at the replacement —allowed_model_ids→allowed_models,model_id→model,target_id→target,embedding_model_id→embedding_model,default_id→default,applies_to_model_id→applies_to: "model:<name>",anonymous.server_ids→anonymous.servers,mcp_rate_limits_by_id→mcp_rate_limits,mcp_access.allow_ids→mcp_access.allow,mcp_access.deny_ids→mcp_access.deny— and likewise refuses a model'spricing_key(set the price inline withcost) and a top-levelpricingcollection (pricing documents are written by the control plane and shared across environments, which a file cannot express).aisix exportrewrites every id spelling back to the name form and dropspricing_key, so a file exported from etcd loads back.aisix validate --resources <file>reports all of these with the offending path.
🛡️ Guardrails
- A guardrail can read only the latest turn (#1159). 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. - An input guardrail on
/v1/responsesnow reads a replayed tool call's name and arguments (#1159). 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.function_call,custom_tool_callandreasoningitems now map to the assistant role andfunction_call_output/custom_tool_call_outputto the tool role. See Breaking changes for the coverage this shifts in both directions.
🧩 MCP
- The
/mcpaccess log says what the request was (#1182). 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_total/tools_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 (#1153, #1156), which is what makes the rename survival above work on the gateway side. An id names one server exactly and is never a glob, so a ceiling that must follow every registered server stays on the name form.
🔭 Observability
- A request the caller abandons now files a usage row (#1190, #1191). 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 / fine-tuning surface file a row with empty model fields and their own attribution, and an abandoned upload files one too. - A streamed request writes its access-log line when the stream ends (#1192). 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(#1192) — how long the request occupied the gateway from arrival to last byte out.latency_mskeeps its meaning, what the caller waited for, 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 (#1193). 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. Direct-model hits are unchanged, and the hit's usage event now names the model that produced the stored body inprovider_model_version. - A
/v1/realtimesession that cannot open its upstream socket contributes astatus="502"sample to the request metrics (#1201), where it previously appeared in the logs and in no counter.
⚡ Performance
- A bulk configuration change no longer competes with the request path (#1150, #1163, #1164, #1167, #1176, #1178, #1179). Watch events are 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–8 ms p99 to under a microsecond, and 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: 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 (#1160, #1169, #1174, #1202). 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 (#1170, #1189). 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.
📦 Packaging & deployment
- Every published tag is a
linux/amd64+linux/arm64manifest list (#1194, #1197) —dev,sha-*, a release candidate and a release alike. Each architecture builds and is smoke-tested on its own native runner, and a tag is not published unless both platforms are present. - The gateway starts unprivileged, and beside an
aisix-*Kubernetes Service (#1195). The image'sUSERis numeric (10001), because the kubelet cannot prove a named user is non-root and refuses such a container underrunAsNonRoot: true— which is what lets a cluster that assigns its own UIDs, such as OpenShift'srestricted-v2SCC, run the pod. Separately, an unrecognisedAISIX_*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, andAISIX_CONFIGnow works when running the binary directly.
🐛 Fixes
- A repeatedly excluded routing target could block a request worker indefinitely (#1154, thanks @ormandj). 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 (#1172). 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.
- A gateway could hang on shutdown (#1204). An initial configuration read or watch creation that never answered, with no etcd request timeout configured, was not cancelled by the shutdown signal.
- A control-plane address without a scheme is accepted, and a malformed one fails the boot (#1203).
managed.cp_base_urlis normalised 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. - Bedrock, the object-store telemetry exporters and the Realtime dial honour the configured
upstreamconnection settings (#1198, #1199, #1200). Pooled Bedrock and object-store connections expire onupstream.pool_idle_timeout_secs(30 s by default) rather than at 90 s, 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 s by default) instead of the SDK's 3.1 s, and a stuck/v1/realtimeupgrade fails at that budget instead of waiting out the kernel's SYN retries.
⚠️ Breaking changes
-
The on-disk configuration cache is now opt-in: set
managed.snapshot_cache_enabled: trueif you rely on disk recovery across restarts (#1163). The setting is new and defaults tofalsein both managed and self-hosted etcd modes, and asnapshot_cache_pathon its own no longer enables persistence — with only the path configured, 1.3.0 writes no snapshot file at all. In 1.2.0 managed mode wrote/var/lib/aisix/config_cache.jsonwith no field set, and a self-hosted etcd deployment opted in by setting a path; both of those now need the new flag.managed: snapshot_cache_enabled: true # NEW — required snapshot_cache_path: /var/lib/aisix/config_cache.json # unchanged default
Or
AISIX_MANAGED__SNAPSHOT_CACHE_ENABLED=truefor a chart or container deployment. Without it, a gateway restarted while its control plane is unreachable logswaiting for the first configuration before binding the proxy listenerand does not serve until the control plane is back; 1.2.0 restored from disk and served. In-memory last-known-good serving is unchanged. Check forsnapshot_cache_pathin yourconfig.yaml, Helm values orAISIX_MANAGED__*variables: every deployment that has it, and every managed-mode deployment that has nothing, is affected. -
An MCP server name may no longer contain
*(#1156).nameanddisplay_nametighten from^(?:[^_]|_[^_])*$to^(?:[^_*]|_[^_*])*$. An*is what an ACL entry, a per-server rate-limit key and the anonymous allowlist use to mean "every server", so a server whose own name contained one could never be addressed unambiguously by those carriers. Rename the server —my*server→my-server— and update every ACL pattern,mcp_rate_limitskey and anonymous allowlist entry that spelled it out; the tool namespace changes with the name, so clients calling<name>__<tool>must follow.aisix validate --resources <file>reports an offending name before the gateway starts. This is a declarative-configuration change: the control plane's own name rules already limited an MCP server name to letters, digits,_,.and-, so a control-plane-managed deployment has no such name to fix. -
A URL rewrite rule with no
hostslist now also rewrites host-matched passthrough requests (#1165). Rewriting became a consistent entry-stage operation across every route family, where before it ran ahead of built-in endpoints and path-prefix passthrough routes only. An existing rule that was implicitly scoped by the fact that host-dispatched traffic bypassed it now applies to that traffic too. Addhoststo scope it back:proxy: url_rewrites: - from: "^/openai/(.*)$" to: "/$1" hosts: ["openai.internal.example.com"] # NEW — restores the old reach
Audit
proxy.url_rewrites(orAISIX_PROXY__URL_REWRITES) against the hosts your passthrough routes match on; a rule whose pattern cannot match a host-dispatched path needs no change. -
A streamed request's access-log line moves to the end of its stream, and gains fields (#1190, #1191, #1192). A streamed request now writes exactly one line, when the stream ends, carrying the final status and the token counts; it used to write one at head time carrying
200and no counts. Re-point any log processing that keyed on the head-time line — an alert on stream volume will see its lines arrive later, and a dashboard readinglatency_msas total request time should move to the newduration_ms. Separately, requests the caller abandoned now file usage rows and previously filed none, so an environment's request count rises and its success rate falls. Those rows carry zero tokens and zero cost and are filterable byerror_class = "client_disconnected"; spend and latency percentiles are unaffected. -
The outbound
User-Agentreports the build version (#1161). Upstream requests carried the fixedaisix/0.1whatever release was running; the default is nowaisix/<build version>—aisix/1.3.0on this release. An upstream allowlist, WAF rule or analytics filter keyed on the literalaisix/0.1has to be updated to match theaisix/prefix instead of the whole string. The telemetry exporter keeps itsaisix-dp/prefix and also reports the release version. -
Bedrock, object-store telemetry export and
/v1/realtimenow honourupstream.pool_idle_timeout_secsandupstream.connect_timeout_ms(#1198, #1199, #1200). Those three stacks previously used 90 s, the AWS SDK's 3.1 s, or nothing at all. With the defaults (30 s and 5 s) a pooled Bedrock or object-store connection is now recycled three times sooner, and a Bedrock guardrail dial that used to fail at 3.1 s now waits up to 5 s. Reviewupstream.pool_idle_timeout_secsandupstream.connect_timeout_msif you use any of the three. -
Two guardrail kinds stop screening replayed tool results on
/v1/responses(#1159).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. The two surfaces now agree, but this is a coverage reduction for an existing rule with unchanged configuration: settext_sourceto its all-messages value to keep screening tool results. The same change closes the opposite gap, so an operator running a block rule whose pattern appears in a replayed tool call's arguments will start seeing refusals on/v1/responsesthat previously passed — that is the bypass being closed, not a regression. -
Two
effort_mappingshapes are no longer accepted (#1184). The empty-string key mapped tonull, and any entry mapped to"", are now refused; a value's minimum length rises from 0 to 1. Neither shape was reachable from the console, and a model that stored one keeps serving, but a resend of the unchanged mapping is refused. Repair or clear such a mapping, and make sure automation that generates one does not serialize an absent value as""— the empty-string key is the way to say "the caller sent no effort", and anullvalue is the way to say "send none upstream".
What's Changed
- fix(redis): bound every command so an unreachable Redis degrades instead of hanging by @jarvis9443 in #1147
- feat(apikey): grant models by resource id, so a rename keeps the grant by @jarvis9443 in #1148
- perf(config): coalesce watch events on a timed window, not on what happened to be buffered by @jarvis9443 in #1150
- fix(redis): cool off per subsystem, so one request pays the cache budget once by @jarvis9443 in #1149
- feat(config): reference models by resource id, so a rename keeps the reference by @jarvis9443 in #1151
- feat(pricing): resolve model prices from shared pricing documents by @jarvis9443 in #1152
- feat(mcp): reference MCP servers by resource id, so a rename keeps the grant by @nic-6443 in #1153
- feat(mcp): let the anonymous allowlist name servers by resource id by @nic-6443 in #1156
- fix(routing): avoid deadlock when exclusion log throttle expires by @ormandj in #1154
- feat(guardrails): add input_messages to scan only the latest turn by @nic-6443 in #1159
- fix: report the build version in outbound User-Agent headers by @jarvis9443 in #1161
- perf(metrics): isolate scrapes and reduce high-cardinality upkeep by @jarvis9443 in #1160
- perf: reduce configuration overhead and preserve concurrent metric samples by @jarvis9443 in #1163
- perf(config): reuse unchanged SHA-256 prefixes across etcd updates by @jarvis9443 in #1164
- feat(proxy): scope URL rewrites by host before all routing by @jarvis9443 in #1165
- perf(etcd): adapt watch coalescing to configuration apply cost by @jarvis9443 in #1167
- perf(metrics): preallocate warmed scrape output by @jarvis9443 in #1169
- perf(ratelimit): index policy candidates by required conditions by @jarvis9443 in #1170
- test(jwt): await every identity binding before authentication cases by @jarvis9443 in #1171
- fix(jwt): share in-flight JWKS and discovery fetches by @jarvis9443 in #1172
- test(harness): preserve complete startup failure diagnostics by @jarvis9443 in #1173
- perf(metrics): reuse sorted registry series across scrapes by @jarvis9443 in #1174
- test(cache): wait for all seeded cache policies by @jarvis9443 in #1175
- perf(config): share resource index strings across snapshots by @jarvis9443 in #1176
- test: wait for the final passthrough ACL key to authenticate by @jarvis9443 in #1177
- perf(etcd): isolate configuration apply from async I/O workers by @jarvis9443 in #1178
- perf(core): reclaim retired configurations off request workers by @jarvis9443 in #1179
- feat(mcp): log the JSON-RPC method, tool and tools/list counts by @jarvis9443 in #1182
- fix(bridge): drop tool_choice when no tool survives protocol translation by @jarvis9443 in #1183
- feat(models): add unset, wildcard and removal entries to effort_mapping by @jarvis9443 in #1184
- fix(models): a thinking block is not an effort setting for effort_mapping by @jarvis9443 in #1186
- feat(responses): bring the chat bridge to parity — reasoning passthrough, multimodal input, text.format, tool parameters, estimated usage by @nic-6443 in #1185
- feat(responses): return custom tool calls as custom_tool_call items; estimate chat completions usage when the upstream omits it by @jarvis9443 in #1187
- feat(providers): honour response_format on Anthropic, Gemini and Bedrock upstreams by @jarvis9443 in #1188
- perf(auth): index JWT API-key bindings by resource generation by @jarvis9443 in #1189
- fix(telemetry): emit usage events for a request the caller abandoned by @jarvis9443 in #1190
- fix(telemetry): close the two gaps where a cancelled request still files no usage row by @jarvis9443 in #1191
- fix(telemetry): write a streamed request's access-log line when the stream ends, not when its head does by @jarvis9443 in #1192
- fix(telemetry): a cache hit reports the cache, and never a target it did not dispatch to by @jarvis9443 in #1193
- build(image): publish linux/amd64 + linux/arm64 for every tag by @jarvis9443 in #1194
- fix: start beside an aisix-* Service, and under runAsNonRoot by @jarvis9443 in #1195
- fix(image): carry the tag set to the merge job as an artifact by @jarvis9443 in #1197
- fix(bedrock): give each test's SDK client its own connection pool by @jarvis9443 in #1196
- fix(bedrock): apply the configured upstream pool and connect timeouts by @jarvis9443 in #1198
- fix(obs): apply the configured upstream pool and connect timeouts to object-store exporters by @jarvis9443 in #1199
- fix(realtime): bound the upstream WebSocket dial with upstream.connect_timeout by @jarvis9443 in #1200
- fix(realtime): count an upstream-connect failure in the request metrics by @jarvis9443 in #1201
- perf(metrics): reuse catalogs for unchanged metric kinds by @jarvis9443 in #1202
- fix(etcd): cancel pending initial reads and watch creation on shutdown by @jarvis9443 in #1204
- fix(managed): give cp_base_url a scheme once, at config load by @jarvis9443 in #1203
- feat(provider_key): resolve the upstream hostname to fixed addresses by @jarvis9443 in #1205
- feat(proxy): serve several proxy listeners, each with its own TLS by @jarvis9443 in #1206
New Contributors
Full Changelog: v1.2.0...v1.3.0