User guide
Operating the gateway once it is running: how callers choose a model, what policy controls, how verdicts and approvals work, and where the money goes. If you have not installed it yet, start with the installation guide.
Vocabulary #
| Term | Means |
|---|---|
| Workspace | A tenant. Maps to exactly one upstream project; credentials are never multiplexed across tenants |
| Principal | Who is calling — a person, an application, an agent or a CI job |
| Virtual key | What an application actually holds. Scoped to a workspace and principal, with a TTL. Never the upstream credential |
| Policy | What a workspace may do: allowed models, feature toggles, tool gates, baseline, budget posture. Versioned; every PUT bumps the version |
| Catalog | The models you configured, with tiers, capabilities and your contract rates |
| Baseline model | The model savings are measured against. A savings number with no baseline is marketing |
| Verdict | The verification outcome for one response: pass, flag, fail or skipped |
| Record | One metadata-only row per inference, behind the live feed and the audit view |
Choosing a model #
Callers put one of four things in the model field. The gateway does not
guess how hard a request is — that was measured and it did not work, so the decision
stays where the knowledge is, at the call site.
| Ask for | You get | Use it when |
|---|---|---|
iag/fast | Cheapest allowed small-tier model | Classification, extraction, formatting, short factual answers, anything an agent does in a loop |
iag/quality | Cheapest allowed large-tier model | Work you would not want retried: long reasoning, code, anything customer-facing |
iag/auto | Small first, with bounded escalation when verification is unhappy | Mixed traffic where you would rather pay a little more than hand-classify |
| a catalog id | Exactly that model | You have a reason. The record shows it was pinned |
Whatever the caller asks for, the response says what actually ran and why:
x-iag-model-routed: openai/gpt-oss-20b
x-iag-route-reason: alias iag/fast → cheapest allowed small-tier model
(excluded, upstream unavailable: local/qwen3-4b)
Policy #
Policy is per workspace and versioned. The cache key includes the policy version, so publishing a policy cannot serve an answer that an older policy allowed.
PUT /admin/v1/policy/ws-pilot
{
"allowed_models": ["openai/gpt-oss-20b", "openai/gpt-oss-120b"],
"features": {"routing": true, "cache": true, "trim": true,
"verify": true, "tool_gating": true},
"tool_gates": {"lookup_order": "read", "issue_refund": "financial"},
"tool_memo": {"lookup_order": {"ttl": "10m", "scope": "principal"}},
"baseline_model": "openai/gpt-oss-120b",
"budget_posture": "fail-closed"
}
A request naming a model outside allowed_models is refused at the gate,
before it costs anything. The allowlist is intersected with the catalog, so a policy
cannot introduce a model the operator did not configure.
Feature toggles #
| Toggle | Off means |
|---|---|
routing | Aliases resolve to the baseline model instead of cascading. The reason still says so |
cache | Every request reaches the upstream. Useful when you are measuring something |
trim | No context optimisation at all. Often the right setting — see why |
verify | Verdicts come back skipped rather than fabricated as passes |
tool_gating | Tool calls pass through unheld. Turning it on buffers streamed tool responses so held calls can be stripped |
Budgets #
PUT /admin/v1/budgets
{"workspace":"ws-pilot","principal":"alice","period":"monthly","cap_usd":250}
Caps are per workspace, or per principal within one. budget_posture
decides what happens when the budget cannot be read:
fail-closed refuses the request, fail-open serves it and
reconciles afterwards. A breach returns a structured error naming the budget and what
to do about it — never a silently truncated answer.
Verdicts and approvals #
Reading a verdict #
Verification is a set of named structural checks, not a truth oracle. It catches the failures that have a shape: an empty completion, a response truncated by the token limit, invalid JSON where the request demanded JSON, a refusal template, hedging density, repetition loops, a language mismatch, a tool call that violates its schema or a business rule.
| Verdict | Means | What the gateway does |
|---|---|---|
pass | No signals, confidence at or above 0.8 | Delivers it |
flag | Soft signals — hedging, repetition, a language mismatch | Delivers it and records why |
fail | Hard signals — invalid JSON on a JSON contract, a schema violation on a gated tool | Holds the tool call. Content is reported, never rewritten |
skipped | Verification off for this workspace | Says so, rather than reporting a pass |
An optional judge can run as a second layer through your own small tier, spend-bounded per check. It is off by default, because it costs money on every request and the structural checks catch the shaped failures for free.
Tool gates #
A tool is classified by what it does, and the class decides whether a human sees it first. Read-class calls flow. Financial and external-communication classes are held.
"tool_gates": {
"lookup_order": "read", // flows
"send_email": "external-comms", // held for approval
"issue_refund": "financial" // held for approval
}
A held call is stripped from the response before the client sees it, and the caller re-requests after the approval lands. That is deliberate: handing a client a tool call and then revoking it is not something the client can act on.
With tool gating on, a response that cannot be parsed at all is refused rather than delivered. A body whose tool calls cannot be inspected is a body whose tool calls cannot be gated.
The approval inbox #
curl -s -H "$A" "$B/approvals?workspace=ws-pilot&status=pending" | jq .
curl -s -H "$A" -X POST "$B/approvals/$ID/approve"
curl -s -H "$A" -X POST "$B/approvals/$ID/deny" -d '{"reason":"customer not verified"}'
Every decision is persisted with the actor, the reason and the time, and written into a hash-chained evidence ledger. The point is not the queue; it is that six months later you can reconstruct who allowed what, and prove the record has not been edited since.
Spending less #
In the order that actually matters, measured rather than assumed.
Keep the prefix stable #
This is the single biggest lever, and it is mostly about what you stop doing. Providers cache prompt prefixes; a stable system prompt comes back almost entirely from cache on every call after the first. One volatile byte at the front destroys that.
| Do | Do not |
|---|---|
| Keep the system prompt and tool schemas byte-identical between calls | Put a timestamp, request id, nonce or trace id anywhere in the prefix |
| Let the gateway canonicalise: sorted keys, compact JSON, volatile fields moved out of the prefix | Re-serialise tool schemas per request with unstable key order |
| Put per-request values in the user turn, at the end | Interpolate the current date into the system prompt "for context" |
| Watch the prefix hit rate tile | Assume it is working |
The gateway prices the provider's cached prompt tokens at your contract's cached rate when you have configured one, and reports the share on every response and on the overview.
Tool-call memoisation #
Agent loops re-issue the same read-only call across turns — the same lookup on turn three and again on turn eleven. The gateway never executes tools, but it sees both the call and its result in the conversation, so it can finish the round-trip itself.
Learning is passive and costs nothing: the client already paid for those executions. When a model proposes calls that are all memo hits, the gateway appends the remembered results and re-invokes, and the client gets the continuation. A mixed proposal is returned untouched, because the client has to execute the miss anyway.
| Guard | Rule |
|---|---|
| Opt-in | Nothing is memoised unless the policy names the tool |
| Read-class only | Refused at write time and again at runtime. A side-effecting call served from memory would be a phantom action |
| Scope | principal by default; workspace is a per-tool choice for shared reference data. Never across workspaces |
| TTL, in process | A tool result is content, so the memo is bounded, in memory, and dies with the process |
| Bounded | At most three continuations per request. A model looping on a memoised call is a loop, not a saving |
| Auditable | An evidence row per served call, a count on the record, and x-iag-tool-memo on the wire |
What it saves is the client's tool execution and one full round-trip of latency per repeat. It does not save upstream tokens, and the product does not claim that it does.
Admin API #
Prefix /admin/v1 on the admin listener, bearer authentication. Lists
return {data, next}; errors return {error: {code, message}}.
| Endpoint | Purpose |
|---|---|
GET /overview?workspace=&window=24h | Requests, spend, baseline, savings, tokens, cache hit rate, blocked, held, p50/p95 latency, plus an hourly series |
GET /requests?workspace=&after=&model=&verdict=&cache= | Live feed and audit, newest first, cursor-paged |
GET /requests/{trace_id} | One record with the full routing decision |
GET /approvals · POST /approvals/{id}/approve · /deny | The inbox |
GET /keys · POST /keys · DELETE /keys/{id} | Virtual keys. The token is returned once |
GET /policy/{ws} · PUT /policy/{ws} | Allowlist, toggles, gates, baseline, posture. PUT bumps the version |
GET /budgets · PUT /budgets · DELETE /budgets/{id} | Caps |
GET /catalog | Configured models with tier, rates, capabilities, context and which upstream serves them |
GET /workspaces · POST /workspaces | Tenancy |
GET /health | Readiness payload for the console header |
The console #
Served at / on the admin listener, on the same origin as the API. Every
number on screen comes from the endpoints above. There is no fixture mode and no demo
data.
| Page | What it answers |
|---|---|
| Overview | What did this cost, what did it save against the baseline, how much came from cache |
| Live feed | What is happening right now, filterable by model, verdict and cache outcome |
| Playground | The console calling the inference surface with a minted key — the same path an application takes |
| Approvals | What is waiting on a human, and what was decided |
| Keys | Who holds what, and revocation |
| Policy & Budgets | What each workspace may do and spend |
| Audit | One request, end to end, including the routing decision record |
Day to day #
| Watch | Because |
|---|---|
| Prefix hit rate | A sudden drop usually means something started interpolating a value into a system prompt |
Records flagged rate: floor | A model is being used that is not in your catalog, so its cost is a safety net rather than a price |
x-iag-cost-basis: estimated | Usage blocks are not arriving; usually aborted streams |
| Verdict mix | A rising flag share is a prompt regression long before it is a complaint |
| Held call age | An approval queue nobody reads is a broken workflow, not a control |
| Route reasons mentioning exclusions | A declared upstream is down and traffic is quietly falling back |