Skip to content

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

consumer key
requested model

Key issued, allowlist never populated. This is the trap.

authenticate virtual key
model in gateway config
model on this key's allowlist
stamp attribution
route to provider
write spend log
It opens on the trap: a key that was issued and never given an allowlist. Pick a different consumer or model and the trace shows exactly where the refusal happens, including the combinations that 401 for a model which is fully deployed.

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.

onboarding a model, both halves required
# 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.

postmortem· severity: low, caught internally
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:

action items3 on file · 1 open
actionstatus

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

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

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.

review closed· ai-gateway · sign-off

The pricing bug and the runbook both have longer versions than fit here; this page has the parts that changed the design. say hello.