← All notes
AI

Models that stay inside

The chat box is the easy part. The hard part is one entry point: who is calling, what it costs, and what happens when legal says the text cannot go to a SaaS.

28 Jul 2026 13 min EN · ES

We ship AI gateways on the same contract as the rest of the platform: rotated keys, per-team quotas, caching when the prompt repeats, traces you can join with the rest of the telemetry. LiteLLM or a proxy you own — the logo does not matter. What matters is that ten API keys stop living in repositories.

This note is the long version: what the gateway actually does, how you control cost without slowing the product team down, what changes when inference has to run inside, and how you operate all of it without inventing a new discipline.

Why a single entry point

The pattern we meet is almost always the same. Three teams discovered models separately, each with its own key, its own SDK and its own way of counting tokens. The bill arrives aggregated and nobody can attribute it. When the vendor changes a model or introduces a rate limit, the incident shows up in three different places and gets diagnosed three times.

A gateway solves that because it turns a diffuse external dependency into an internal interface with an owner. The properties we are after:

The contract with the product team

Adoption is won with compatibility. If the gateway speaks the same dialect as the SDK they already use, migrating costs one environment variable; if you invent your own API, the gateway becomes a toll booth and people route around it.

-  OPENAI_API_KEY=sk-live-...            # provider key sitting in the service
-  OPENAI_BASE_URL=https://api.openai.com/v1
+  OPENAI_API_KEY=${GATEWAY_TOKEN}       # internal token, one per team
+  OPENAI_BASE_URL=https://ai.internal/v1

# the service asks for a logical name, not a provider model
model: "summarise-fast"

That logical name is the piece that pays off most over time. When a cheaper model appears, or a workload has to move to internal inference, you change the mapping in one place. Without logical names, every provider change is a pull-request campaign across the whole monorepo.

model_list:
  - model_name: summarise-fast
    litellm_params: { model: openai/gpt-4o-mini, api_key: os.environ/OPENAI_KEY }
  - model_name: summarise-fast          # fallback
    litellm_params: { model: vllm/qwen2.5-7b, api_base: http://vllm.ai.svc:8000/v1 }

router_settings:
  routing_strategy: latency-based-routing
  num_retries: 2
  fallbacks: [{ summarise-fast: ["summarise-internal"] }]
  cooldown_time: 60

Cost: budgets, not surprises

Model cost behaves unlike the rest of your infrastructure: it scales with user behaviour and prompt size, not with replica count. A badly placed retry loop, or a context that grows without truncation, can multiply the bill within a day, and the usual way to find out is at the end of the month.

What we put in the gateway from day one:

# the useful alert is not "cost went up", it is "cost per request went up"
sum by (team, feature) (rate(llm_cost_usd_total[1h]))
  / sum by (team, feature) (rate(llm_requests_total[1h]))
  > 1.5 * avg_over_time(... [7d])

The distinction matters: if total cost rises because the product has more users, that is good news. If cost per request rises, something changed in the prompt, the model, or the retry logic, and you want a page.

When the data cannot leave

Then the gateway does not point at a vendor. It points at inference in your cluster: vLLM, Ollama, GPU Operator, the same GitOps you already use. Product still talks to one endpoint. Infra decides whether that call leaves or stays.

Serving a model inside is not just deploying one more container. The differences worth planning for:

Without that switch between outside and inside, “AI in the product” becomes a permanent exception: another network, another bill, another incident nobody knows who owns. With it, it is one more service on the map.

The data boundary is declared, not remembered

“Don’t send personal data to the API” is not a control, it is a hope. The control lives on the request path: network policy stops product pods from reaching the internet, so the only possible exit is the gateway, and the gateway enforces classification.

In practice, each logical route carries a sensitivity label. Routes marked internal may only resolve to models running inside, and that constraint is checked in CI against the configuration, not at runtime when it is already too late.

Logging counts too. Storing full prompts is tempting for debugging and is exactly the compliance problem you were trying to escape. We store metadata and hashes by default, and content only under sampling, with short retention and audited access.

Failure modes that do not look like the usual ones

The same team operates it

We do not hand over a notebook and leave. The gateway lands with a pipeline, a runbook, and alerts that only fire when cost or latency leaves the agreed band. Its configuration lives in Git and gets promoted like any other service: by digest, with a canary, with a rehearsed rollback.

The runbook answers concrete questions: what to do when the primary provider is down, how to move a workload to internal inference while live, how to raise a team’s budget during a launch and how to revert it afterwards, and who approves a new route being allowed to leave the network.

If a product piece is missing around it — an internal portal for tokens and quotas, an on-call bot, a cost-per-feature dashboard — we build that too.


If you already have three teams calling models and no way to know what it costs or what text leaves the network, send us two lines. You get an initial read and a ballpark quote within 24h.