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 #

StageWhat it doesWhat it emits
1 wire hygieneCaps the body, strips every inbound identity header, preserves identity content-encoding toward the upstream, injects a traceparentx-iag-trace-id
2 gateResolves the virtual key to a principal and workspace, checks the model allowlist, runs the budget preflightrefusal with a named reason, or a resolved principal
3 exact cacheCanonicalised request hash, scoped so a hit cannot cross workspace, principal, policy version or catalog versionx-iag-cache: exact | miss
4 trimPrunes unused tool schemas, drops near-duplicate retrieved chunks, windows old turns — only above a configured prompt sizex-iag-tokens-saved (an estimate, labelled)
5 routeResolves an alias to a concrete model, or runs a small-first cascade with bounded escalation over allowed modelsx-iag-model-routed, x-iag-route-reason
6 invokeCalls the upstream that serves the routed model; reassembles streamed frames into one response objectx-iag-upstream-request-id
7 verifyStructural and linguistic checks, tool-call schema and business rules, optional spend-bounded judge; holds gated tool callsx-iag-verdict, x-iag-confidence, x-iag-checks
8 meterPrices the upstream's own usage block against your catalog rates; cached prompt tokens at the cached rate where your contract has onex-iag-cost-usd, x-iag-cost-basis, x-iag-savings-usd
9 recordWrites one metadata-only row per inference, outermost so it also observes denials and upstream errorsthe 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.

ComponentPostureMeaning
Identity, policy, budget enforcement, gated trust classesFail closedIf it cannot be evaluated, the request is refused with a named reason
Optimisers — cache, trim, routeFail openIf they break, the request proceeds unoptimised and the record says so
Budget pre-checkConfigurablePer 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.

RuleWhy
Strip every inbound identity header before forwardingThe 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 reverseSDKs 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 upstreamA gzipped response was once metered at zero
Cap request bodies in the chainBeyond the upstream's inspection limit the model field parses empty, which skips model-allowlist evaluation entirely
Correlate on the upstream's own request idClient-supplied correlation ids are discarded upstream by design
Send the gateway's own User-AgentDefault SDK user agents have been blocked at the upstream edge with an unlogged refusal
Poll, never await a webhookThe platform emits none, by design; alerting is the gateway's job
Never follow a redirect toward an upstreamA 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.

ListenerServesWho reaches it
http.addr — default 0.0.0.0:8080/v1/* inference onlyapplications and agents
admin.addr — default 127.0.0.1:8081console at /, /admin/v1/*, and /v1/* for the playgroundoperators, 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 #

NameResolves to
iag/fastthe cheapest allowed model in the small tier
iag/qualitythe cheapest allowed model in the large tier
iag/autoa small-first cascade with bounded, verification-coupled escalation
a concrete catalog iditself — 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.