> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Gateway Changelog

> What changed in the AI Gateway and when — including the behaviour changes that could break a working integration, stated plainly rather than buried.

# AI Gateway Changelog

Newest first. Entries marked **⚠ Behaviour change** could alter what a working
integration sees; everything else is additive.

<Note>
  This changelog was backfilled on 2026-08-25 from the commit history, so entries
  before that date are reconstructed rather than written at the time. Dates are the
  date the change landed on the integration branch, not the date it reached
  production.
</Note>

***

## 2026-08-25

### Extended-thinking and refusal text are redacted

**⚠ Behaviour change, security.** The streaming redactor scanned a model's
`content` and nothing else. Two other fields carry model-visible text and were
going out unscanned:

* **Anthropic `thinking_delta`** — with extended thinking enabled, the model's
  reasoning streams in its own delta type, and reasoning quotes the prompt
  constantly. An SSN in a thinking block reached the client verbatim **and
  produced no detection event**, so it appeared in no evidence row and fired no
  canary alert. Because the buffered path scans the whole response body,
  `stream: true` was effectively an opt-out from redaction for this content.
* **OpenAI `delta.refusal`** — when a model declines, its explanation streams in
  `refusal`, not `content`, and a refusal routinely quotes back what it is
  refusing.

Both are now redacted, each in its own hold-back window. See
[what in a stream is scanned](/ai-gateway/streaming-guarantees#what-in-a-stream-is-scanned).

If you replay Anthropic thinking blocks verbatim on a later turn, note that
redacting their text invalidates the block's `signature` — re-prompt instead.

Streamed **tool-call arguments** are still not scanned; that is stated explicitly
on the streaming page rather than left to be discovered.

### MCP sessions are scoped to the presenting token

**⚠ Behaviour change.** The upstream MCP session pool was keyed on the
client-supplied `mcp-session-id` header. Two different phantom tokens sending the
same value — including the common case of sending none at all — shared one
upstream connection. The pool key now leads with the verified token, so **each
token gets its own upstream MCP session**.

What you may notice: slightly more upstream connections, and one extra
initialization handshake the first time each token calls. If your upstream MCP
server keeps per-session state, that state is now per-token rather than shared.
A single token holds at most **4** concurrent sessions per server per environment;
opening a fifth closes the least recently used.

### `server_type: 'collection'` MCP servers are refused at creation

Creating an MCP server backed by a route collection was accepted by the API and
then failed on every call, because the feature was never built. It is now refused
at create with a clear message. An existing collection row answers **501** (it
previously produced a generic 502 that pointed you at an upstream nothing had
dialled).

### `resources/*` and `prompts/*` on the MCP plane are documented as refused

No code change — the gateway governs **tools**, and has always answered
method-not-found (`-32601`) for resource and prompt methods. It was not written
down anywhere. See [MCP gateway](/ai-gateway/mcp-gateway).

***

## 2026-08-24

### Every content block in a streamed response is redacted

**⚠ Behaviour change, security.** The OpenAI streaming redactor inspected
`choices[0]` only. A request with `n > 1` streamed its second and later
completions **unredacted** — the control was running and looking at the wrong
element. Gemini's redactor had the same defect one field over
(`candidates[0].parts[0]`), and additionally missed a candidate's second text
part.

All completions are now extracted and redacted, each with its own hold-back
window. If you use `n > 1` with PII redaction on, expect redaction to appear in
completions where it previously did not.

### `X-KC-Team` is honoured

**⚠ Behaviour change.** Sending `X-KC-Team` previously cleared the team on the
usage row — the opposite of what the header is for — so calls that named a cost
centre were the ones attributed to nobody. The header is now resolved against your
directory and lands on `attributed_team_id`. See
[Budgets & FinOps](/ai-gateway/budgets-finops#the-x-kc-team-override).

### Rate limits on all three data planes

`/v1/ai`, `/v1/mcp` and `/v1/oauth/token` register ahead of the proxy's catch-all
and so had **no request-rate ceiling of any kind**. All three now enforce
cross-worker sliding windows — see [Limits](/ai-gateway/limits#request-rate). The
ceilings are generous abuse floors; a normal integration will not meet one.

### Provider credentials are actually injected

**⚠ Behaviour change, and the big one.** The data plane did not perform route
secret injection at all: every `/v1/ai` upstream call went out with **no provider
credential**, so it 401'd against any real provider. The route's
`inject_headers_json` / `inject_body_json` are now rendered through the same
secret-resolution chokepoint the main proxy uses, on the primary route, every
failover route, the output-schema retry, and the semantic-cache embedder.

If you had worked around this by putting a real provider key in your own client,
stop — the gateway now injects one too and the upstream will see whichever it
resolves.

### Live/Test confinement is enforced

**⚠ Behaviour change.** A token's `kc_live_` / `kc_test_` segment was parsed and
discarded, so a Test token drove Live routes. The segment is now part of the route
lookup on the primary and every failover route, and a token whose environment
cannot be proven is refused. A `kc_test_` token that had been reaching a Live route
will now get a 404 for that agent's route.

### Firewall policies can be created, and `block` is reachable

No product surface could create a row in the firewall-policy table, so with no
policy the evaluator capped every outcome at `warn` — **`block` was unreachable for
every tenant** while the UI presented it as configurable. Policies are now
creatable from `/v1`, the admin API and the gateway-creation seeder, and a `block`
policy blocks. See [Firewall](/ai-gateway/firewall).

### Tenant PII recognizers are read by the data plane

Custom recognizers and compliance-pack recognizers were written and never read.
They now compile into the detector stack on the outbound, streaming and buffered
paths. An attached policy that cannot be resolved, or that holds a pattern that
will not compile, now **refuses the request** rather than forwarding unredacted.

### The MCP control plane exists

`/v1/ai-gateway/.../mcp-servers` CRUD, tool management, an admin twin and an MCP
tab. Before this, creating an MCP server required a raw database insert.

### MCP upstream egress and credentials go through the chokepoints

**Security.** Tenant `upstream_url` values are now resolved-and-pinned like every
other tenant-supplied destination, and upstream auth **must** be a
`{{secret_id:…}}` reference — a cleartext credential in the `auth` column is
refused rather than forwarded.

### Streaming hold-back leaked at the boundary

**⚠ Behaviour change, security.** The hold-back FSM deferred a match ending inside
its tail but emitted everything before that same index, so an entity *straddling*
the boundary had its head emitted in the clear. On a typical one-word-per-frame
stream that was nearly every entity. Separately, patterns that do not match until
complete (JWTs, provider API keys) had their heads emitted at the default window.
Both are fixed; the minimum hold-back window is now 96 characters, raised from
whatever a tenant had configured below it.

### Error boundary and fail-closed policy loads

A database error inside the pipeline used to reject an async middleware promise
that Express 4 ignores — leaving the request open indefinitely. Every path now
answers. A firewall or PII policy that cannot be loaded refuses the request (503)
instead of proceeding with the control off.

### Guardrail quality is measured

A labelled corpus and a recall ratchet now run on every build, so a weakened
detector fails CI. Published figures live in
[PII redaction](/ai-gateway/pii-redaction).

***

## 2026-08-12

### Usage rollup is scheduled

`ai_gateway_usage_daily` had no writer registered anywhere, so it was empty — and
everything reading it (the spend-spike check, console usage) silently saw nothing.
It now runs hourly on a single scheduler process.

***

## 2026-08-08

### H-31 — the response cache is keyed on the caller, not just the conversation

**⚠ Behaviour change, security.** The cache's isolation boundary was
`x-kc-conversation-id`, a plain client header with no binding to the caller's
token. Two users in one tenant sending the same body **with no conversation
header** both scoped to the empty string and could serve each other's responses.
Stored response headers were replayed wholesale, `Set-Cookie` included.

The cache key now folds in the **phantom token id** — the only caller identity the
gateway verifies — and the key prefix moved `v1` → `v2` so nothing written under
the weaker assumption can ever be matched again. A request the gateway cannot
attribute to a token does not read from or write to the cache at all.

What you may notice: **every cache entry written before 2026-08-08 was
abandoned**, so hit rates started from zero that day. Two clients that were
(incorrectly) sharing a cache entry no longer do.

### H-32 — capability-token scope is enforced

**⚠ Behaviour change, security.** A token's `providers`, `models`, `tools`,
`egress_routes`, `max_cost_per_day_usd` and `max_tokens_per_day` were HMAC-signed
and **never checked** — only `valid_window` and `ip_cidrs` were. The admin UI
rendered the unenforced fields as though they were restrictions.

They are now enforced, after model policy resolves. Three fail-open cases inside
the fix were corrected in the same change: a malformed scope **denies** rather
than being ignored; a present-but-empty list is malformed rather than
"unrestricted"; and a cost cap **refuses** a model with no price rather than
metering it as free.

If you minted tokens with scope fields set, expect them to start taking effect —
this is the change most likely to turn a working call into a 403. Check
`X-Auth-Failure` and the `error` code on any new refusal before assuming an outage.

### H-33 — buffered responses get response-side redaction

**⚠ Behaviour change, security.** Non-streaming responses bypassed all
response-side PII redaction and detokenization, so a client could opt out of
redaction simply by omitting `stream: true`. Both branches now run the same stack.

***

## 2026-07-09

### Semantic caching, conversation-scoped

Semantic (embedding-similarity) cache matching was re-enabled with a hard
conversation scope, so an approximate match can only occur within one conversation.
See [Response caching](/ai-gateway/quickstart#response-caching).

### Impossible-travel step-up

`geo_velocity_action = 'block'` flags a token for **step-up** on an anomalous
location change rather than revoking it: the token's next use is refused with
`step_up_required` until it is re-authenticated or the flag is cleared. See
[Tokens & DPoP](/ai-gateway/tokens-and-dpop#impossible-travel-detection-and-step-up).

***

## Earlier

The gateway was built as roughly thirty sequential changes through May–July 2026:
the phantom-token format and DPoP binding, the PII detector stack and hold-back
streaming redactor, the prompt firewall and canary tokens, the pricebook, budgets
and FinOps exports, OIDC workload federation at `/v1/oauth/token`, the response
cache, and the MCP gateway. Those predate this changelog; the pages in this
section describe what they do today rather than the order they arrived in.
