Skip to content

v1.3.0 — Private-link upstreams, multiple proxy listeners, and a faster configuration path

Choose a tag to compare

@github-actions github-actions released this 18 Sep 04:33
· 33 commits to main since this release
04e816f

🚀 Get started

New to AISIX? Get the gateway running and route your first LLM request in minutes:

📦 Download

Pull the container image from the GitHub Container Registry:

docker pull ghcr.io/api7/aisix:1.3.0

The 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/arm64 images for every tag (#1194, #1197), and a gateway that starts unprivileged beside an aisix-* Kubernetes Service (#1195).

🔌 Providers & API surface

  • response_format on Anthropic, Gemini and Bedrock upstreams (#1188). A caller asking for JSON — through /v1/chat/completions or /v1/responses' 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), 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 written. A caller's own tools and an explicit 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 (#1185, #1187). 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; normalises every tool_choice form; and serialises 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.
  • tool_choice is dropped when no tool survives protocol translation (#1183). 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.
  • A buffered /v1/chat/completions against 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/completions still 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, 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 — and a request that turned reasoning off with thinking.type: disabled is never given a tier. Two shapes that used to be accepted are now refused: the empty-string key mapped to null, and any entry mapped to "".
  • Outbound requests advertise the build version (#1161). The default User-Agent is now aisix/<build version> — the same source aisix --version and the Server header read — instead of the fixed aisix/0.1. See Breaking changes.

🧭 Routing & declarative configuration

  • Several proxy listeners, each with its own TLS (#1206). 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. 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, unchanged when listeners is absent or empty. 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:

    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 listeners is non-empty: proxy.addr is not bound (the field stays required, and the gateway logs one INFO line saying it is ignored), and proxy.tls beside it is a configuration error rather than a certificate that applies to nothing — move the certificate onto the proxy.listeners entry 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 404 to any request that does not carry its own hostname — and Host is not a forwardable header. A new optional resolve_addresses 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 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 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. Every surface that dispatches through the Provider Key honours it — the provider bridges and the whole passthrough family. Bedrock and /v1/realtime do not, because they build their own transport and do not honour the key's tls either. 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 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. Absolute-form requests use their URI authority ahead of a conflicting Host header; origin-form requests use Host. 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 cost last — 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.yaml names 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's pricing_key (set the price inline with cost) and a top-level pricing collection (pricing documents are written by the control plane and shared across environments, which a file cannot express). aisix export rewrites every id spelling back to the name form and drops pricing_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_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.
  • An input guardrail on /v1/responses now reads a replayed tool call's name and arguments (#1159). 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. function_call, custom_tool_call and reasoning items now map to the assistant role and function_call_output / custom_tool_call_output to the tool role. See Breaking changes for the coverage this shifts in both directions.

🧩 MCP

  • The /mcp access log says what the request was (#1182). 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 / 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 (#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 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 / 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 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 (#1192) — how long the request occupied the gateway from arrival to last byte out. latency_ms keeps 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 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 (#1193). 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. Direct-model hits are unchanged, and the hit's usage event now names the model that produced the stored body in provider_model_version.
  • A /v1/realtime session that cannot open its upstream socket contributes a status="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_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 (#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/arm64 manifest 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's USER is numeric (10001), because the kubelet cannot prove a named user is non-root and refuses such a container under runAsNonRoot: true — which is what lets a cluster that assigns its own UIDs, such as OpenShift's restricted-v2 SCC, run the pod. Separately, an unrecognised 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, and AISIX_CONFIG now 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_url is normalised 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.
  • Bedrock, the object-store telemetry exporters and the Realtime dial honour the configured upstream connection settings (#1198, #1199, #1200). Pooled Bedrock and object-store connections expire on upstream.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 uses upstream.connect_timeout_ms (5 s by default) instead of the SDK's 3.1 s, and a stuck /v1/realtime upgrade 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: true if you rely on disk recovery across restarts (#1163). The setting is new and 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, 1.3.0 writes no snapshot file at all. In 1.2.0 managed mode wrote /var/lib/aisix/config_cache.json with 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=true for a chart or container deployment. Without it, a gateway restarted while its control plane is unreachable logs waiting for the first configuration before binding the proxy listener and 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 for snapshot_cache_path in your config.yaml, Helm values or AISIX_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). name and display_name tighten 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_limits key 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 hosts list 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. Add hosts to scope it back:

    proxy:
      url_rewrites:
        - from: "^/openai/(.*)$"
          to: "/$1"
          hosts: ["openai.internal.example.com"]   # NEW — restores the old reach

    Audit proxy.url_rewrites (or AISIX_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 200 and 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 reading latency_ms as total request time should move to the new duration_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 by error_class = "client_disconnected"; spend and latency percentiles are unaffected.

  • The outbound User-Agent reports the build version (#1161). Upstream requests carried the fixed aisix/0.1 whatever release was running; the default is now aisix/<build version> — aisix/1.3.0 on this release. An upstream allowlist, WAF rule or analytics filter keyed on the literal aisix/0.1 has to be updated to match the aisix/ prefix instead of the whole string. The telemetry exporter keeps its aisix-dp/ prefix and also reports the release version.

  • Bedrock, object-store telemetry export and /v1/realtime now honour upstream.pool_idle_timeout_secs and upstream.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. Review upstream.pool_idle_timeout_secs and upstream.connect_timeout_ms if you use any of the three.

  • Two guardrail kinds stop screening replayed tool results on /v1/responses (#1159). 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. The two surfaces now agree, but this is a coverage reduction for an existing rule with unchanged configuration: set text_source to 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/responses that previously passed — that is the bypass being closed, not a regression.

  • Two effort_mapping shapes are no longer accepted (#1184). The empty-string key mapped to null, 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 a null value 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