incident review · ai-gateway
AI Gateway
One endpoint for every model
front matter · ai-gateway
- status
- resolved · monitoring
- date
- 2026
- filed by
- Jack Devlin, platform engineer · Loweconex, a UK IoT platform business
- scope
- every AI workload, one endpoint
- impact
- none. Built while there were two consumers.
- outcome
- adding an AI feature is a config change, and spend lands against the tenant and feature that caused it
- summary
- A self-hosted LLM gateway in front of every AI workload. Services hold a virtual key with a model allowlist.
exhibittry it
Two different ways to get a 401
Key issued, allowlist never populated. This is the trap.
finding 1before: no gateway
The third API key is the one that hurts
Our first AI feature shipped with a provider key in a secret, and that was fine. By the third I could see where it was heading: keys scattering across repos, and no way to say what any of it cost.
None of that is an AI problem. It's the same problem as unmanaged database credentials, and the fix is the same one: put the credential somewhere central and hand out scoped access instead.
LiteLLM is an open-source LLM proxy. I didn't write a proxy, I ran that one on Kubernetes through GitOps like everything else we deploy. The work worth describing is the key and allowlist model, the attribution scheme, and the runbook.
finding 2request · auth + allowlist
Virtual keys, with explicit model allowlists
Services don't hold provider credentials any more. They hold a virtual key with an explicit list of models it may call. Ask for anything else and you get a 401.
A gateway that helpfully substitutes another model gives you something that works and behaves differently from the thing you tested. I'd rather it broke.
# 1. the gateway has to know the model exists
# (model_list entry in the gateway config, shipped via GitOps)
# 2. every key that should reach it has to be told, separately
curl -X POST "$GATEWAY/key/update" \
-H "Authorization: Bearer $MASTER_KEY" \
-d '{"key": "sk-…", "models": ["gemini-2.5-flash", "gemini-3.6-flash"]}'
# skip step 2 and consumers get 401 for a model that
# demonstrably exists. what can this key actually reach?
curl -H "Authorization: Bearer $VIRTUAL_KEY" "$GATEWAY/v1/models"Those two halves being separate is the sharp edge, and it has caught me more than once: a model can be fully deployed and still 401 for everybody, because being deployed and being permitted are separate facts. It's in the runbook now.
finding 3request · spend tags
Whose spend is it?
Every call carries its tenant, plus tags for environment and feature. Chat, scheduled estate summaries and the nightly schema compile all bill to one service but answer completely different questions about cost.
So "what is the AI costing us" became answerable per tenant and per feature, which matters the first time somebody asks about cost to serve.
finding 4postmortem · attached
The 4x pricing bug
Attribution being right is not the same as the number being right. This one is the reason the last action item is still open.
- symptom
- Spend read about fourfold high, for weeks.
- cause
- The cost dashboard multiplies tokens by per-million prices held as dashboard variables, and ours were set to a different model's pricing than the one deployed.
- why it lived
- Nobody questioned it, because the number was in a dashboard and dashboards look authoritative.
- lesson
- Tokens are measured. Prices are config, and config rots.
finding 5after: what got cheap
What it made cheap
There's an automated pull request review agent running on PRs across eight production services now, and building it needed no provider credentials, no billing conversation, no new secret.
Model upgrades became config too. Moving a consumer between versions is an allowlist entry and an env var, which is how every consumer followed three generations of the same model family without anyone rewriting one.
What the review tracked, and the one item still open:
| id | action | status |
|---|---|---|
| 01 | Collapse to one gateway, environment as a tag It started as a deployment per environment. One instance means fewer moving parts, and spend I can compare across environments rather than sum across dashboards. | shipped |
| 02 | Write down the two-step onboarding trap The first time it caught me I lost an afternoon to a model that was demonstrably deployed. | runbook |
| 03 | Stop dashboard price constants from rotting Closing this properly means something that compares those constants against what the provider actually charges, on a schedule. | open |
resolution200 OK · monitoring
Where it stands
It's dull infrastructure now, which is what I wanted. I built it while there were two consumers rather than twelve, so adding an AI feature is a config change. Doing it later would have meant unpicking a dozen scattered provider keys first.
The pricing bug and the runbook both have longer versions than fit here; this page has the parts that changed the design. say hello.