Integrate & ship
AI Gateway
An OpenAI-compatible endpoint in front of your agents and connected models, and a metrics API in front of the semantic layer. Point any OpenAI SDK, IDE plugin, evaluation harness or other agent at /api/v1/ with a gateway key; every call runs as the key's owner, under that owner's model rules, budgets, traces and audit trail.
What it is#
The platform already governs every model call it makes. Until now only callers that spoke its own request shape could reach an agent. A gateway key opens the OpenAI shape without opening anything else: it is minted by one user, reaches only that user's agents and the models that user may call, pays from that user's budgets, and is traced and audited under that user's name. This is the inbound half of the gateway; the outbound half — routing the platform's own traffic through LiteLLM, Portkey or Helicone — is the setting beside it on the same tab and is unchanged.
Keys#
Mint a key under Integrations → LLM Gateway → API access. The plaintext gw_… is shown once; the row keeps a hash and the first characters. Revoking is immediate and permanent, and every change to a key is audited by trigger.
| Setting | What it does |
|---|---|
| Scopes | agents lets the key call your saved agents (model = agent:<name or id>); models lets it call a connected model directly (model = <provider>/<model>); metrics lets it read the semantic layer (GET /metrics, POST /metrics/query). |
| Agents | With the agents scope, an optional allow-list; none ticked means every agent you own. |
| Model patterns | With the models scope, provider/model patterns the key may call (openrouter/*, anthropic/claude-*); empty means anything your IAM model rules allow. |
| Semantic models | With the metrics scope, an optional allow-list of semantic models the key may query; none ticked means every model you own or are granted. Naming a model never grants access you lack. |
| Fallback chain | Ordered provider/model entries tried when the requested model fails with a provider error, before the instance-wide chain. |
| Calls a minute | A per-key rate limit; blank uses the instance default. |
| Monthly budget | A ceiling in USD on what this key may spend, measured from the traces it makes. |
| Expires | Optional; an expired key answers 401 the moment it lapses. |
Calling it#
Base URL https://<your host>/api/v1/. GET /models lists what the key may name; POST /chat/completions runs one turn, streamed as server-sent events when stream is true.
curl https://<your host>/api/v1/chat/completions \
-H "Authorization: Bearer gw_..." \
-H "Content-Type: application/json" \
-d '{"model": "agent:Support triage", "stream": true,
"messages": [{"role": "user", "content": "A customer reports a failed payment"}]}'from openai import OpenAI
client = OpenAI(base_url="https://<your host>/api/v1/", api_key="gw_...")
reply = client.chat.completions.create(
model="openrouter/openai/gpt-4o-mini",
messages=[{"role": "user", "content": "What is a lakehouse?"}],
)
print(reply.choices[0].message.content)- Taken from the request:
model,messages(system and developer messages become the instruction; tool messages are dropped, because an agent runs its own tools),stream,temperature,max_tokens, andstream_options.include_usage. Client-suppliedtoolsare ignored: an agent's tools are the ones its owner configured, and a bare model has none. - Added to the reply: an
agentswarmsobject with the trace id, the model fallen back from, citations, tool calls and any guardrail note; headersX-Trace-Id,X-Gateway-Model(the model that answered),X-Gateway-FallbackandX-Gateway-Cache. - No memory between calls. The caller holds the conversation and replays it, the way the OpenAI API works; a key is not a person.
The semantic layer#
A key with the metrics scope answers governed questions without an agent in between. GET /metrics lists the semantic models the key may query, described for a client: names, labels, each metric's aggregation and format, each dimension's type, synonyms and sampled values, the declared parameters, the hierarchies, and the grains and period comparisons a time dimension accepts — never the SQL behind them. A shared model with a restricted grant is listed with its access_note and without the masked fields. POST /metrics/query runs one query in the same structured shape the runner and the metric_query agent tool use.
curl https://<your host>/api/v1/metrics -H "Authorization: Bearer gw_..."
curl https://<your host>/api/v1/metrics/query \
-H "Authorization: Bearer gw_..." \
-H "Content-Type: application/json" \
-d '{"model": "revenue", "metrics": ["net_revenue", "orders"],
"dimensions": ["region", "order_date"], "grains": {"order_date": "month"},
"filters": [{"field": "order_date", "op": "last_n_days", "value": 180}],
"order_by": [{"field": "order_date", "dir": "desc"}], "limit": 500}'- The body:
model(a name or id fromGET /metrics),metrics,dimensions,filters(field, op, value — the comparison ops and the relative-date windows such aslast_n_days,this_month,ytd),grains(dimension → day, week, month, quarter, year or a fiscal grain),order_by,limit,compare(prior_period, mom, yoy) andparamsfor declared parameters. A request that fails validation answers 400 naming the field; one the compiler refuses answers 400 in the compiler's words. - The answer:
{ object: "metrics.result", model, columns, rows, row_count, truncated, sql }, withaccess_notefor a restricted share,rollupwhen a pre-aggregate answered andresolution_noteswhen a synonym was resolved. Rows stop at the request'slimit, the instance cap (AI_GATEWAY_METRICS_MAX_ROWS, 10,000 by default) or the semantic layer's own ceiling of 10,000 rows, whichever is smallest;truncatedsays whether more matched. - Governance is the semantic layer's own. The query runs as the key's owner through the same chokepoint as a dashboard tile or an agent's
metric_querycall — the owner's models plus the ones IAM shares with them, a grantee's row filters and field masks rewritten into the query, the data read as the model owner — and auditsmetric.querywithvia: gateway, the key, the compiled SQL and a digest of the result. The key's allow-list narrows that access and never widens it. A metrics query makes no model call, so budgets are not touched. See Semantic layer.
Fallback#
When the requested model fails with a provider error — throttling, exhausted credits, a timeout, a 5xx — the gateway tries the key's chain, then the instance-wide chain, each entry once, and answers with the first that works. For an agent only the model changes; the prompt, tools and knowledge stay the agent's. A caller's own mistake is never retried, and a policy refusal of the model the caller asked for answers 403; a forbidden model that only appears in a chain is skipped. A fallback happens only before any token has reached the caller. Every switch is audited as gateway.fallback.
Governance#
- Identity. The turn runs on the chat route's internal channel as the key's owner; data tools read what the owner may read.
- Model rules. IAM allow-lists apply to the model that answers, requested or fallen back to. See Access control.
- Budgets. The owner's personal and group budgets apply, and the key's own monthly ceiling on top; every trace made through the key carries the key as its cost scope, so the ceiling is measured. Over budget answers 429
insufficient_quota. - Audit.
gateway.chatfor every turn — including whether the cache answered it —gateway.cache.clearwhen an owner empties the cache,gateway.fallbackfor every switch,gateway.access.deniedfor a revoked, expired, out-of-scope or throttled key with the caller's address; agent turns also auditagent.chat. A metric query auditsmetric.querywithvia: gateway, the key, the compiled SQL and a digest of the result. - Traces. Each turn is an execution trace under the owner with the agent's name, so Observability shows gateway traffic beside everything else.
Errors use the OpenAI shape
{ "error": { "message", "type", "code" } } with codes invalid_api_key (401), insufficient_scope and model_not_allowed (403), model_not_found (404), rate_limit_exceeded and insufficient_quota (429), invalid_request_error (400), upstream_error (502).Semantic cache#
The same question asked twice costs twice. A key can switch on a cache that matches on the meaning of a question rather than its bytes, so “what was revenue last quarter” and “how much revenue did we make last quarter” are one question, answered once. It is off unless a key turns it on — on the key when it is minted, or from the Cache column afterwards. Nobody inherits one by upgrading, because a cache that answers a question with a nearly identical question's answer is a correctness risk that has to be chosen deliberately.
An entry belongs to one owner, one target (the agent id, or provider/model for a bare model call) and one system instruction. No answer crosses a user, an agent, or a differently instructed run of the same agent. There is no global cache and no way to ask for one.
- Never cached: a conversation with a history. “And for Europe?” means nothing without what came before it, and matching on the last message alone would answer it with whatever the last person who asked that got.
- Never cached: a turn above the temperature ceiling. A high temperature asks for variety; serving it from cache answers a different question than the one asked.
- Never cached: an answer that used a tool. It read something live, and freezing it for a day would serve yesterday's number tomorrow.
- What you get back. Every reply carries
X-Gateway-Cache—hit,miss,skip(the cache is on but this call was not eligible) oroff. A hit setsX-Gateway-Modelto the model that wrote the answer, putscachedin theagentswarmsobject with the similarity and the question it matched, and reports zero usage, because a hit spends nothing at the provider. - It still costs one embedding of the question, on the owner's connected embedding provider — the same one the knowledge bases use, roughly three orders of magnitude less than a completion. With no embedding provider connected, every call simply misses.
The cache is consulted after every check that can refuse a call — inactive key, wrong scope, forbidden model, exhausted budget — and before the first provider call. A refused key stays refused whether or not the answer happens to be sitting in a table. A hit audits gateway.chat with cache: hit, the similarity and zero tokens; flipping the switch on a key audits through the key table's own trigger; and emptying the cache from the same card audits gateway.cache.clear.
0.97 similarity is deliberately high
Limits#
| Setting | Default | Where |
|---|---|---|
| AI_GATEWAY_RATE_LIMIT_PER_MIN | 60 | Calls a minute one key may make unless it sets its own; Admin → Developer runtime. |
| AI_GATEWAY_FALLBACK_MODELS | none | Comma-separated provider/model entries every call may fall back to, after the key's chain. |
| AI_GATEWAY_METRICS_MAX_ROWS | 10,000 | Rows one metrics query may return; a smaller limit in the request wins. Admin → Developer runtime. |
| AI_GATEWAY_CACHE_SIMILARITY | 0.97 | Similarity a cached question must reach to answer a new one; Admin → Developer runtime. |
| AI_GATEWAY_CACHE_TTL_HOURS | 24 | Hours a cached answer stays reusable; expired rows are never served. |
| AI_GATEWAY_CACHE_MAX_TEMPERATURE | 0.3 | Above this temperature a turn is neither served from nor written to the cache. |
| Fallback entries per key | 10 | Entries that do not parse as provider/model are dropped when saved, and said so. |
How this compares#
LiteLLM and Portkey are gateways in front of model providers: keys, routing, fallbacks and spend for raw models. This endpoint is a gateway in front of your agents as well as your models, and it inherits the platform's governance instead of carrying its own copy. If you already run one of those gateways, keep it as the outbound route and put this endpoint in front of the agents.
Troubleshooting#
| Symptom | Cause and fix |
|---|---|
| 401 invalid_api_key | Missing, malformed, revoked or expired key. Mint a new one under Integrations → LLM Gateway. |
| 404 model_not_found | model is neither agent:<name or id> nor <provider>/<model>, the agent is inactive, or two agents share the name — use agent:<id>. |
| 403 insufficient_scope | The key lacks the scope, or the agent is not on its allow-list. |
| 403 model_not_allowed | Your IAM model rules forbid the model, or the key's model patterns do. |
| 429 insufficient_quota | The owner's or the key's monthly budget is spent. |
| The reply came from a different model | A fallback fired; X-Gateway-Model names it and the audit log has gateway.fallback with the reason. |
| The same answer keeps coming back | The key's semantic cache is answering. X-Gateway-Cache: hit says so; clear the cache or turn the switch off on the key. |
| X-Gateway-Cache is always skip | The cache is on but nothing qualifies: the calls are follow-ups with a history, or the temperature is above the ceiling. |
| X-Gateway-Cache is always miss | Nothing similar enough is stored yet, the answers used tools, or no embedding provider is connected for the owner. |
| 404 model_not_found on /metrics/query | The name is not among GET /metrics: not a model the owner owns or is granted. A model the key's allow-list excludes answers 403 model_not_allowed. |
| 400 on /metrics/query names a field | The request or the compiler refused it: an unknown metric or dimension, a grain on a non-time dimension, a missing parameter. GET /metrics shows the vocabulary. |