Architecture
What runs on the request path, in what order, and what happens when each part fails. If you want a gateway running first, start with the installation guide.
Where it sits #
The gateway is consumer-side. It is deployed inside the enterprise blast radius, not in front of it: private subnets, an internal load balancer, adjacent to the applications and agent runtimes it governs. The Token Factory is reached over a private interconnect. The gateway never serves inference itself, and it holds no model weights.
Z1 apps, agents, CI ─┐
├─► Z3 iag-lite ─── private interconnect ───► Z5 Token Factory
Z2 people (OIDC) ───┘ │ │
└─► Z3 state: Postgres (metadata only) └─► model serving
One workspace maps to one upstream project, and tenants are never multiplexed onto a single upstream credential. Prompt and completion bodies are not stored by default; body capture is a per-workspace opt-in with an explicit retention window.
The request path #
Both ingress dialects converge on one middleware chain. An Anthropic request is translated into an OpenAI-shaped body on the way in and translated back on the way out, so every stage downstream sees one object regardless of which SDK called.
HTTP ingress OpenAI /v1/chat/completions | Anthropic /v1/messages
└─ dialect adapter
└─ chain
1 wire hygiene body cap 8 MiB, strip inbound identity headers, traceparent
2 gate virtual key → principal, model allowlist, budget preflight
3 exact cache key scoped by workspace + principal + policy + catalog version
4 trim tool pruner → dedup → history window
5 route alias or cascade over the workspace's allowed models
6 invoke upstream client; streaming tee reassembles the full response
7 verify structural checks, tool-call interception, approvals
8 meter usage block × catalog rates → cost, baseline, savings
9 record one metadata-only row per inference, live-feed cursor
The nine stages #
| Stage | What it does | What it emits |
|---|---|---|
| 1 wire hygiene | Caps the body, strips every inbound identity header, preserves identity content-encoding toward the upstream, injects a traceparent | x-iag-trace-id |
| 2 gate | Resolves the virtual key to a principal and workspace, checks the model allowlist, runs the budget preflight | refusal with a named reason, or a resolved principal |
| 3 exact cache | Canonicalised request hash, scoped so a hit cannot cross workspace, principal, policy version or catalog version | x-iag-cache: exact | miss |
| 4 trim | Prunes unused tool schemas, drops near-duplicate retrieved chunks, windows old turns — only above a configured prompt size | x-iag-tokens-saved (an estimate, labelled) |
| 5 route | Resolves an alias to a concrete model, or runs a small-first cascade with bounded escalation over allowed models | x-iag-model-routed, x-iag-route-reason |
| 6 invoke | Calls the upstream that serves the routed model; reassembles streamed frames into one response object | x-iag-upstream-request-id |
| 7 verify | Structural and linguistic checks, tool-call schema and business rules, optional spend-bounded judge; holds gated tool calls | x-iag-verdict, x-iag-confidence, x-iag-checks |
| 8 meter | Prices the upstream's own usage block against your catalog rates; cached prompt tokens at the cached rate where your contract has one | x-iag-cost-usd, x-iag-cost-basis, x-iag-savings-usd |
| 9 record | Writes one metadata-only row per inference, outermost so it also observes denials and upstream errors | the row behind the live feed and the audit view |
Verification and approvals run outside the cache, so a cached answer is never delivered with a verdict that was computed for a different policy version.
Streaming #
Request-side stages run before the upstream call, so a streamed request is gated, cached, trimmed and routed exactly like a buffered one. Frames are then streamed to the client as they arrive, with one documented exception: when a request carries tools and the workspace has tool gating on, the response is buffered so that held calls can be stripped before the client sees them. Delivering a tool call and then revoking it is not a thing a client can act on.
In both cases the invoke stage reassembles the full response — content, tool calls and
the usage block — so verify, meter and record see the same object for streamed and
non-streamed calls. On OpenAI streams the gateway injects
stream_options.include_usage=true so the usage block actually arrives;
when it does not, cost is billed at the abort floor and flagged
estimated rather than silently recorded as zero.
Fail posture #
The rule is one sentence: a cache outage must never become an inference outage, and an optimiser must never become a security control.
| Component | Posture | Meaning |
|---|---|---|
| Identity, policy, budget enforcement, gated trust classes | Fail closed | If it cannot be evaluated, the request is refused with a named reason |
| Optimisers — cache, trim, route | Fail open | If they break, the request proceeds unoptimised and the record says so |
| Budget pre-check | Configurable | Per workspace: refuse on an unreadable budget, or proceed and reconcile |
Hot-path rules #
These come from verified upstream behaviour. Breaking one causes silent billing or attribution corruption rather than a test failure, which is why they live in the chain and in the upstream client rather than in a proxy config.
| Rule | Why |
|---|---|
| Strip every inbound identity header before forwarding | The upstream stamps some of them conditionally, so a client-supplied value can otherwise survive into metering and misattribute spend |
| Never convert a funds denial into a rate-limit denial, or the reverse | SDKs and upstream traffic policy both retry one of them, so mislabelling triples upstream attempts on a request that can never succeed |
| Never compress or re-encode toward the upstream | A gzipped response was once metered at zero |
| Cap request bodies in the chain | Beyond the upstream's inspection limit the model field parses empty, which skips model-allowlist evaluation entirely |
| Correlate on the upstream's own request id | Client-supplied correlation ids are discarded upstream by design |
| Send the gateway's own User-Agent | Default SDK user agents have been blocked at the upstream edge with an unlogged refusal |
| Poll, never await a webhook | The platform emits none, by design; alerting is the gateway's job |
| Never follow a redirect toward an upstream | A redirect re-sends the body, and on a same-host hop it keeps the credential — an exfiltration path, not a convenience |
Two listeners #
The split is a security control, not a convenience.
| Listener | Serves | Who reaches it |
|---|---|---|
http.addr — default 0.0.0.0:8080 | /v1/* inference only | applications and agents |
admin.addr — default 127.0.0.1:8081 | console at /, /admin/v1/*, and /v1/* for the playground | operators, on an internal network |
The admin surface mints virtual keys, moves spend caps and rewrites policy. An application that can reach inference must not reach it merely because the two once shared a port, so config validation refuses a boot where the two addresses are equal. Every unmatched path on the application listener returns a JSON error, never the console HTML.
Neither listener terminates TLS. That belongs at your internal load balancer. A deployment that publishes either port to the internet is misconfigured.
Routing #
Routing is never a black box: every response carries the model that ran and the reason it was chosen, and the full decision record is persisted per request.
Virtual model names #
| Name | Resolves to |
|---|---|
iag/fast | the cheapest allowed model in the small tier |
iag/quality | the cheapest allowed model in the large tier |
iag/auto | a small-first cascade with bounded, verification-coupled escalation |
| a concrete catalog id | itself — pinned, with the tier recorded |
Routine-ness is declared at the call site, not guessed per request. Three candidate classifiers were measured and none beat sending everything to the small tier; the measurements are on the evidence page.
Declared upstreams #
A catalog model may declare the endpoint that serves it, so a self-hosted small model
can sit on the small tier and take iag/fast traffic:
- id: local/qwen3-4b
tier: small
input_per_1m: 0 # allowed only because the model declares an upstream
output_per_1m: 0
upstream:
base_url: "http://model-host:8090" # origin only
model: "mlx-community/Qwen3-4B-Instruct-2507-4bit" # its id on that server
Every hot-path rule above still applies, because they live in the chain and the client rather than in the destination. The upstream credential is per endpoint and the Token Factory key is never sent to a declared upstream — config refuses an entry that names it, and refuses one whose origin is the Factory itself.
The catalog id is the only name that escapes: the endpoint's own id for the model is substituted on the way out and translated back on the way in, on both the JSON and the streaming paths. That is not cosmetic, because the meter prices what the response body names.
Availability is the optimiser's fail-open. Each declared endpoint is probed at boot and
on an interval; one that stops answering has its models excluded from routing with the
exclusion named in the route reason, while a request that pinned an
unavailable model is refused with a clear 503 model_unavailable rather
than a failure blamed on the Factory.
Trust boundaries #
The claim the deployment has to support is that prompt content traverses exactly two hops: inside the VPC, and a private interconnect to your own Token Factory region. Corporate identity never leaves the enterprise, and no inference path touches the public internet.
There is one deliberate exception, off by default. A declared upstream is a third
content-bearing flow, and it exists only where your config names it. It is shown on the
catalog and in the console as local:<host>, because an operator has
to be able to see that prompt content reaches somewhere other than the Factory. The
two-hop claim continues to hold while that endpoint sits inside your VPC, and the
deployment documentation states plainly that this is operator discipline rather than
something the gateway enforces.
What is stored #
All gateway state lives in your Postgres: workspaces, virtual keys, policy versions, budgets, the request record, held tool calls and approvals, and the evidence ledger. The request record is metadata only — identity, model, tokens, cost, verdict, routing decision, timings. Prompt and completion text are not written unless body capture is switched on for that workspace with a retention window.
Out of scope, named rather than hidden #
The single-container profile does not include: listener TLS, an OIDC session login for the console, the hosted Envoy form factor, regional edge steering, semantic caching with a real embedder, or upstream catalog synchronisation. Each is a real gap with a reason, and each is listed in the design document rather than implied to exist.