Gateway API
The gateway speaks both the OpenAI API and the Anthropic Messages API, and routes either one to any provider you have configured. Point an OpenAI or Anthropic SDK at your gateway URL and it works out of the box.
https://gateway.assistant-billing.dev/v1
To use a custom domain (e.g.,
ai.yourapp.com), see Custom Domains.
All requests require a gateway API key (sk_ab_* or temp_ab_*) via the Authorization: Bearer header.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v1/chat/completions | Chat completions (streaming supported) |
POST | /v1/completions | Legacy completions (streaming supported) |
POST | /v1/embeddings | Text embeddings |
POST | /v1/responses | Responses API (streaming supported) |
GET | /v1/responses/{id} | Retrieve a response; add ?stream=true to resume a background run |
POST | /v1/responses/{id}/cancel | Cancel a background response |
DELETE | /v1/responses/{id} | Delete a stored response |
GET | /v1/responses/{id}/input_items | List the input items of a response |
POST | /v1/messages | Anthropic Messages API (streaming supported) |
POST | /v1/messages/count_tokens | Anthropic token counting |
POST | /v1/messages/batches | Submit an Anthropic message batch |
GET | /v1/messages/batches/{id} | Batch status; /results streams the output |
POST | /v1/messages/batches/{id}/cancel | Cancel an Anthropic message batch |
POST | /v1/files | Upload a file (multipart) |
GET | /v1/files | List files; /{id} retrieves one, /{id}/content downloads it |
DELETE | /v1/files/{id} | Delete a file |
POST | /v1/batches | Submit a batch at the discounted tier |
GET | /v1/batches | List batches; /{id} retrieves one |
POST | /v1/batches/{id}/cancel | Cancel a batch |
POST | /v1/images/generations | Image generation (default: dall-e-3) |
POST | /v1/images/edits | Image editing (default: dall-e-2) |
POST | /v1/images/variations | Image variations (default: dall-e-2) |
POST | /v1/audio/speech | Text-to-speech (default: tts-1) |
POST | /v1/audio/transcriptions | Speech-to-text (default: whisper-1) |
POST | /v1/audio/translations | Audio translation (default: whisper-1) |
POST | /v1/moderations | Content moderation (default: omni-moderation-latest) |
POST | /v1/feedback | Submit feedback/scores for a request |
GET | /v1/models | List available models |
GET | /v1/realtime | WebSocket upgrade for OpenAI Realtime API (see Realtime) |
POST | /v1/realtime/calls | Metered WebRTC call with an SDP offer (see Realtime) |
POST | /v1/realtime/sessions | Ephemeral client secret for WebRTC |
Chat Completions
POST /v1/chat/completions
OpenAI-compatible chat completions with routing, billing, caching, and fallback.
Request Body
All standard OpenAI fields are supported (model, messages, temperature, tools, stream, etc.) plus gateway-specific fields:
| Field | Type | Description |
|---|---|---|
model | string | Required. Model ID (e.g. gpt-4o, claude-sonnet-4-20250514) |
messages | array | Required. Conversation messages |
stream | boolean | Enable streaming. Default: false |
provider | object | Provider routing preferences (see Provider Preferences) |
models | string[] | Fallback model list. Requires route: "fallback" |
route | string | Set to "fallback" to enable model fallback |
transforms | string[] | Request transforms (e.g. ["middle-out"]). Only supported on /v1/chat/completions |
import { GatewayClient } from "@assistant-billing/sdk"
const gateway = new GatewayClient({
apiKey: "sk_ab_live_xyz789",
baseURL: "https://gateway.assistant-billing.dev",
})
const response = await gateway.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Explain quantum computing in one paragraph." },
],
temperature: 0.7,
max_tokens: 500,
})
console.log(response.choices[0].message.content)Streaming
When stream: true, the gateway:
- Injects
stream_options: { include_usage: true }to capture token usage - Streams SSE chunks to the client in real-time
- Extracts usage from the final chunk for billing
- Calculates cost and deducts after stream completes
- Returns metadata in response headers (sent before the first chunk)
const stream = await gateway.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Write a haiku about TypeScript." }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) process.stdout.write(content)
}Provider Preferences
Control routing per-request via the provider field in the request body:
{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}],
"provider": {
"order": ["openai", "azure"],
"only": ["openai", "azure"],
"ignore": ["groq"],
"allow_fallbacks": true
}
}| Field | Type | Description |
|---|---|---|
order | string[] | Try providers in this order |
only | string[] | Only use these providers |
ignore | string[] | Never use these providers |
allow_fallbacks | boolean | Allow fallback chain (default: true) |
Model Fallback
Try multiple models in order until one resolves:
const response = await gateway.chat.completions.create({
model: "gpt-4o",
models: ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-20250514"],
route: "fallback",
messages: [{ role: "user", content: "Hello!" }],
})The gateway tries each model's route in order. The first successful resolution is used. This is client-side model selection -- different from provider fallback chains configured in the dashboard, which retry a single model across multiple providers.
Transforms
Pre-process the request body before sending to the provider.
middle-out -- Compresses long conversations by keeping the first 2 and last 6 non-system messages (plus all system messages). Only applies when the total message count exceeds 10 and there are more than 8 non-system messages.
{
"model": "gpt-4o",
"messages": [...],
"transforms": ["middle-out"]
}Messages
POST /v1/messages
The Anthropic Messages API, with the same routing, billing, caching, and fallback as Chat Completions. Point the Anthropic SDK at your gateway URL and it works unchanged.
This endpoint forwards the Anthropic request shape to the provider's own messages endpoint. It does not convert between provider protocols, so the model you name has to be served by a provider that speaks Messages (Anthropic, Bedrock, Vertex). To reach an OpenAI model, use /v1/chat/completions or /v1/responses instead.
Request Body
All standard Anthropic fields are supported (model, messages, system, max_tokens, tools, thinking, stream, and so on), plus the same gateway fields Chat Completions accepts:
| Field | Type | Description |
|---|---|---|
model | string | Required. Model ID (e.g. claude-opus-5, anthropic/claude-sonnet-5) |
messages | array | Required. Conversation messages |
max_tokens | number | Required by Anthropic. Passed through unchanged; the gateway does not rewrite it into max_completion_tokens the way it does for OpenAI endpoints |
effort | string | low, medium, high, xhigh, or max. See Reasoning effort |
stream | boolean | Enable streaming. Default: false |
provider | object | Provider routing preferences (see Provider Preferences) |
models | string[] | Fallback model list. Requires route: "fallback" |
curl https://gateway.assistant-billing.dev/v1/messages \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"system": "You are a concise assistant.",
"messages": [{ "role": "user", "content": "Explain prompt caching in one paragraph." }]
}'Reasoning effort
effort and OpenAI's reasoning_effort are the same control spelled two ways, and the gateway translates whichever one you send into the form the routed provider accepts. Two rules are applied on the way through:
reasoning_effort: "none"has no Anthropic equivalent, so it becomeseffort: "low"withthinking: { "type": "disabled" }.- Claude Opus 5 rejects
thinking: { "type": "disabled" }abovehigh. If you disable thinking explicitly and also ask forxhighormax, the effort is clamped tohighrather than letting the request fail.
Prompt caching
cache_control blocks pass through untouched, and the response's cache_read_input_tokens and cache_creation_input_tokens are billed at Anthropic's published multipliers: 0.1x for reads, 1.25x for 5-minute writes, and 2x for 1-hour writes. When usage.cache_creation splits writes by TTL, each bucket is priced at its own rate.
Requests sharing a cacheable prefix are routed to the same upstream account where the project has several, so a warm cache survives across turns. X-AB-Cache reports whether the gateway's own response cache served the request.
Streaming
Set stream: true for standard Anthropic SSE. Usage arrives split across message_start and message_delta; the gateway merges both before billing, and reads output_tokens_details.thinking_tokens from the final message_delta when present.
A response that ends in stop_reason: "refusal" is not billed, matching Anthropic's own behaviour for a request declined before any output.
Token counting
POST /v1/messages/count_tokens
Counts tokens for a prompt without running it. Takes the same body as /v1/messages. Useful now that Claude Fable 5 and Opus 4.7 and later use a tokenizer that produces roughly 30% more tokens for the same text than earlier models, which makes local estimates unreliable.
curl https://gateway.assistant-billing.dev/v1/messages/count_tokens \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{ "role": "user", "content": "How many tokens is this?" }]
}'Batches
POST /v1/messages/batches submits an Anthropic message batch at the discounted tier; GET /v1/messages/batches/{id} reports status and /results streams the output. POST /v1/messages/batches/{id}/cancel stops one.
A batch belongs to the account that created it, so the gateway records that account and routes every later call back to it. Server tool calls inside a batch are priced the same as they are outside one.
Requests Without Provider Keys
A model whose provider you have not configured is still served, through an aggregator account that pays the provider on your behalf. X-AB-Route-Type: virtual marks these requests, and X-AB-Carrier names what delivered them.
| Carrier | Endpoints | Models |
|---|---|---|
cloudflare | /v1/chat/completions | Anthropic, Google AI Studio, Groq, OpenAI, and xAI models |
vercel | /v1/chat/completions, /v1/responses, /v1/messages, /v1/messages/count_tokens, /v1/embeddings, /v1/images/generations, /v1/audio/speech, /v1/audio/transcriptions | Catalog models that Vercel AI Gateway lists; platform audio requires a character rate for speech or a minute rate for transcription |
openrouter | /v1/chat/completions, /v1/completions, /v1/audio/speech, /v1/audio/transcriptions | Catalog models listed by OpenRouter, using your project's OpenRouter account |
Requests carried by the platform's accounts are charged to your wallet at the catalog rate. Connect your own Vercel AI Gateway or OpenRouter account under Providers and keyless requests use it instead, with nothing charged to the wallet for them. A provider key of your own always takes precedence over either.
Where both platform carriers can serve a request, Cloudflare is tried first because only its responses are cached, and Vercel takes over when Cloudflare is unavailable. To require Vercel AI Gateway for a single request, spell the model the way Vercel does behind a vercel/ prefix, such as vercel/anthropic/claude-sonnet-4.5.
Caching
The gateway supports response caching via Cloudflare AI Gateway. It applies to every request that passes through Cloudflare AI Gateway: credentialed routes (using your own provider API keys), which reach the provider through your project's Cloudflare AI Gateway, and virtual routes that Cloudflare carries (X-AB-Carrier: cloudflare). Virtual routes carried by Vercel AI Gateway are not cached.
- Cache hits return
X-AB-Cache: HITheader - Cached responses cost $0 -- no balance deduction
- Saved costs tracked in
X-AB-Cost-Usd: 0and logged assavedCostUsd - Cache behavior is managed by Cloudflare AI Gateway -- configure it via your Cloudflare AI Gateway dashboard
- Cache is keyed on the full request body (model + messages + params)
Providers also bill a prompt prefix they have already processed at a discount. For OpenAI models the gateway sends one prompt_cache_key per project and workspace (per API key for a key without a workspace) with chat completions and responses requests, so every request of a workspace can reuse a prefix another one wrote; a prompt_cache_key in your request is sent as it is. Cache reads and writes are billed at the provider's rates, and the dashboard reports the share of input read from the provider's cache and what it saved.
Embeddings
POST /v1/embeddings
const result = await gateway.embeddings.create({
model: "text-embedding-3-small",
input: "The quick brown fox jumps over the lazy dog",
dimensions: 256,
})
console.log(result.data[0].embedding) // [0.0023, -0.0091, ...]Does not support streaming or model fallback.
Images
POST /v1/images/generations
curl https://gateway.assistant-billing.dev/v1/images/generations \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-H "Content-Type: application/json" \
-d '{
"model": "dall-e-3",
"prompt": "A sunset over mountains, oil painting style",
"size": "1024x1024",
"n": 1
}'POST /v1/images/edits and POST /v1/images/variations default to dall-e-2 and use multipart form data.
Audio
| Endpoint | Default model | Request | Response |
|---|---|---|---|
POST /v1/audio/speech | tts-1 | JSON with model, input, and voice; optional response_format, speed, instructions, and stream_format | Audio bytes in the requested format, or SSE with stream_format: "sse" on a model that supports it |
POST /v1/audio/transcriptions | whisper-1 | Multipart file and model; optional language, prompt, temperature, response_format, and stream | Transcript in the requested format, or an event stream where supported |
POST /v1/audio/translations | whisper-1 | Multipart file and model; optional prompt, temperature, and response_format | Translated transcript in the requested format |
Speech defaults to mp3. The OpenAI-compatible response_format selects mp3, opus, aac, flac, wav, or pcm when the selected provider supports it. stream_format: "sse" requests speech events instead of a binary audio response. The gpt-4o-mini-tts family reports token usage only in that SSE stream; a request on the platform's account without SSE is refused because it cannot be billed from the binary response. Vendor adapters for ElevenLabs, Deepgram, and Cartesia do not support SSE speech.
Transcription and translation default to JSON. response_format can request json, verbose_json, text, srt, or vtt; diarized_json is available when the model supplies diarization data. For a nonstreaming request, the gateway asks the upstream for a JSON format that reports usage, then renders the format requested by the client. Set multipart stream=true for streaming transcription with the gpt-4o-transcribe family. Translation does not stream.
| Billing unit | Models | Usage |
|---|---|---|
| Input characters | tts-1, tts-1-hd, and character priced vendor speech models | Characters read from input, or the provider's billed character count when reported |
| Audio minutes | whisper-1, gpt-transcribe, Groq Whisper, and minute priced transcription models | Reported audio duration; Groq charges at least ten seconds per transcription |
| Audio and text tokens | gpt-4o-mini-tts and gpt-4o-transcribe families | Reported input and output tokens, with audio tokens priced at their audio rates |
For a nonstreaming audio response, X-AB-Cost-Usd reports the calculated request cost when a usable rate and usage are available. Streaming usage is recorded after the stream ends, so its cost is not in the initial response headers. Speech without a usable billing rate is refused on the platform's account rather than served without a charge.
| Provider or carrier | Audio coverage | Speech response formats |
|---|---|---|
OpenAI with OPENAI_API_KEY | Speech, transcription, and translation through its OpenAI-compatible API | Formats supported by the selected OpenAI model |
Groq with GROQ_API_KEY | Speech (Orpheus), transcription, and translation through its OpenAI-compatible API | wav |
ElevenLabs with ELEVENLABS_API_KEY | Speech (eleven_flash_v2, eleven_flash_v2_5, eleven_multilingual_v2, eleven_v3) and transcription (scribe_v2) | mp3, wav, pcm |
Deepgram with DEEPGRAM_API_KEY | Speech (aura, aura-2, flux) and transcription (nova-3, nova-3-multi) | mp3, opus, aac, flac, wav, pcm |
Cartesia with CARTESIA_API_KEY | Speech (sonic-3.6) and transcription (ink-whisper) | mp3, wav, pcm |
| Vercel AI Gateway | Speech and transcription for listed models, including Fish Audio s1 and Microsoft mai-voice-2 | Formats supported by the selected Vercel model; no streaming speech |
| Your OpenRouter account | Speech and transcription for models listed by OpenRouter | Formats supported by the selected OpenRouter model |
Use the catalog model ID in model. For ElevenLabs and Cartesia speech, voice is that vendor's voice ID. For Deepgram speech, voice is its voice name, such as thalia-en with aura-2, which the gateway combines with the model into Deepgram's own model name. Vercel AI Gateway can serve listed audio models without a direct provider key, either through the platform account when the billing unit is supported or through your project's own Vercel account. A project's OpenRouter account can also serve its listed audio models. Streaming speech and transcription are unavailable through Vercel AI Gateway. Translation is available only through OpenAI and Groq provider keys.
# Text-to-speech
curl https://gateway.assistant-billing.dev/v1/audio/speech \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-H "Content-Type: application/json" \
-d '{"model": "tts-1", "input": "Hello, welcome to Assistant Billing!", "voice": "alloy"}' \
--output speech.mp3
# Transcription (multipart)
curl https://gateway.assistant-billing.dev/v1/audio/transcriptions \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-F file=@audio.mp3 \
-F model=whisper-1Moderations
POST /v1/moderations
Default model: omni-moderation-latest. Model field is optional.
curl https://gateway.assistant-billing.dev/v1/moderations \
-H "Authorization: Bearer sk_ab_live_xyz789" \
-H "Content-Type: application/json" \
-d '{"input": "Some text to check for policy violations"}'Feedback
POST /v1/feedback
Attach scores to a completed request for evaluation tracking. The request_id must reference an existing request log (prefixed with rl_).
// Single numeric score
await gateway.feedback("rl_a1b2c3d4e5f6", 5, { session: "abc" })
// Multiple named scores
await gateway.scores("rl_a1b2c3d4e5f6", {
quality: 5,
relevance: 3,
thumbs_up: true,
})You can pass either score (single numeric value with optional name) or scores (object with multiple named values). Boolean values are stored as 1/0.
Models
GET /v1/models
Returns the list of models available to the current API key, based on enabled providers and model permissions.
const models = await gateway.models.list()
// [{ id: "gpt-4o", object: "model", owned_by: "openai", context_window: 128000, ... }]The response includes extended fields beyond the OpenAI spec: context_window, max_output, capabilities, and pricing. Capabilities include audio_input and audio_output alongside reasoning, tools, images, and structured output. pricing uses input, output, audio_input, audio_cached_input, and audio_output for USD per million tokens; characters for USD per million input characters; and audio_minute for USD per minute of audio. Each pricing key appears only when that model has a rate for the unit, and pricing is absent when no rate is available.
Stateful resources
Background responses, uploaded files, and batches live on one upstream account. The gateway records which account created each one, so a later retrieve, cancel, or delete reaches it even when the project has several credentials for the same provider. The same mapping backs previous_response_id: a turn continuing a stored response is pinned to the account holding it.
Response Headers
Every proxy response includes metadata headers:
| Header | Description | Example |
|---|---|---|
X-Request-Id | Unique request identifier | rl_a1b2c3d4e5f6 |
X-AB-Model | Actual model used (after routing) | gpt-4o-2024-08-06 |
X-AB-Provider | Provider that served the request | openai |
X-AB-Cost-Usd | Request cost in USD | 0.003450 |
X-AB-Tokens-In | Input tokens | 150 |
X-AB-Tokens-Out | Output tokens | 230 |
X-AB-Latency-Ms | Total latency in milliseconds | 1234 |
X-AB-Cache | Cache status | HIT or MISS |
X-AB-Retry-Attempts | Number of attempts (only present if retried) | 2 |
X-AB-Route-Type | Route resolution method | virtual, credentialed, or passthrough |
X-AB-Carrier | What delivered the request | vendor, cloudflare, vercel, or openrouter |
virtual means the request was served without local provider credentials, by an aggregator account (see Requests Without Provider Keys). credentialed means the request was routed through a provider with your own API key. passthrough means the request was forwarded as-is via pass-through proxy mode.
For streaming responses, headers are sent before the first chunk. X-AB-Cost-Usd, X-AB-Tokens-In, and X-AB-Tokens-Out are only available in non-streaming responses (usage is extracted from the final stream chunk and logged asynchronously).
Vercel AI SDK
The gateway is OpenAI-compatible, so it works as a drop-in with the Vercel AI SDK:
import { createOpenAI } from "@ai-sdk/openai"
import { generateText, streamText } from "ai"
const provider = createOpenAI({
baseURL: "https://gateway.assistant-billing.dev/v1",
apiKey: "sk_ab_live_xyz789",
})
// Non-streaming
const { text } = await generateText({
model: provider("gpt-4o"),
prompt: "Hello!",
})
// Streaming
const result = streamText({
model: provider("gpt-4o"),
prompt: "Hello!",
})
for await (const chunk of result.textStream) {
process.stdout.write(chunk)
}No special adapter needed -- just point baseURL at your gateway.
OpenAI SDK
Any OpenAI-compatible SDK works. Just override the base_url and api_key:
import OpenAI from "openai"
const client = new OpenAI({
baseURL: "https://gateway.assistant-billing.dev/v1",
apiKey: "sk_ab_live_xyz789",
})
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello!" }],
})