AI gateway
Every model through one key — OpenAI Chat Completions, OpenAI Responses and Anthropic Messages — paid from prepaid credits that stop at zero.
One gateway, every major model, one key. Usage is priced in dollars you can read, and paid from a balance you loaded on purpose — so a runaway loop costs what is in the wallet and nothing more.
Turn it on
In the console, open Integrations and enable the AI gateway. That is the whole setup — there is no key to create or paste.
Every Camplax app already has CAMPLAX_PROJECT_TOKEN in its environment, and camplax dev sets it on your laptop. The Camplax AI clients read it on their own, so the code below runs as written. Only calls from outside Camplax — a script on another host, say — need a key from Keys.
Call a model
The gateway speaks the OpenAI shape, so existing code and SDKs work with a new base URL:
curl https://camplax.dev/v1/ai/chat/completions \
-H "Authorization: Bearer $CAMPLAX_PROJECT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4.1-mini",
"messages": [{ "role": "user", "content": "Say hi" }]
}'
Endpoints:
| Method | Path | What it does |
|---|---|---|
POST | /v1/ai/chat/completions | OpenAI Chat Completions; "stream": true returns server-sent events |
POST | /v1/ai/responses | OpenAI Responses — tools, reasoning and streaming in Responses events |
POST | /v1/ai/messages | Anthropic Messages — tools, extended thinking and streaming in Anthropic events (also at /v1/ai/v1/messages) |
POST | /v1/ai/embeddings | Embeddings |
GET | /v1/ai/models | Models this project may call |
GET | /v1/ai/providers | Providers behind those models |
Model names are provider/model — swap openai/gpt-4.1-mini for another model with a string change. No new account, no new key, no new invoice.
Use the OpenAI or Anthropic SDK you already have
All three formats run through the same gateway: the same key, allowed models, fallbacks, any limits you set, data policy, request log and credits. Pick the one your code already speaks and change only the base URL:
import OpenAI from "openai";
const openai = new OpenAI({
baseURL: "https://camplax.dev/v1/ai",
apiKey: process.env.CAMPLAX_PROJECT_TOKEN,
});
const res = await openai.responses.create({
model: "openai/gpt-5-mini",
input: "Say hi",
reasoning: { effort: "low" },
});
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
baseURL: "https://camplax.dev/v1/ai", // the SDK adds /v1/messages
apiKey: process.env.CAMPLAX_PROJECT_TOKEN,
});
const msg = await anthropic.messages.create({
model: "anthropic/claude-sonnet-4.5",
max_tokens: 1024,
thinking: { type: "enabled", budget_tokens: 512 },
messages: [{ role: "user", content: "Say hi" }],
});
What to know:
- Model ids are the gateway's,
provider/model—anthropic/claude-sonnet-4.5, not Anthropic's dated ids. Any model the project allows works on any of the three formats. If the project's policy denies a model or limits providers, an id without a provider prefix is refused, because the policy cannot tell whose it is. - Extended thinking fits the token cap. When a max-tokens cap you set lowers
max_tokensbelow a Messagesthinking.budget_tokens, the budget is lowered to fit; if the cap leaves no room for thinking at all, the request fails withmax_tokens_capped. - Streaming keeps each format's own events —
response.output_text.deltaand friends on Responses,message_start/content_block_delta/message_stopon Messages — so the SDKs' stream helpers work unchanged. - Every stream ends with its token usage. On Chat Completions the gateway always turns on
stream_options.include_usage, so the last chunk before[DONE]carriesusageand an emptychoiceslist — code that readschoices[0]on every chunk should skip it. Responses and Messages streams already carry usage in their own events. - The gateway stores nothing. On Responses,
previous_response_idandstore: trueare refused with a400andstoreis sent asfalse; send the whole conversation ininput. - The key goes in
Authorization: Beareror, as the Anthropic SDK sends it,x-api-key. Errors on/messagescome back in Anthropic's{ "type": "error", "error": { … } }shape, so the SDK raises its own typed errors. /v1/messages/count_tokensis not served.
The SDK — @camplax/ai
For JavaScript and TypeScript apps, @camplax/ai wraps the gateway in two layers.
A Vercel AI SDK provider, so streamText, generateText, Agent, tool calls, approvals, embeddings and useChat all run against the gateway:
import { camplax } from "@camplax/ai";
import { streamText } from "ai";
const result = streamText({
model: camplax("openai/gpt-4.1-mini"),
messages,
});
First-class support for Eve (by Vercel) — Eve's agent/agent.ts accepts any AI SDK LanguageModel, so Eve agents run against Camplax AI Gateway on prepaid credits:
// agent/agent.ts
import { defineAgent } from "eve";
import { camplax } from "@camplax/ai";
export default defineAgent({
model: camplax("openai/gpt-4.1-mini"),
});
Scaffold an Eve agent powered by @camplax/ai at any time with camplax add agent <name> --eve.
A zero-dependency client for places the AI SDK does not reach — an edge handler, a script, a non-TS service:
import { createCamplaxClient } from "@camplax/ai";
const ai = createCamplaxClient(); // reads CAMPLAX_PROJECT_TOKEN
for await (const ev of ai.stream({ model: "openai/gpt-4.1-mini", messages })) {
if (ev.type === "text") process.stdout.write(ev.text);
if (ev.type === "done") console.log(ev.usage); // cost in dollars
}
Both read CAMPLAX_PROJECT_TOKEN (in every Camplax environment, and under camplax dev) and default to the gateway — and both take a baseURL/endpoint plus apiKey for any other OpenAI-compatible provider, so the same code shape follows the model wherever it lives. In a Plax app you do not need either — plax.ai.chat and agent() already stream through the gateway.
Search the web
Add the web_search tool to a request and the model searches when it needs to, then answers with links to its sources:
const result = await plax.ai.chat({
model: "openai/gpt-4.1-mini",
messages: [{ role: "user", content: "What changed in the news today?" }],
tools: [{ type: "web_search", parameters: { max_results: 5 } }],
});
Sources come back as url_citation annotations on the message. Useful parameters: max_results (1–25, default 5), search_context_size (low, medium, high), allowed_domains and excluded_domains. Every model searches the same way, through the same search engine, so results and prices do not change when you switch models.
What a search costs. $0.001–0.005 a search for up to 10 results, and $0.001 for each result past 10. That is charged on top of the tokens the model reads and writes, with no markup. The results are read as prompt tokens, so a search on a small model usually comes to about a cent in all. The request log shows each call's full cost.
How many searches. The model may search more than once in a call. The gateway allows 3 tool calls per request unless you send max_tool_calls (up to 30), and before the call it holds enough credit for every one of them, with each search's results re-read on each later step. That hold is often much more than the call costs, and the rest goes back when it ends. If the wallet cannot cover it, the call gets a 402: send a lower max_tool_calls or max_results, or top up.
Other tools the gateway runs for you: web_fetch reads one page ($0.001 a page; max_content_tokens limits how much of it the model reads), and datetime tells the model the current date and time, free. Code written for a provider's own search works too — OpenAI's web_search and web_search_options, Anthropic's web_search_20250305 and web_fetch_20250910, a :online model id and the web plugin all run on the same search, at the same price.
Hosted tools whose cost the gateway cannot hold before the call are refused with a 400 tool_not_supported and cost nothing: image generation, hosted shells and code runners, multi-model fusion, sub-agents and remote MCP servers. Function tools that your own code runs are always fine. Plugins work the same way: web, file-parser, response-healing and context-compression are accepted, any other gets plugin_not_supported. A PDF that may go through OCR (the file parser with no engine, or mistral-ocr) holds up to $2 for it, the most OCR can charge for one document; set pdf.engine to pdf-text or native to skip that.
Limits
The gateway has none of its own. No requests-a-minute limit, no cap on tokens, no monthly spend cap: if your account has credits, your app can spend them as fast as it likes. The one stop is your balance — every call holds the most it could cost first, so it can never spend credits you do not have.
If you want limits, set them per project under Settings in AI Gateway. Each is off until you set it, and clearing the field turns it off again:
- Allowed models — the list a request may use; anything else is rejected.
- Fallback models and retries — if a provider is down or throttling, the gateway retries then walks the fallback list, and tells you it did with the
X-Camplax-Fallback-Indexresponse header. A request's ownmodels: [...]routing list is checked against the allowed models too, entry by entry. - A max-tokens cap — applied server-side to every output-limit field (
max_tokens,max_completion_tokens,max_output_tokens) on every format, so a client cannot ask for more. Without one, a call gets the model's own limit. - Requests a minute — per key, for the whole project, or both; past it you get
429withRetry-After. - A monthly spend cap — production and previews each take their own; past it calls get
402spend_cap_reacheduntil the 1st, rather than overrun.
A call waits up to 10 minutes for the model, so slow reasoning models finish; set a shorter Timeout if you would rather fail fast.
Every request is logged with its model, latency and cost in dollars, so the console shows what a request cost in money rather than tokens-per-thousand.
When a request is refused
A request the project's policy turns away never reaches a model and costs nothing. It comes back as a 400 with one of these codes:
| Code | When |
|---|---|
model_not_allowed | The model is not on Allowed models, or the id has no provider prefix while the policy checks providers. |
model_not_available | The model is free (a :free id, or one priced at zero), or the id picks a model for you instead of naming one. The gateway serves paid models only — name one, like openai/gpt-4.1-mini. |
model_denied | The model (or a :variant of it) is on the policy's deny list. |
provider_not_allowed / provider_denied | The model's provider is outside the allowed providers, or on the denied list. |
invalid_models | The request's own models routing list is not an array of model id strings. |
tool_not_supported | The request carries a hosted tool whose cost cannot be held before the call (image generation, a hosted shell or code runner, fusion, sub-agents, remote MCP). The error names the tool. web_search, web_fetch, datetime and function tools work. |
plugin_not_supported | A plugin other than web, file-parser, response-healing or context-compression. |
invalid_tools / invalid_max_tool_calls | tools is not an array, or max_tool_calls is not a whole number of 1 or more. |
max_tokens_capped | On Messages with extended thinking, the project's max-tokens cap leaves max_tokens too small for any thinking budget. Raise the cap or turn thinking off. |
Each entry of a models routing list is checked the same way as the main model, and the error names the entry that failed.
Money and project state refuse upstream of any model too — with their own statuses, not a 400:
| Status | Code | When |
|---|---|---|
402 | credits_exhausted | The account's prepaid credits cannot cover the most this call could cost — they are empty, or the call asks for more output than what is left pays for. Top up on Billing, or send a smaller max_tokens, and the same call works. |
402 | spend_cap_reached | The project reached the monthly spend cap its owner set. It resets on the 1st; an owner can raise or remove it under AI Gateway settings. |
402 | preview_spend_cap | A preview deploy or camplax dev used up the monthly preview cap the owner set. It resets on the 1st; an owner can raise or remove it under AI Gateway settings. |
403 | project_suspended | The project is suspended — every spend path, AI included, is refused while it is. |
503 | billing_unavailable | The wallet balance could not be read; the call was refused rather than allowed to run unbilled. Retry shortly. |
Reading the logs
Open a request row and you get the full picture: the model you asked for, the model that actually served (servedModel — it can differ when a fallback fired or the provider substituted), the provider, the key that called, and the token counts — prompt, completion, cached and reasoning tokens separately.
Timing is split so you can see where the wait went: total latencyMs, and for streamed requests ttfbMs (time to the first token) against generationMs (the rest of the stream). Cost is costEstimateUsd — real dollars per request, summed in the overview.
By default a log row is metadata only. Turn on Record prompts and replies in Policy (logBodies) to keep the prompt, the reply and the model's reasoning on each row — off by default, because those words are your users' data. Log rows are kept for the project's retention window and then purged by the retention sweep.
When the money runs out
AI draws from the account's prepaid credits — one balance for all your projects, bought in any amount from $5 on Billing. A call costs exactly what the model's provider charges for it, with no markup; Camplax's only fee is 10% + 30¢ on top of a purchase when you buy it. Before a paid call goes out, the gateway holds the most it could cost (its prompt plus its output limit at the model's price); when it ends, the hold becomes what the provider reported and the rest goes back. A call whose cost is unknown — it timed out, or a stream was cut off before the provider said what it used — keeps the whole hold, because the provider may have finished it anyway. A call with no output limit is held at the most the model can write (its published output limit); a model not yet in the catalog is capped at 16,384 output tokens. Images count as what vision models bill for them, not their bytes, and a call that searches the web also holds every search it may make (see Search the web). So $5 of credits cannot start calls worth more than $5. At zero, AI calls fail with a 402 credits_exhausted error your app can catch. Everything else stays online. Nothing auto-charges, and nothing is billed after zero.
In a Plax app
The starter's server helper reads CAMPLAX_PROJECT_TOKEN and calls the same endpoint, keeping the key off the client:
const result = await plax.ai.chat({
messages: [{ role: "user", content: "Summarise this note" }],
});
Call it only from a server route — spend uses your credits, and a browser key would hand them to anyone.