AI Model Gateway¶
Editions: OSS, Cloud, Enterprise. Unless stated otherwise, everything on this page ships in OSS.
The Preloop Model Gateway centralizes model traffic from managed runtimes. Instead of handing provider API keys to every local agent container, Preloop keeps credentials in its secret store and issues short-lived gateway tokens to enrolled runtimes.
Why Use the Gateway¶
| Direct provider access | Through Preloop Gateway |
|---|---|
| Raw API keys on every machine | Short-lived runtime tokens |
| Spend scattered across providers | Canonical ApiUsage ledger with attribution |
| No per-agent model limits | Subject-scoped allowed-model lists |
| Hard to audit prompts/responses | Normalized gateway events and session drill-down |
The gateway is the companion to the MCP Safety Layer: tools go through /mcp/v1, model calls go through /openai/v1, /anthropic/v1, or /gemini/v1.
Supported Endpoints¶
Preloop exposes provider-compatible ingress on the gateway service:
| Path | Purpose |
|---|---|
GET /openai/v1/models |
Model listing for OpenAI-compatible clients |
POST /openai/v1/chat/completions |
Chat completions with SSE streaming |
POST /openai/v1/responses |
Responses API with SSE streaming |
POST /anthropic/v1/messages |
Anthropic-format clients such as Claude Code and Claude Desktop |
POST /anthropic/v1/messages/count_tokens |
Token counting; forwarded upstream, not recorded or charged |
GET /anthropic/v1/models |
Model listing in Anthropic's shape, filtered by allowed models |
Gemini-compatible routes under /gemini/v1beta |
Google/Gemini clients when configured |
Authentication reuses the same runtime bearer tokens issued during managed agent onboarding. Preloop preserves API-key context and runtime-principal metadata for attribution.
The OpenAI-compatible surface also serves clients you point at the gateway by hand with an account API key. Cursor is the main example: its Override OpenAI Base URL setting routes the AI panel's third-party model calls (Ask/Plan and Agent modes) through /openai/v1, with alias resolution serving Claude and Gemini models over the same path. Tab autocomplete, inline edit, and Cursor-billed bundled models stay on Cursor's backend and cannot be routed through any gateway. See Cursor model routing.
Subscription OAuth Passthrough¶
If you use Claude Code with a Pro/Max subscription instead of an API key, Preloop can still route that traffic through the gateway. The credential type is a Claude Code OAuth token stored on the AI model, and the gateway treats it specially:
- Byte-faithful proxying. Anthropic requires the exact Claude Code request shape (the sentinel system block first,
cache_controlmarkers intact) and rejects anything else. So the gateway forwardssystem,messages, and nestedcache_controlblocks untouched, only the model identifier and streaming flag are set by Preloop, and SSE streams are relayed verbatim. - Still governed and budgeted. Budget preflight, governance tool-stripping, attribution, and usage recording run exactly as on the normal path. Only the message bytes are sacred.
- No context optimization on this traffic. Message-level context optimizations are deliberately not applied: rewriting blocks would break byte-fidelity and destroy the prompt-cache prefix. See Session Optimization.
- The credential does not leak to other agents. The OAuth token lives on one AI model in your account, and gateway calls resolve models per authenticated subject with per-agent attribution: another agent cannot ride a Claude Code subscription credential.
- Validation is surfaced honestly. Onboarding sends a direct gateway route/accounting probe using managed configuration; it does not launch the application. If the provider throttles it or refuses on billing/quota grounds, the agent stays enrolled but shows an unverified badge in the console (and a Reason in
preloop agents list/onboarding summaries) untilpreloop agents validate <agent> --livepasses. A successful probe verifies the direct route and accounting; application behavior remains unverified. - Brief provider overloads are retried. When Anthropic answers an explicit
502,503,504or529that the gateway classifies as transient, the gateway retries the same request (same body, credential and headers) with jittered exponential backoff, honouringRetry-Afterup toMODEL_GATEWAY_UPSTREAM_RETRY_AFTER_CAP_SECONDS. A providerx-should-retry: falseor a terminal classification stops it. Nothing else is retried: not429, auth or revoked-token errors, validation errors, quota or policy denials, and not transport failures or timeouts (Anthropic may already have processed the request). A stream is never reopened once Anthropic has accepted it. The budget isMODEL_GATEWAY_UPSTREAM_RETRY_MAX_ATTEMPTStotal attempts (default 3); set it to1to turn retries off. If every attempt fails, the client gets the last attempt's error, mapped exactly as a single failed attempt would be (a final504is reported as502, like any other unclassified provider 5xx), with that attempt's error message andx-should-retry/ rate-limit headers. The request still records one usage row, withmeta_data.upstream_retriesholding the number of extra attempts. Claude Code runs its own retries on top of this, so one client request can reach Anthropic up to three times by default, and Claude Code's retries multiply that. These retries absorb short overloads; they cannot hide a long Anthropic outage.
Claude Desktop and the Claude apps gateway¶
Claude Desktop and Claude Code can reach Preloop's Anthropic surface in two ways.
Desktop direct. Set Desktop's inferenceProvider to gateway, inferenceGatewayBaseUrl to https://<your-preloop>/anthropic, and inferenceGatewayApiKey to a Preloop API key. Add inferenceCustomHeaders: {"X-Preloop-Client": "claude-desktop"} so usage rows record client: claude_desktop. Desktop's model picker reads GET /anthropic/v1/models. Attribution and budgets follow the API key, as for any other client.
Claude apps gateway upstream. A customer-run Claude apps gateway can use Preloop as its provider: anthropic upstream:
upstreams:
- provider: anthropic
base_url: https://<your-preloop>/anthropic
forward_user_identity: true
auth:
api_key: ${PRELOOP_UPSTREAM_KEY}
headers:
x-preloop-upstream-secret: ${PRELOOP_UPSTREAM_SECRET}
The key must be a trusted upstream key: an API key with the scope model_gateway:trusted_upstream. Only account admins can create one:
curl -X POST https://<your-preloop>/api/v1/auth/api-keys \
-H "Authorization: Bearer <admin token>" -H "Content-Type: application/json" \
-d '{"name": "claude apps gateway",
"scopes": ["model_gateway:trusted_upstream"],
"trusted_upstream_secret": "<at least 16 random characters>",
"per_subject_budget": {"period": "monthly", "hard_limit_usd": 50}}'
On a trusted upstream key Preloop reads the identity headers the apps gateway adds with forward_user_identity: true:
| Header | Meaning |
|---|---|
x-claude-gateway-user-id |
IdP sub; required to identify the developer |
x-claude-gateway-user-email |
Developer email, when the IdP supplies one |
x-litellm-end-user-id |
Email fallback |
- Each developer becomes a gateway subject, keyed on the IdP
suband scoped to the key. When the email matches an existing member of the key's account, the subject is linked to that member, so the member'suserbudgets apply. Preloop never creates users, logins or permissions from these headers. - On any other credential the identity headers are ignored, because any client can forge them.
- The secret is optional but recommended, because the key sits in the gateway's config file. When
trusted_upstream_secretis set, a request without the matchingx-preloop-upstream-secretheader gets401. Preloop stores only a sha256 hash. - Budgets: a
gateway_subjectbudget policy limits one developer;per_subject_budgeton the key is the default for every developer without their own policy; the key'sapi_keybudget still caps the gateway as a whole. Subject-scopedallowed_models(governance subject typegateway_subjects, keyed by the subject id) apply to messages,count_tokensandGET /anthropic/v1/models. There is no REST writer forgateway_subjectsyet; until there is, per-developer model limits are set in the account's subject governance store, and the key's ownallowed_modelscovers every developer behind it. - Usage rows carry
meta_data.gateway_source(claude_apps_gatewayordirect),meta_data.client(claude_desktop,claude_codeorunknown),gateway_subject_idandgateway_subject_email. Audit entries name the subject. - Sessions keep Claude Code's
x-claude-code-session-id. A request without it is grouped asgw:<gateway_subject_id>:<UTC date>.
Why denials are 429 on this path. The apps gateway fails over to its next upstream on 403, which would let a developer bypass a Preloop budget. It relays a 429 on an email-carrying request to the developer as-is. So on a trusted upstream key with identity headers, budget denials return:
HTTP/1.1 429 Too Many Requests
retry-after: 86400
x-should-retry: false
{"type":"error","error":{"type":"billing_error","message":"Preloop budget exceeded for dev@example.com: ..."}}
retry-after is whole seconds until the budget period resets. Rate limits return 429 with "type": "rate_limit_error". The account kill switch, allowed_models denials, model authorization denials (model_not_authorized) and content policy denials (content_policy_denied, including require_approval holds that were not approved) return 429 with "type": "permission_error" and x-should-retry: false on this path too, so failover cannot route around them. Requests without a trusted upstream identity get 429 for budget denials too, and keep 403 for policy denials (see Status codes). POST /anthropic/v1/messages/count_tokens is not budgeted, but it honours the kill switch: a halted account forwards nothing upstream. A developer whose IdP sends no email gets the same 429, but the apps gateway treats it as a capacity error and fails over; that cannot be fixed on Preloop's side.
Passthrough. On the byte-faithful subscription OAuth path, anthropic-version, anthropic-beta (any value) and every other anthropic-* request header are forwarded verbatim, as are unknown body fields and the system array. Models with a provider API key still go through the gateway's translation layer. Streams stay text/event-stream. Upstream error bodies and the anthropic-ratelimit-unified-* and x-should-retry response headers are relayed to the client.
What Gets Recorded¶
Every gateway request can persist:
- token usage and estimated cost in
ApiUsage - provider, model alias, managed agent, runtime session, flow, and API key attribution
- normalized
model_gateway_callevents with redaction-aware payload capture (see Redaction for what is stored and how to store less) - provider-neutral conversation previews for operator review
The console Cost, Audit > Sessions and Models views read from this ledger.
A rejected API key marks its model¶
When a provider answers 401 for a model that uses your own API key, the gateway records the provider, its sanitized sentence and the first and latest rejection times on the stored credential. The model reads credentials_status: error with credentials_last_error_code: upstream_auth_rejected. An upstream billing 402 records upstream_billing_rejected instead. The API, CLI and Models view expose the same status and detail.
The first rejection in an episode creates an audit event (ai_model.credential_rejected), queues an owner email (subject to each owner's email preference) and emits the account webhook event of the same name to subscribed endpoints. Repeated rejections send no further notice until acceptance or key replacement clears the mark. The notice names the model, provider, sanitized provider sentence and first rejection time, and explains that the provider rejected the model's key, not the owner's Preloop login. For authentication, replace or rotate the API key for this model; for billing, add credits or replace the key. Email delivery is best effort and deferred off the gateway request; delivery failure does not re-arm the episode.
A flow selecting a model marked with an authentication rejection tests that stored key before creating an execution, clone lease or container. OpenRouter uses its free authenticated key endpoint; other providers receive a one-token completion, with no retries and a five-second timeout. Probe calls are metered and audited through the gateway. A successful probe clears the mark and starts the run. A fresh authentication rejection refuses the trigger with HTTP 409, code model_credentials_rejected, and the model name and sanitized provider sentence. Network errors, timeouts, 429 and 5xx do not refuse the run. Healthy models receive no extra probe, query or write on their gateway success path. Billing marks never refuse runs because credits can be added at the provider.
A mark remains visible until an accepted request or key replacement/rotation clears it. Subscription OAuth logins keep their existing refresh-failure tracking; 403 answers, system models and deployment-provided hosted keys are not tracked. No key material is included in notices, audit entries or webhook payloads.
Every flow response (GET /api/v1/flows, GET /api/v1/flows/summary, GET /api/v1/flows/{id}) carries model_health for the flow's own model: {"status": "error", "code": "upstream_auth_rejected", "model_id": ...} while that model's credential is marked, {"status": "missing", "code": "model_deleted", ...} after a forced delete removed it, ok otherwise, and null for a flow with no model. One list call answers "which of my flows will fail". The Flows list shows a quiet Model key failing or Model deleted chip on those rows.
What uses a model¶
GET /api/v1/ai-models/{model_id}/used-by lists what is configured to use a model, independent of traffic:
agents: managed agents bound to the model.flows: flows that select it as their own model, in a routing or label rule, or as the retry model, enabled or disabled, withis_enabled,last_execution_atandlast_execution_status.ci_identities: restricted CI identities bound to those flows, withis_active,active_key_countand the soonestkey_expires_at. No key material.shared_credential_models: other models of the account that use the same stored credential, with theircredentials_status.enabled_flow_countanddisabled_flow_count: how many listed flows can run, and how many are disabled. The console header reads "2 enabled flows · 1 disabled".
Only resources the caller can see are listed; restricted CI keys cannot call it. The model page in the console shows the same list.
For account admins and the model's editors, the single-model read (GET /api/v1/ai-models/{model_id}) adds credentials_expires_at: when a stored subscription login (Claude Code, Codex) expires. It is null for API keys and for other readers. credentials_last_verified_at is when Preloop last wrote the credential, on import, CLI push or server-side refresh. The model page shows both next to Upstream credentials. No token material is returned.
Rotating a credential (POST /api/v1/ai-models/{model_id}/credentials/rotate) returns affected_models (every model sharing the secret) and affected_flows (the flows that select any of them).
Before you retire a model or key¶
- Read
GET /api/v1/ai-models/{model_id}/used-by. Repoint every listed flow, and every CI identity's flow, at the replacement model. - If the key is shared (
shared_credential_modelsis not empty), rotate it rather than removing it, and checkaffected_flowsin the rotation response. - Delete the model. While any enabled flow of the account still selects it,
DELETE /api/v1/ai-models/{model_id}answers409with{"detail": {"code": "model_in_use", "flow_count", "disabled_flow_count", "flows"}}and changes nothing. Disabled flows cannot fail now, so they do not block the delete, but they still lose the model.?force=truedeletes anyway. Either way, the flows that had it as their own model are left without one, reportmodel_health.status: missing, and the audit entry for the delete lists every one of them, enabled or disabled, underorphaned_flows. Routing, label and retry rules that named the model are listed in the audit entry too but are not rewritten.
Which models an agent sees¶
A managed agent's credential is offered exactly the models that belong to it, on GET /openai/v1/models, GET /anthropic/v1/models and in the enumeration of a model_not_authorized error:
- Explicit allowlist first. When the agent (or its API key, flow or gateway subject) has
allowed_modelsset — the checked models under Models & spend on the agent page — that list is what the agent sees and may call. - Otherwise its configured models. With no allowlist, an agent that has configured models — the models
preloop agents onboardbound for it plus any sibling the gateway registered when the agent picked a model in its own picker — sees those and nothing else. Account-wide BYOK and Preloop-hosted models are not offered to it just because they exist in the account. - Agents with no configured models (custom runtimes enrolled through a runtime session token, MCP-only enrollments), plain API keys and console user tokens keep seeing every BYOK, API-key and hosted model in the account.
Subscription OAuth models (Claude Code, Codex) are additionally bound to one managed agent and never appear for any other credential, whatever the allowlist says. A request for a model that exists in the account but is outside the agent's set returns 400 model_not_authorized; the message says whether the model belongs to another agent's subscription or is simply not configured for this agent, and lists the models the credential can use.
Budget and Allowed Models¶
Before dispatching upstream, Preloop evaluates:
- Account-level gateway budgets
- Flow-level budgets when the call originates from a flow execution
- Subject-scoped allowed-model lists on the active API key or managed agent
Soft limits can annotate responses; hard limits deny the call before it reaches the provider.
OSS includes account, flow, API-key and managed-agent budgets with soft and hard limits, plus spend summaries and budget-health alerts.
Cloud and Enterprise
Per-user and per-team budgets, soft and hard limit notifications, and negotiated price overrides come from the RBAC and billing plugins. See Enterprise Billing & FinOps.
Status codes¶
Every gateway router (/openai/v1, /anthropic/v1, /gemini/v1beta) uses one status per condition, with the body in the router's own provider format.
| Condition | Status | Anthropic error.type |
OpenAI error.type / error.code |
Gemini error.status |
Headers |
|---|---|---|---|---|---|
| Budget hard limit (account, flow, user, team, API key, gateway subject, per-model, per-execution ceiling, trial or free hosted-model allowance, pricing required for enforcement) | 429 |
billing_error |
insufficient_quota / insufficient_quota |
RESOURCE_EXHAUSTED |
retry-after: <seconds>, x-should-retry: false |
| Rate limit (Preloop or upstream) | 429 |
rate_limit_error |
rate_limit_error / upstream value |
RESOURCE_EXHAUSTED |
retry-after when known; never x-should-retry: false |
| Policy denial (kill switch, model allowlist, model authorization, content policy) | 403 |
permission_error |
permission_error |
PERMISSION_DENIED |
none |
| Hosted credit exhausted (Cloud and Enterprise hosted billing) | 402 |
unchanged | unchanged | unchanged | unchanged |
Details for a budget denial:
retry-afteris an integer number of seconds until the budget window resets, or3600when the reset is unknown.x-should-retry: falsetells the Anthropic and OpenAI SDKs not to retry. Claude Code also stops retrying whenretry-afteris above 60 seconds.- The OpenAI body keeps the Preloop machine code in
error.preloop_code(budget_limit_exceededorexecution_budget_exceeded). - Anthropic and Gemini messages keep the
Model gateway budget exceeded: ...prefix (the per-execution ceiling saysExecution budget exceeded: ...). - The Gemini body adds
google.rpc.ErrorInfo(domain: preloop.ai) andgoogle.rpc.RetryInfodetails. Gemini CLI treats aretryDelayabove five minutes as a terminal quota error; a shorter one makes it wait for the window to reset before it retries. - Usage and audit rows record status
429, auditerror_typebudget_limit_exceededanderror_classbudget_exceeded. Budget denials do not count toward the rate limit report.
Requests from a trusted upstream (the Claude apps gateway) that carry a developer identity also get 429 for policy denials, because the apps gateway fails over to its next upstream on 403.
Migrating from 403 budget denials
Before this change a budget hard limit returned 403 (permission_error), the same status as a policy denial. Integrations that match 403 to detect an exhausted budget must match 429 with billing_error (Anthropic), insufficient_quota (OpenAI) or RESOURCE_EXHAUSTED (Gemini) instead. To tell a Preloop budget denial from an upstream rate limit, check x-should-retry: false, error.preloop_code on the OpenAI router, or the Model gateway budget exceeded message prefix.
Who produced an error¶
An error from the gateway can come from Preloop itself (for example a missing or rejected Preloop key) or from the upstream model provider (for example the provider rejecting the credential configured for the model). The HTTP status is the same in both cases, so the error body says which one it was:
| Field | Meaning |
|---|---|
origin |
gateway when Preloop decided, upstream when the model provider did |
provider |
The upstream provider configured for the model, for example openrouter |
category |
The upstream error class, the same value as the X-Preloop-Error-Class header |
upstream_status |
The HTTP status the provider reported |
request_id |
The gateway request id recorded on the run's usage row |
cause |
On a 424 repeated_upstream_failure: the first failure of the run (origin, provider, category, upstream_status, message, request_id), so a later error never stands in for the original cause |
The OpenAI and Anthropic routers put these keys inside error, next to message, and in the error / response.failed streaming events. The Gemini router carries them as google.rpc.ErrorInfo metadata (string values, domain: preloop.ai) in error.details.
A provider-reported 401, 402, 403 or 404 names the provider in the message and quotes its own sentence unchanged, for example:
Upstream provider openrouter rejected the provider credential configured for this model (HTTP 401). Provider message: User not found. This is not a rejection of your Preloop credentials.
The provider's wording is passed through as the provider's message; it does not by itself say which account or key the provider evaluated.
Managed Agent Onboarding¶
When you run preloop agents onboard openclaw or preloop agents onboard hermes, the CLI attempts to rewrite local model configuration so traffic targets the Preloop gateway URL instead of direct provider endpoints. MCP configuration is narrowed to the managed Preloop MCP entry at the same time.
If model rewriting is unsupported for a particular runtime build, MCP governance can still succeed while model traffic needs a manual follow-up edit.
Latency¶
Measured hop vs calling the model directly (self-hosted OSS, latency ping, not a soak): Gateway overhead. HTTP to the gateway port is the Preloop process. Public HTTPS adds the reverse proxy and TLS.