Test evidence
Every number on this site comes from here. Each measurement below was taken against a live Token Factory using the gateway's own counters, and each one is reproducible on your own traffic with the commands at the bottom.
How we measure #
| Rule | Why |
|---|---|
| Counters come from the upstream's own usage block | The gateway's token estimates are approximations and are labelled as such. They are never used for a claim |
| Correctness is scored by deterministic checkers | Never by a model grading another model |
| Latency is wall-clock through the full chain | Not the model's own reported time. What the caller waits for is the number |
| Cost is priced from catalog rates | The same rates the gateway meters with, so the number reconciles against a bill |
| Results that went against the product stay on the page | Two of the measurements below retired a feature we had been describing as a saving |
Prefix caching is the real lever #
Providers cache prompt prefixes. The question was how much of a realistic prompt comes back cached, and what breaks it. One stable system prompt of roughly 860 tokens, called four ways:
| Call | Prompt tokens | Served from cache |
|---|---|---|
| First call | 862 | 64 — the provider's block baseline |
| Identical repeat | 862 | 848 (98%) |
| Same system prompt, different user turn | 862 | 848 |
| One timestamp added at the front | 870 | 64 — back to baseline |
The fourth row is the whole finding. A single volatile value at the front of a prompt costs you the entire cached prefix on every subsequent call. So the gateway canonicalises the cacheable prefix — sorted keys, compact JSON, per-request values moved out of it — prices cached tokens at your contract's cached rate, and puts the hit rate on a tile so a regression is visible the day it happens.
On a mixed session workload rather than a synthetic repeat, 78% of prompt tokens came back cached.
History windowing lost to it #
Trimming old turns out of a conversation is the classic token-saving move, and it was the headline of our own earlier material. Measured on a ten-turn conversation, with trimming off and then on:
| Turn | Trim off — in / cached | Trim on — in / cached | Tokens trimmed |
|---|---|---|---|
| 8 | 5089 / 3664 | 5089 / 5088 | 0 |
| 10 | 6461 / 5088 | 4298 / 80 | 2163 |
Trimming removed 2,163 real tokens and destroyed the cached prefix, dropping it from 5,088 tokens to 80. The trimmed request has fewer tokens and more of them at full price. It only wins at a cached discount below roughly 43%, which no provider offers.
So windowing is no longer a savings claim. It is reserved for a context that would not otherwise fit, and the default threshold is set high enough that it stays out of the way.
A local model as the small tier #
A 4-billion-parameter model running on a laptop, declared as a small-tier catalog entry, against the Factory's small tier. Five uncached calls each, measured end to end through the full gateway chain, on a task both answered correctly:
| Routed model | p50 latency | Cost per call | Correct |
|---|---|---|---|
| Local 4B, declared upstream | 162 ms | $0.000000 | yes |
| Factory small tier | 1303 ms | $0.000014 | yes |
Roughly an eighth of the latency on routine work, and it loses clearly on hard work.
That is exactly why it is a small-tier entry a caller opts into with
iag/fast, and not a cache placed in front of everything. The honest value
here is latency and locality: upstream spend on routine work was already small.
The outage cycle #
Exercised end to end against the running stack:
| Condition | Observed |
|---|---|
| Local endpoint healthy | iag/fast routed to the local model, $0, metered |
| Endpoint stopped, after the next probe | Fell through to the Factory small tier; route reason read (excluded, upstream unavailable: local/qwen3-4b) |
| A request that pinned the local model while it was down | 503 model_unavailable, naming the model, rather than a failure blamed on the Factory |
| Endpoint restarted | Routed back to the local model on the next probe |
| Streaming through the declared endpoint | Translated per frame; the endpoint's internal model id appeared in zero frames |
Three classifiers, none of which earned its place #
The obvious feature is a router that reads each request and decides whether it needs the expensive model. We tried three: a 4-billion-parameter local model as a judge, a published task-and-complexity encoder, and an open-source router trained on human preference data. Each was scored on the only question that matters operationally — will the cheap tier get this right — against deterministic checkers.
Results for the strongest of the three, on 32 prompts built so the cheap tier had something to fail:
| Policy | Sent to the expensive tier | Correct | Cost per 1,000 requests |
|---|---|---|---|
| Everything to the small tier | 0 | 94% | $0.0341 |
| Everything to the medium tier | 32 | 91% | $0.1116 |
| Router, threshold 0.5 | 24 | 91% | $0.0962 |
| Router, threshold 0.7 | 5 | 91% | $0.0556 |
Every routing threshold cost more and was no more correct. The cause is not the routers. On this catalog and this traffic the expensive tier is simply not more correct than the cheap one: on the hard half of the set the small tier scored 88% and the medium tier 81%. A router can only convert cost into quality where a quality gap exists to buy.
There is also a boundary problem worth stating: the two strongest routers in that open-source package call a third-party embedding API for every prompt. On a gateway whose purpose is to keep prompt content on a governed path, that is disqualifying regardless of accuracy.
This is why routing is declared at the call site rather than inferred. If your catalog pairs tiers that do have a real quality gap, the measurement takes a task file and a ground-truth file and reruns in minutes.
Tool memoisation, stated precisely #
Serving a repeated read-only tool call from what the conversation already answered saves the client one tool execution and one full round-trip of wall-clock latency per repeat. It does not save upstream tokens: the continuation is an inference the client would have made anyway, and both inferences are metered.
Its value therefore depends entirely on the repeat rate of read-only calls in your own agent traces — which the evidence rows make queryable, so you can measure it rather than take a number from us.
What pre-merge review caught #
Security findings are evidence too. The most recent change to the request path — letting a catalog model declare its own upstream — went through an adversarial review before merge. Six defects were found and fixed in the same change, each with a regression test that fails without the fix.
| Finding | Why it mattered |
|---|---|
| The upstream client followed HTTP redirects | A declared endpoint answering a redirect received the full prompt body, and on a same-host hop the credential survived with it. Both the traffic client and the health probe now refuse redirects |
| A case-variant model name escaped the declared endpoint | The allowlist matched case-insensitively while dispatch did not, so a pinned local model could silently go to the Factory instead. Pinned names are now canonicalised |
| One leaked goroutine per abandoned stream | Measured at 52 resident after 50 aborted streams. Now zero |
| Nothing stopped an endpoint being handed the Factory credential | Naming the same environment variable twice would have done it. Both that and declaring the Factory's own origin are now refused at boot |
| The health probe was unauthenticated | An endpoint requiring a bearer was marked permanently unavailable, silently diverting traffic back to the Factory |
| The CISO-facing boundary document still claimed two hops absolutely | It now carries the new flow, and states that keeping that endpoint inside your VPC is operator discipline rather than an enforced control |
Results that went against us #
Kept here on purpose, because a page of only favourable results is not evidence.
- Our headline token-saving feature lost to the provider's cache. History windowing is no longer a savings claim.
- Our own "tokens saved" counter was overstated. It is a characters-per-token estimate, it was reporting more than the upstream's counters supported, and it is now labelled an estimate and excluded from every claim.
- Three attempts at automatic routing all failed to beat the simplest policy. The feature was not shipped; the decision was moved to the caller.
- The local model is not a general win. It matches on routine work and loses clearly on hard work, which is why it is opt-in and tier-scoped.
Reproduce it on your own traffic #
Nothing here needs our data. Point the gateway at your Factory and read your own counters.
# prefix cache: same prompt twice, watch the cached share
for i in 1 2; do
curl -si localhost:8080/v1/chat/completions -H "Authorization: Bearer $KEY" \
-d '{"model":"iag/fast","messages":[{"role":"system","content":"<your real system prompt>"},
{"role":"user","content":"hello"}]}' \
| grep -i 'x-iag-upstream-cached-tokens\|x-iag-cost-usd'
done
# the same prompt with a timestamp in front — the cached share should collapse
# then check the aggregate over a day, from the admin API
curl -s -H "$A" "$B/overview?workspace=ws-pilot&window=24h" \
| jq '{cache_hit_rate, upstream_prefix_hit_rate, spend_usd, savings_usd}'
# small tier versus large tier on YOUR tasks, scored by your own checkers:
# run each prompt through both, compare correctness, then compare cost per 1k.
curl -s -H "$A" "$B/requests?workspace=ws-pilot&model=openai/gpt-oss-20b" | jq '.data[].cost_usd'
If a measurement on your traffic disagrees with a number on this page, the number on this page is the one that is wrong. Tell us what you saw and we will publish the correction.