A1 API Gateway — Integration Guide

Consumer API v1 · 19 September 2026 · For developers calling the gateway from another project. The machine-readable version is docs/INTEGRATION.md — give that one to your AI coding assistant.
Verification status. Everything on this page for Claude models was exercised by hand against the running gateway with real vendor calls — 60 scenarios, all passing (tests/manual/anthropic-scenarios.md). The OpenAI / xAI path has so far been verified against a mock vendor only; treat it as designed behaviour until a real key has been run through it. The same goes for embeddings, speech and transcription (added 19 September 2026): tested end to end against a mock of each vendor, not yet with real keys.
  1. What the gateway is
  2. Setup in five minutes
  3. Endpoints
  4. Model names and aliases
  5. Which endpoint to use
  6. Features
  7. What comes back in the headers
  8. Errors and what to do about them
  9. Limits, budgets and money
  10. What is not supported
  11. Getting and checking a key
  12. Telling your AI assistant

1. What the gateway is

A1 API Gateway is the company's single door to the AI vendors — Anthropic, OpenAI and xAI for chat, OpenAI for pictures and embeddings, ElevenLabs, AssemblyAI and OpenAI for speech and transcription. Your project never holds a vendor key. It holds a gateway key that starts with sk-gw-, sends requests to the gateway in the vendor's own format, and the gateway does the rest: checks the key and the caller's IP, applies rate limits and spending caps, forwards the request with the real vendor key, measures the exact cost, and returns the vendor's answer unchanged.

your project ──(sk-gw key)──▶ A1 API Gateway ──(real vendor key)──▶ Anthropic / OpenAI / xAI / ElevenLabs / AssemblyAI │ ├─ authenticates the key, checks the caller's IP ├─ enforces rate limits and budgets BEFORE calling the vendor ├─ meters the exact cost of every request └─ logs it for the admin console
Three rules for every project. Never call a vendor API directly. Never put a vendor key in the project — only sk-gw- keys. Read the gateway URL and the key from environment variables, never from source code.

2. Setup in five minutes

2.1 Two environment variables

AI_GATEWAY_URL=https://api.a1apigateway.com        # local development: http://127.0.0.1:8080
AI_GATEWAY_KEY=sk-gw-xxxxxx-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The production address is https://api.a1apigateway.com. Keep it in an environment variable so a local or staging gateway can be swapped in without code changes.

2.2 Change two lines in your SDK setup

Keep using the SDK you already use. Point it at the gateway and hand it the gateway key.

OpenAI SDK · works for every provider, including Claude
// TypeScript                                        # Python
import OpenAI from "openai";                         from openai import OpenAI

const ai = new OpenAI({                              ai = OpenAI(
  apiKey: process.env.AI_GATEWAY_KEY,                    api_key=os.environ["AI_GATEWAY_KEY"],
  baseURL: `${process.env.AI_GATEWAY_URL}/v1`,           base_url=f"{os.environ['AI_GATEWAY_URL']}/v1",
});                                                  )

const res = await ai.chat.completions.create({       res = ai.chat.completions.create(
  model: "anthropic/claude-opus-5",                      model="anthropic/claude-opus-5",
  max_tokens: 1024,                                      max_tokens=1024,
  messages: [{ role: "user", content: "…" }],            messages=[{"role": "user", "content": "…"}],
});                                                  )
The OpenAI SDK adds /chat/completions to the base URL itself, so the base URL must end in /v1.
Anthropic SDK · Claude models, native API
// TypeScript                                        # Python
import Anthropic from "@anthropic-ai/sdk";           import anthropic

const ai = new Anthropic({                           ai = anthropic.Anthropic(
  apiKey: process.env.AI_GATEWAY_KEY,                    api_key=os.environ["AI_GATEWAY_KEY"],
  baseURL: process.env.AI_GATEWAY_URL,                   base_url=os.environ["AI_GATEWAY_URL"],
});                                                  )

const res = await ai.messages.create({               res = ai.messages.create(
  model: "anthropic/claude-opus-5",                      model="anthropic/claude-opus-5",
  max_tokens: 1024,                                      max_tokens=1024,
  messages: [{ role: "user", content: "…" }],            messages=[{"role": "user", "content": "…"}],
});                                                  )
The Anthropic SDK adds /v1/messages itself, so its base URL is the bare gateway URL — no /v1.
curl
curl "$AI_GATEWAY_URL/v1/chat/completions" \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-opus-5","max_tokens":256,"messages":[{"role":"user","content":"ping"}]}'

That is the whole integration. Everything below is detail you will want when something surprises you.

3. Endpoints

MethodPathPurposeFormat
POST/v1/chat/completionsChat with any providerOpenAI Chat Completions
POST/v1/messagesChat with Claude, nativeAnthropic Messages, forwarded untouched
POST/v1/messages/count_tokensCount input tokens before sending (Claude), freeAnthropic count_tokens, forwarded untouched
POST/v1/images/generationsGenerate pictures (OpenAI GPT-Image) — your key must allow imagesOpenAI Images, forwarded untouched
POST/v1/embeddingsTurn text into vectors (OpenAI embedding models)OpenAI Embeddings, forwarded untouched
POST/v1/audio/speechText to speech (ElevenLabs, OpenAI tts-1)OpenAI Speech; the reply is raw audio
POST/v1/audio/transcriptionsSpeech to text (AssemblyAI, ElevenLabs Scribe, OpenAI whisper-1)OpenAI Transcriptions, multipart upload
GET/v1/modelsModels this key may use, each with its capabilitiesOpenAI list + capabilities
GET/v1/keyThis key's limits, budgets and routingGateway JSON

Authentication is either Authorization: Bearer sk-gw-… or x-api-key: sk-gw-… — both SDKs already send one of them.

4. Model names and aliases

Names are provider/model: anthropic/claude-opus-5, openai/gpt-5.6, xai/grok-4.3, openai/text-embedding-3-small, elevenlabs/eleven_flash_v2_5, assemblyai/universal-3-5-pro. The prefix can be left off when the name is unambiguous (claude-…, gpt-…, o…, grok-…, text-embedding-…, tts-…, whisper-…). ElevenLabs and AssemblyAI models always need their prefix: a bare eleven_flash_v2_5 or universal-2 is 404 model_not_found.

Your gateway admin can also define aliases for your organisation — for example settlement-default pointing at whatever model the team has standardised on. Use the alias in production code: when the team moves to a newer model the admin changes the alias and nothing in your project needs a deploy. The x-gw-model response header always tells you which real model answered.

GET /v1/models lists exactly what your key is permitted to use. A name that does not exist gives 404 model_not_found; one that exists but is not allowed for your key gives 403 model_not_allowed.

4.1 Capabilities — check, don't guess

Every model in the list carries a capabilities block saying what you may send to it, so a client can check up front instead of discovering a 400 in production:

{"id":"anthropic/claude-opus-5","object":"model","owned_by":"anthropic",
 "capabilities":{"streaming":true,"images":true,"documents":true,"tools":true,"structured_output":true,
                 "reasoning":true,"prompt_caching":true,"sampling_params":false,"forced_tool_choice":true,
                 "endpoints":["/v1/chat/completions","/v1/messages","/v1/messages/count_tokens"]}}

true and false are asserted by the gateway; null means the gateway does not know and the vendor decides. documents means PDFs and text documents as file content parts (§6.3b); prompt_caching means repeated context is billed at the cached rate (§6.3c); endpoints lists which consumer endpoints accept the model — exactly one for image, embedding, speech and transcription models. Those models report every other flag as false (nothing streams on their endpoints), except sampling_params, which is true for transcription models (temperature). The block is an extra field on the standard OpenAI list entry, so OpenAI-shaped clients simply ignore it.

{"id":"openai/text-embedding-3-small","object":"model","owned_by":"openai",
 "capabilities":{"streaming":false,"images":false,"documents":false,"tools":false,"structured_output":false,
                 "reasoning":false,"prompt_caching":false,"sampling_params":false,"forced_tool_choice":false,
                 "endpoints":["/v1/embeddings"]}}

4.2 One model, one door

Every model in the catalogue does one kind of thing and answers on the endpoint for that thing only:

KindEndpointModels
chatPOST /v1/chat/completions (Claude also on /v1/messages)Claude, GPT, o-series, Grok
imagePOST /v1/images/generationsgpt-image-*
embeddingPOST /v1/embeddingstext-embedding-*
speechPOST /v1/audio/speechelevenlabs/eleven_*, tts-1, tts-1-hd
transcriptionPOST /v1/audio/transcriptionsassemblyai/*, elevenlabs/scribe_v2, whisper-1

A model sent to the wrong endpoint is refused before any vendor call with 400 invalid_request, and the message names the right one:

Model 'text-embedding-3-small' is an embedding model and answers on POST /v1/embeddings, not /v1/chat/completions.
This is a change for image models: until 19 September 2026 an image model at the chat door (or a chat model at the image door) was 404 model_not_found. Code that branched on that 404 should branch on the 400 instead. capabilities.endpoints in GET /v1/models tells you which endpoint each model answers on.

4.3 Prices worth knowing

The catalogue was re-read from the vendors' own price pages on 19 September 2026.

Corrections apply from 19 September 2026; requests already made keep the price they were charged. x-gw-cost-usd is always what you were actually charged.

5. Which endpoint to use

Use /v1/chat/completions. It is the standard door for every project: one format for every provider, PDFs and documents, automatic prompt caching, tools, structured output, reasoning, images, streaming — and every request is validated and priced by the gateway before a vendor is called, so spending caps are exact.

/v1/messages exists for the rare Claude-only feature the OpenAI format cannot express — citations, anthropic-beta features, vendor server tools. It forwards the body untouched, which also means price-changing fields reach the vendor unpriced. Ask the gateway admin before using it.

For Claude models, /v1/chat/completions is translated; the few OpenAI-only parameters that have no equivalent are listed in §10, and every ignored field is reported in x-gw-warnings — nothing is dropped silently.

Pictures, embeddings, speech and transcription each have a door of their own. A model sent to the wrong one is a 400 invalid_request that names the right one (§4.2).

You wantEndpointSection
a picturePOST /v1/images/generations§6.9
vectors for search or clusteringPOST /v1/embeddings§6.10
audio from textPOST /v1/audio/speech§6.11
text from audioPOST /v1/audio/transcriptions§6.12

6. Features

6.1 Conversations

Both endpoints are stateless. Send the whole history on every call — system prompt, earlier user and assistant turns, then the new message. On /v1/chat/completions the system prompt is a message with role: "system"; on /v1/messages it is the top-level system field.

6.2 Streaming

Add "stream": true (or use the SDK's streaming helper). Text arrives as it is generated, exactly as from the vendor. At the end of the stream the gateway adds one extra line before [DONE]:

: x-gw-cost-usd=0.000146 x-gw-billing=byok

It starts with a colon, which makes it an SSE comment: every SDK ignores it, so it never breaks parsing. Read it only if you want the cost of a streamed call.

If your client disconnects part-way through a stream, the gateway keeps reading the vendor's answer (for up to a minute) and bills the tokens the vendor actually generated. Cancelling does not refund a request.

6.3 Images

JPEG, PNG, GIF and WebP, either inline as base64 or as an https:// URL, in the vendor's usual content-part shape (image_url on the OpenAI door, image + source on the Anthropic door).

Image URLs are downloaded by the vendor's servers, not by the gateway and not by you. Anything behind a login, on a private network, or protected against hotlinking fails with 400 invalid_request and the vendor's message "Unable to download the file" — and so does any host whose robots.txt disallows crawlers, which Anthropic honours (picsum.photos and Wikimedia, for example: "disallowed by the website's robots.txt"). Send anything you do not control as base64.

6.3b PDFs and documents

Send the document inline as a file content part with a base64 data URL. PDFs and text types (plain text, Markdown, JSON, CSV, YAML, XML) are accepted; the gateway converts them to Claude's document block, with the filename as the title, and passes them through for OpenAI models. Other types — Word, Excel — are refused with 400 naming the media type, and file_id references are refused because there is no Files API through the gateway.

{"model":"anthropic/claude-opus-5","max_tokens":1024,"messages":[{"role":"user","content":[
  {"type":"file","file":{"filename":"contract.pdf","file_data":"data:application/pdf;base64,JVBERi0xLjQK…"}},
  {"type":"text","text":"What is the termination notice period?"}]}]}

Cost is reserved per page (worst case 4,600 tokens a page — the text plus the page as an image) and settled to the real usage afterwards. Anthropic accepts up to 32 MB and 100 pages per PDF on 200K-context models; the gateway accepts 20 MB bodies.

6.3c Prompt caching — automatic

Repeated context — a long system prompt, a big tool list, the earlier turns of a conversation — is cached by Anthropic and billed at 10 % of the input price on the next call within five minutes (refreshed on each use). The gateway turns this on for every Claude request; you do nothing. Writing the cache costs 1.25× on the cached prefix once, so it pays for itself on the second call.

6.4 Tool calling

OpenAI-style tools and tool_choice are translated for Claude; the reply comes back as tool_calls with finish_reason: "tool_calls", and you continue with role: "tool" messages as usual. strict: true on a function is honoured — the vendor then guarantees the arguments match the schema. Only tools[].type: "function" is supported; the legacy functions / function_call API is rejected.

6.5 Structured JSON output

response_format: {"type": "json_schema", …} is mapped to Claude's structured output and the answer is guaranteed to match the schema. {"type": "json_object"} has no Claude equivalent, so the gateway appends a system instruction asking for a single JSON object and sets a warning — a request, not a guarantee. Use a schema when the shape matters.

6.6 Reasoning

reasoning_effort on /v1/chat/completions maps to Claude's adaptive thinking: none disables it; low, medium, high, xhigh, max set the effort; minimal becomes low. Omitted means the model's default. On models without adaptive thinking (Haiku 4.5, Sonnet 4.5 and older) the level becomes a fixed thinking budget instead (1,024 to 32,000 tokens, kept below max_tokens) and temperature / top_p are dropped — both reported in x-gw-warnings; if max_tokens leaves no room, thinking is skipped with a warning. On /v1/messages, send thinking / output_config as Anthropic documents them.

6.7 Counting tokens before you send

POST /v1/messages/count_tokens takes the same model, system, messages and tools you are about to send and returns {"input_tokens": 21}. It is free at the vendor, so the gateway charges nothing and holds no budget for it — it only counts against your requests-per-minute and shows in the request log at cost 0. Claude models only. Both SDKs' helpers work unchanged: ai.messages.countTokens(…) in TypeScript, ai.messages.count_tokens(…) in Python. Use it to estimate a call's cost — input tokens times the input price, plus max_tokens times the output price — before deciding to make it.

6.8 Sampling and max_tokens

temperature, top_p and stop pass through (Anthropic's temperature range is 0–1; higher values are clamped with a warning; the newest Claude models accept none of these and they are dropped with a warning). max_tokens falls back to your key's default when omitted and is refused when above your key's maximum.

6.9 Generating images

POST /v1/images/generations — OpenAI's own path and body, forwarded as sent. The reply is the vendor's, unchanged: each picture is in data[].b64_json. The OpenAI SDK works without modification (client.images.generate({…}) against the gateway's base URL).

curl "$AI_GATEWAY_URL/v1/images/generations" \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" \
  -H "content-type: application/json" \
  -d '{
        "model": "openai/gpt-image-2.5-flare",
        "prompt": "a single ripe lemon on a white marble counter, morning light",
        "n": 1,
        "size": "1024x1024",
        "quality": "low"
      }'

Models. gpt-image-2.5-flare (fast, the sensible default) and gpt-image-2.5-sunburst (highest quality); gpt-image-2 is still priced. Prefixed or bare model ids both work. DALL·E is not available — OpenAI removed dall-e-2 and dall-e-3 from the API on 12 May 2026.

FieldValuesNotes
prompttext, required
n1–10bounded by your key; every picture is billed
size1024x1024, 1536x1024, 1024x1536, auto, or any WIDTHxHEIGHTbounded by your key, compared by area
qualitylow, medium, high, xhigh, max, autobounded by your key
output_formatpng, jpeg, webpdefault png
backgroundtransparent, opaque, autotransparent needs png or webp
streamrefused, 400 unsupported_parameter

Your key's image settings

Image generation is off by default on every key. An admin switches it on per key, together with ceilings on how many pictures per request, how large, at what quality, which image models, and an optional cost cap. Read your own from GET /v1/key and check at startup rather than discovering a limit in production:

"images": {
  "allowed": true,
  "max_images_per_request": 2,
  "max_size": "1536x1024",
  "max_quality": "high",
  "max_cost_per_request_usd": null,
  "allowed_models": []
}

allowed_models: [] means every image model the key may already use. A request over any ceiling is refused before a vendor is called, so it costs nothing.

What comes back when it is refused

SituationStatus and code
Images not switched on for this key403 service_not_allowed
A chat model sent here400 invalid_request — names the endpoint to use instead (was 404 model_not_found before 19 September 2026)
An image model sent to /v1/chat/completions400 invalid_request — points back here (was 404 model_not_found)
n, size or quality above your key's ceiling400 invalid_request, naming the ceiling
Worst case above a per-request cost cap402 request_too_expensive, naming estimate and cap
stream: true400 unsupported_parameter
Budget or credit exhausted402, as anywhere else
The prompt itself refused by OpenAI400 invalid_request, prefixed The provider rejected the request:

Cost and timing

Billed in tokens like everything else: the prompt at the model's input rate, the pictures at its output rate, so x-gw-cost-usd is exact. Unlike chat, the worst case is known before the call — how many pictures, how large and at what quality are all in the body — so the reservation is tight rather than a guess.

At 1024x1024 a low picture is about $0.006 and max about $0.21 — roughly forty times. Choose quality deliberately; auto is reserved at the most your key allows, because you have not said. Expect 10–60 seconds, with no streaming, so set your client timeout accordingly.
Image editing and variations (/v1/images/edits, /v1/images/variations) are not proxied. Generation only.

6.10 Embeddings

POST /v1/embeddings — OpenAI's own path and body. The body goes to OpenAI exactly as you sent it, with only model rewritten to the vendor's id, and OpenAI's reply comes back unchanged.

curl
curl "$AI_GATEWAY_URL/v1/embeddings" \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "openai/text-embedding-3-small",
        "input": ["Clause 4 limits liability to the fees paid.", "Payment is due within 30 days."]
      }'
{"object":"list",
 "data":[{"object":"embedding","index":0,"embedding":[-0.0061,0.0142,…]},
         {"object":"embedding","index":1,"embedding":[0.0217,-0.0093,…]}],
 "model":"text-embedding-3-small",
 "usage":{"prompt_tokens":19,"total_tokens":19}}
OpenAI SDK · ai is the client from §2.2
// TypeScript                                        # Python
const res = await ai.embeddings.create({             res = ai.embeddings.create(
  model: "openai/text-embedding-3-small",                model="openai/text-embedding-3-small",
  input: ["Clause 4 …", "Payment is due …"],             input=["Clause 4 …", "Payment is due …"],
});                                                  )
const vectors = res.data.map((d) => d.embedding);    vectors = [d.embedding for d in res.data]
// number[] each, 1536 long                          # list[float] each, 1536 long

Models and prices — per 1M input tokens. Nothing is generated, so there is no output price. The prefix is optional.

ModelPriceVector length (default)
openai/text-embedding-3-small$0.021536
openai/text-embedding-3-large$0.133072
openai/text-embedding-ada-002$0.101536
FieldValuesNotes
inputa string, an array of strings, an array of token ids, or an array of token-id arraysrequired; an empty string or empty array is 400 invalid_request before OpenAI is called
encoding_formatfloat (OpenAI's default), base64passed through as sent — see below
dimensionsintegertext-embedding-3-* only; shortens the vector. OpenAI decides
userstringpassed through

Floats or base64. The gateway sends encoding_format only if you did: ask for nothing and you get arrays of floats, ask for base64 and you get base64 strings, untouched. The gateway never switches to base64 behind your back. (The OpenAI SDKs do so themselves: when you leave encoding_format out, embeddings.create asks for base64 on the wire and decodes it back into floats for you. That works unchanged through the gateway. With curl or a plain HTTP client you get floats unless you ask.)

What is billed. usage.prompt_tokens from OpenAI's reply, at the model's input rate, and nothing else. Before the call the gateway reserves an over-estimate — text at one token per 3 bytes, token-id arrays at their exact length — which counts against your per-request cap, then settles to the real count. x-gw-cost-usd is exact.

Limits OpenAI enforces: 8,192 tokens per input, 2,048 inputs per request, 300,000 tokens across all inputs in one request. The gateway does not pre-check these; OpenAI's refusal comes back as 400 invalid_request prefixed The provider rejected the request:. The gateway's own body limit is 20 MB (413 request_too_large). Split large batches into several requests.

Retries and logging. The gateway retries once itself on a transient vendor failure (a 5xx or a dropped connection) — safe, because an embedding is idempotent. On a key that logs at full, the input is kept but the vectors are not.

What comes back when it is refused

SituationStatus and code
A chat, image, speech or transcription model sent here400 invalid_request — names the endpoint to use
An embedding model sent to /v1/chat/completions400 invalid_request — points back here
input missing, empty, or not one of the four shapes400 invalid_request, before any vendor call
Over one of OpenAI's limits above400 invalid_request, prefixed The provider rejected the request:
Estimate above your key's per-request cost cap402 request_too_expensive, naming estimate and cap
Budget or credit exhausted402, as anywhere else
Model not permitted for this key or organisation403 model_not_allowed
A mail key ("service": "mail" in GET /v1/key)403 service_not_allowed
OpenAI rejected the vendor key503 credential_unavailable — the vendor key is marked unhealthy and nothing is charged; tell the admin

6.11 Speech

POST /v1/audio/speech — OpenAI's speech request, answered by ElevenLabs or by OpenAI's tts-1 / tts-1-hd. The reply is the audio itself: raw bytes with the vendor's Content-Type (audio/mpeg for mp3), not JSON. Errors are JSON as usual, so check the status before writing the body to a file.

curl
curl "$AI_GATEWAY_URL/v1/audio/speech" \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "elevenlabs/eleven_flash_v2_5",
        "voice": "narrator",
        "input": "Your settlement has been approved and will be paid on Friday.",
        "response_format": "mp3"
      }' \
  --fail-with-body --output reply.mp3
OpenAI SDK · saving an mp3
// TypeScript                                        # Python
import { writeFile } from "node:fs/promises";

const res = await ai.audio.speech.create({           res = ai.audio.speech.create(
  model: "elevenlabs/eleven_flash_v2_5",                 model="elevenlabs/eleven_flash_v2_5",
  voice: "narrator",                                     voice="narrator",
  input: "Your settlement has been approved …",          input="Your settlement has been approved …",
  response_format: "mp3",                                response_format="mp3",
});                                                  )
await writeFile("reply.mp3",                         res.write_to_file("reply.mp3")
  Buffer.from(await res.arrayBuffer()));

Models and prices — per character of input. ElevenLabs models need the elevenlabs/ prefix.

ModelPriceNotes
elevenlabs/eleven_flash_v2_5$0.05 per 1K charactersfastest and cheapest, multilingual
elevenlabs/eleven_flash_v2$0.05 per 1K charactersEnglish only
elevenlabs/eleven_multilingual_v2$0.10 per 1K characters
elevenlabs/eleven_v3$0.10 per 1K characters
elevenlabs/eleven_turbo_v2_5, elevenlabs/eleven_turbo_v2$0.05 per 1K charactersdeprecated by ElevenLabs — use Flash
tts-1 (or openai/tts-1)$15 per 1M characters ($0.015 per 1K)
tts-1-hd$30 per 1M characters ($0.03 per 1K)

The voice

FieldOpenAI modelsElevenLabs models
inputrequiredrequired. Each vendor caps the length (OpenAI: 4,096 characters); its refusal comes back as 400 invalid_request
response_formatmp3 (default), opus, aac, flac, wav, pcmmp3 (default; 44.1 kHz, 128 kbit/s), opus (48 kHz, 128 kbit/s), wav (24 kHz), pcm (raw 24 kHz 16-bit mono, the same as OpenAI's). aac and flac are 400 invalid_request, before the call
speed0.25–4.0 (OpenAI decides)0.7–1.2; outside that, 400 invalid_request before the call
instructionspassed to OpenAIignored, and named in x-gw-warnings
stream, stream_format: "sse"refused, 400 unsupported_parameter

For OpenAI models the body is forwarded as sent (only model rewritten). For ElevenLabs models only the fields above are used.

What is billed. The characters of input at the model's rate, counted as Unicode code points (an emoji is one character). The count is known before the call, so the reservation is exact. For ElevenLabs, the vendor's own figure wins when it reports one (the character-cost response header): a Voice Library voice with a custom rate costs more characters than the text has. x-gw-cost-usd is exact. As a guide, 1,000 characters cost $0.05 on Flash, $0.10 on v3 or Multilingual v2 and $0.015 on tts-1.

Access and limits. Any AI key whose model allowlist includes the model may use it; unlike images, audio has no separate switch on the key. Tokens per minute does not apply — there are no x-ratelimit-*-tokens headers — but requests per minute, concurrency, budgets and credit do. On a key that logs at full, the text is kept, never the audio.

What comes back when it is refused

SituationStatus and code
input or voice missing400 invalid_request
ElevenLabs model with a voice that is neither an organisation name nor a voice id400 invalid_request, listing your organisation's voice names
ElevenLabs model with aac / flac, or speed outside 0.7–1.2400 invalid_request, before the call
stream: true or stream_format: "sse"400 unsupported_parameter
A model that is not a speech model400 invalid_request, naming its endpoint
Estimate above your key's per-request cost cap402 request_too_expensive
Budget or credit exhausted402, as anywhere else
Model not permitted / a mail key403 model_not_allowed / 403 service_not_allowed
The vendor rejected its key503 credential_unavailable — nothing charged; tell the admin

6.12 Transcription

POST /v1/audio/transcriptions — OpenAI's transcription upload, answered by AssemblyAI, ElevenLabs Scribe or OpenAI's whisper-1. Send multipart/form-data with the audio in file, exactly as for OpenAI.

curl
curl "$AI_GATEWAY_URL/v1/audio/transcriptions" \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" \
  -F model=assemblyai/universal-3-5-pro \
  -F file=@call-recording.mp3 \
  -F response_format=json \
  --max-time 900
{"text":"Hello, I'm calling about settlement S-1042…","usage":{"type":"duration","seconds":184}}
OpenAI SDK · a timeout longer than the gateway's
// TypeScript
import { createReadStream } from "node:fs";

const t = await ai.audio.transcriptions.create(
  { model: "assemblyai/universal-3-5-pro", file: createReadStream("call-recording.mp3"), language: "en" },
  { timeout: 15 * 60 * 1000 }, // AssemblyAI can take up to 10 minutes: wait longer than the gateway does
);
console.log(t.text);

# Python
with open("call-recording.mp3", "rb") as f:
    t = ai.with_options(timeout=900).audio.transcriptions.create(  # wait longer than the gateway's 10 minutes
        model="assemblyai/universal-3-5-pro",
        file=f,
        language="en",
    )
print(t.text)

Models and prices — per second of audio, rounded up to a whole second. AssemblyAI and ElevenLabs models need their prefix.

ModelPriceNotes
assemblyai/universal-3-5-pro$0.21 per hourthe only one that takes prompt at AssemblyAI
assemblyai/universal-2$0.15 per hour
elevenlabs/scribe_v2$0.22 per hour
whisper-1 (or openai/whisper-1)$0.006 per minute ($0.36 per hour)OpenAI shuts it down on 26 February 2027
FieldNotes
filerequired. Up to 25 MB; larger is 413 request_too_large (Audio upload exceeds 25 MB (26214400 bytes).). Common formats (mp3, m4a, wav, ogg, webm, flac) work with all three vendors; the vendor decides
modelrequired
languageoptional ISO-639-1 code (en, de, …). Without it each vendor detects the language
promptsent to whisper-1 and AssemblyAI; ignored for ElevenLabs Scribe, with an x-gw-warnings note
response_formatjson (default), text, verbose_json, srt, vtt — for every vendor. Anything else (such as diarized_json) is 400 invalid_request
temperaturesent to every vendor
timestamp_granularities[]ignored, with an x-gw-warnings note: the gateway always asks for word timings, and verbose_json carries word and segment timings whenever the vendor provides them
streamtrue is refused, 400 unsupported_parameter

Every format works with every vendor

The gateway renders the five formats itself from the vendor's answer, so subtitles from AssemblyAI or ElevenLabs have the same shape as from whisper-1.

response_formatContent-TypeBody
jsonapplication/json{"text": "…", "usage": {"type": "duration", "seconds": 184}}seconds is what you are billed for
texttext/plainthe transcript
verbose_jsonapplication/jsontask, language, duration, text, words: [{word, start, end}], segments: [{id, start, end, text}], times in seconds
srttext/plainnumbered SubRip cues
vtttext/vttWebVTT cues

For AssemblyAI and ElevenLabs, segments and subtitle cues are built from the word timings: a cue ends at the end of a sentence, at a pause of more than a second, or at 7 seconds or 16 words. whisper-1's own segments are used as they are. language is whatever the vendor reports, so its form differs between vendors.

Time, money, and the one charged error

whisper-1 and Scribe answer in one call. AssemblyAI works asynchronously: the gateway uploads the file, submits the job and polls until it finishes, all inside your one request, which for a long recording can take minutes. The gateway waits up to 10 minutes, upload included. Set your client's timeout above that, as in the examples — the OpenAI SDKs default to exactly 10 minutes, and curl to none. If your client gives up first, the SDK counts it as a timeout and retries: a second job, charged again, while the first one still finishes and is charged too.

What is billed. Seconds of audio as the vendor reports them, rounded up (at least 1). The length is not known before the call, so the gateway reserves as if the file were compressed speech at 32 kbit/s (file bytes ÷ 4,000 = seconds), then settles to the real duration. That over-counts almost any real file — uncompressed 16 kHz WAV by 16× — so a 25 MB WAV holds about 6,550 seconds (about $0.38 at Universal-3.5 Pro) until it settles. With a tight per-request cap or a nearly spent budget, send compressed audio (mp3, opus) so the reservation fits.

A 504 here is charged. If AssemblyAI has accepted the job but not finished it within the 10 minutes, you get 504 upstream_timeout — and the request is charged at the reservation estimate, because AssemblyAI finishes the job and bills for it regardless. The 504 carries the charge like any billed response — x-gw-cost-usd, x-gw-billing, x-gw-usage-estimated: 1 — and x-should-retry: false, and its message says that retrying will transcribe and charge again. The OpenAI SDKs obey x-should-retry, so they do not retry it; if you have retry logic of your own, make it honour that header too. The transcript cannot be fetched through the gateway afterwards. Disconnecting does not cancel a job either — it is still charged.

Access and limits. As for speech: any AI key whose model allowlist includes the model, with requests per minute, concurrency, budgets and credit as usual. On a key that logs at full, the transcript and the form fields are kept, never the audio.

What comes back when it is refused

SituationStatus and code
Not multipart/form-data, or no file400 invalid_request, before any vendor call
response_format not one of the five400 invalid_request
stream=true400 unsupported_parameter
A model that is not a transcription model400 invalid_request, naming its endpoint
AssemblyAI could not transcribe the file (too short, unreadable)400 invalid_request, prefixed The provider rejected the request: — nothing charged
Size-based estimate above your key's per-request cost cap402 request_too_expensive
Budget or credit exhausted402, as anywhere else
Model not permitted / a mail key403 model_not_allowed / 403 service_not_allowed
File over 25 MB413 request_too_large. The gateway refuses on the Content-Length header, so a client that streams a larger body may see the connection closed instead of the 413 — check the size before uploading
AssemblyAI rate-limiting the gateway's key (it signals this with a 403)429 upstream_rate_limited, with retry-after
The vendor rejected its key503 credential_unavailable — nothing charged; tell the admin
AssemblyAI still working after 10 minutes504 upstream_timeout with x-should-retry: falsecharged at the estimate; do not retry

6.13 Sending mail — POST /v3/mail/send

The gateway speaks SendGrid's own path and wire format, so an existing project moves onto it by changing two lines and nothing else:

// TypeScript                                        # Python
sgMail.setApiKey(process.env.AI_GATEWAY_KEY);        sg = SendGridAPIClient(os.environ["AI_GATEWAY_KEY"])
sgMail.client.setDefaultRequest(                     sg.client.host = os.environ["AI_GATEWAY_URL"]
  "baseUrl", process.env.AI_GATEWAY_URL);

The body is forwarded to SendGrid verbatim. The gateway authenticates the key, applies its address rules, counts the recipients and attributes the send; it never rewrites your mail.

curl -sS $AI_GATEWAY_URL/v3/mail/send \
  -H "Authorization: Bearer $AI_GATEWAY_KEY" -H "Content-Type: application/json" \
  -d '{"personalizations":[{"to":[{"email":"someone@example.com"}]}],
       "from":{"email":"noreply@yourdomain.com"},
       "subject":"Hello","content":[{"type":"text/plain","value":"Hi there"}]}'
# 202 Accepted, empty body — exactly as SendGrid answers

A mail key is a different kind of key. It is scoped to this endpoint and cannot call a model at all, and an AI key cannot send mail: either mistake is 403 service_not_allowed. Ask your admin for the kind you need.

Limits count recipients, not requests. One call can address up to 1,000 people, so a request-per-minute limit would not stop a blast. Your key carries ceilings per minute, per hour and per day, and a maximum per send; over any of them the send is refused with 429 recipient_limit_exceeded and nothing reaches SendGrid, so nothing counts against your account's reputation. The answer carries x-gw-recipients and x-ratelimit-remaining-recipients-minute / -hour / -day.

SituationStatus and code
An AI key at this endpoint, or a mail key at a model endpoint403 service_not_allowed
A body whose recipients cannot be counted400 invalid_request — it is never forwarded uncounted
Over a recipient ceiling429 recipient_limit_exceeded, with retry-after
SendGrid rejected the messageIts own status and body, relayed; the recipients are given back
No SendGrid key on the organisation503 credential_unavailable

Every send is recorded in the key's send log with the sender, the recipients, the subject and the outcome — and, on a key that logs at full, the message itself. Refused sends are recorded too, with the recipients they would have reached.

7. What comes back in the headers

HeaderMeaning
x-gw-request-idQuote this in any question to the gateway admin — it finds the request in the log instantly.
x-gw-modelThe real model that answered, after alias resolution.
x-gw-cost-usdExact cost of this request. On streams it is in the trailer comment instead.
x-gw-billingbyok — billed to the organisation's own vendor account; prepaid — deducted from prepaid credit.
x-gw-budget-remaining-usdWhat is left in the tightest budget that applies to your key.
x-gw-warningsParameters that were changed or dropped in translation, separated by semicolons. Worth logging in development.
x-gw-usage-estimatedPresent only when the cost is a worst-case estimate because the vendor's usage figures never arrived, or a transcription was charged its estimate on a 504.
x-should-retryOnly on a charged transcription 504, set to false: do not retry. The OpenAI SDKs obey it.
x-ratelimit-limit-requests, …-remaining-requests, …-reset-requestsYour requests-per-minute limit, what is left, when it resets.
retry-afterOn every 429: seconds to wait.

8. Errors and what to do about them

Errors come in the format of the endpoint you called — the OpenAI envelope on /v1/chat/completions, /v1/images/generations, /v1/embeddings and /v1/audio/*, the Anthropic envelope on /v1/messages — so the SDKs raise their normal typed exceptions. Every error carries a stable code; branch on that, not on the message.

{"error":{"message":"Monthly budget of $10.000000 for this key would be exceeded (remaining $0.002000, estimate $0.002014). Resets 2026-10-01T00:00:00.000Z.","type":"budget_error","code":"budget_exceeded",
          "request_id":"req_01…","budget":{"scope":"key","period":"monthly","cap_usd":10,"remaining_usd":0.002,"resets_at":"2026-10-01T00:00:00.000Z"}}}
HTTPCodeMeaningDo
400invalid_requestMalformed body, the model answers on a different endpoint (the message names it), or the vendor rejected it (its message is relayed)Fix the request
400unsupported_parameterParameter has no equivalent for this providerRemove it (see §10)
401missing_api_key / invalid_api_keyNo key, or wrong keyCheck AI_GATEWAY_KEY
402budget_exceededYour key's daily / monthly / lifetime cap is reached; budget.resets_at says whenWait for the reset or ask the admin to raise the cap
402tenant_ceiling_exceededThe whole organisation's monthly ceiling is reachedTalk to the admin
402credit_exhaustedNo prepaid credit and no vendor key for this providerTalk to the admin
402request_too_expensiveThe worst case for this one request is over the per-request capLower max_tokens or shorten the input
402vendor_key_cap_reachedThe vendor key your request routes through has reached its monthly capTalk to the admin: the cap can be raised, or the key can fall back to the organisation default
403ip_not_allowedYour address is not on the key's listRun from an allowed network, or ask the admin to add the address — the console shows every refused address with one button to allow it
403key_suspended / key_revoked / key_expiredThe admin disabled the key (suspended is temporary; revoked and expired are final)Ask the admin; revoked/expired need a new key
403model_not_allowedYour key may not use that model, or the vendor key it routes through does not allow it — the message names whichPick one from GET /v1/models, which lists exactly what this key can reach today
403service_not_allowedYour key may not use this endpoint — a mail key calling images, embeddings or audio, or images not switched on for the keyUse an AI key; ask the admin to allow images
404model_not_foundUnknown model nameCheck spelling and prefix
413request_too_largeBody too big (20 MB; 25 MB for a transcription upload)Shrink it
429rate_limit_exceeded / concurrency_limit_exceeded / tenant_rate_limit_exceeded / upstream_rate_limitedToo fast, too many in flight, organisation-wide limit, or the vendor is throttlingWait retry-after seconds and retry with backoff
502 / 504upstream_error / upstream_timeoutThe vendor failed or timed outRetry once with backoff, then surface the error — but not a transcription 504 carrying x-should-retry: false, which is charged (§6.12)
503provider_unavailable, credential_unavailable, platform_cap_reached, gateway_unavailable, tenant_storage_unavailableNo usable vendor key right now, or a gateway dependency is down (it fails closed on purpose)Retry with backoff; alert the admin if it persists
Every refusal is recorded. Whatever the gateway turns away, and whatever the vendor fails, is written to your organisation's refusal log with the key, the address it came from, your User-Agent, the reason and the numbers behind it — and an admin sees it under Refusals in the console. So a key used from a new address will be noticed: five distinct new addresses within an hour suspend it automatically. Wrong secrets never suspend a key, however many are tried, but every attempt is recorded. The secret itself is never stored — only the public key id, the part before the last dash.
Two guarantees. Any 401, 402, 403, 404, 413 or 429 from the gateway means the request was never sent to the vendor and nothing was charged. And a safe retry policy is: retry only 429, 502, 503 and 504, honour retry-after, exponential backoff, at most three attempts. Never retry the other 4xx codes — they will fail the same way. The one exception to "retry a 504": on /v1/audio/transcriptions, a 504 after AssemblyAI accepted the job is charged and carries x-should-retry: false, so a retry pays twice — surface it instead. Any retry logic of your own should honour x-should-retry.

9. Limits, budgets and money

Every gateway key carries limits set by the admin when it was created: requests per minute and per day, concurrent requests, daily / monthly / lifetime spending caps, a per-request cost cap, an allowed-model list, and address rules. They exist so that a leaked key or a runaway loop has a bounded blast radius. There is no tokens-per-minute limit: a cheap model returns a great many tokens for very little money, so the figure never bounded anything anyone cared about, and the x-ratelimit-*-tokens headers are no longer sent. You can read your own limits any time with GET /v1/key.

The gateway checks money before the vendor is called. It reserves the worst case — max_tokens multiplied by the output price plus the input — then settles to the real cost afterwards. This means a needlessly large max_tokens can be refused with 402 even though the actual answer would have been short. Ask for what you need.

The other endpoints reserve their own way: embeddings on the input alone, speech on the exact character count, transcription on the file's size (§6.10–6.12). A prompt past a model's long-context line is reserved and billed at the higher rate for the whole request (§4.3).

x-gw-billing tells you who pays: byok means the organisation's own vendor account is billed directly and the gateway only meters; prepaid means the cost is deducted from credit held with the gateway.

10. What is not supported

On /v1/chat/completions with Claude models

Rejected with 400 unsupported_parameterAltered — always reported in x-gw-warnings
n > 1 · logprobs · top_logprobs · seed · audio in or out · file parts with a file_id or a non-PDF/non-text media type · legacy functions / function_call / role: "function" · non-function tool types frequency_penalty, presence_penalty, logit_bias (dropped) · response_format: json_object (becomes a system instruction) · temperature > 1 (clamped) · sampling parameters on models that do not accept them (dropped) · forced tool_choice on models that only accept auto (downgraded) · any other field the translator does not know (store, metadata, prompt_cache_key, …: ignored and named)

On /v1/chat/completions with OpenAI models

Refused with 400 unsupported_parameter because they change what OpenAI charges in ways the gateway cannot price yet — refusing is better than under-billing: web_search_options, prediction, audio / modalities: ["audio"], and service_tier other than auto / default.

On every endpoint

CapabilityStatus
Files APINot yet. Send documents inline as base64 on /v1/messages.
Message BatchesNot yet.
Token countingYesPOST /v1/messages/count_tokens, Claude models, free.
Image generationYesPOST /v1/images/generations, OpenAI GPT-Image models, opt-in per key (§6.9).
Image editing and variations, partial-image streamingNot yet. Generation only; stream is refused with 400 unsupported_parameter.
EmbeddingsYesPOST /v1/embeddings, OpenAI embedding models (§6.10).
Speech and transcriptionYesPOST /v1/audio/speech (ElevenLabs, tts-1) and POST /v1/audio/transcriptions (AssemblyAI, ElevenLabs Scribe, whisper-1) (§6.11, §6.12).
Streaming speech or transcriptionRefused with 400 unsupported_parameter; the whole audio or transcript comes back in one response.
OpenAI's token-billed audio models (gpt-4o-mini-tts, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-transcribe)Not yet. The gateway cannot meter them reliably; use tts-1 / whisper-1, ElevenLabs or AssemblyAI.
Vendor features beyond OpenAI's audio shape (speaker labels, diarized_json, summaries, redaction, voice settings other than speed), audio translationNot available.
Vendor-native pass-through (/v1/vendors/…), OpenAI Responses APINot yet.
PDFs and documentsYesfile parts on /v1/chat/completions (§6.3b).
Prompt cachingYes — automatic for Claude (§6.3c).
Vendor server tools (web search, code execution)Refused on /v1/chat/completions; pass through on /v1/messages but their per-use surcharge is not included in x-gw-cost-usd.
Claude's thinking text in the replyNot returned on /v1/chat/completions — the OpenAI shape has no field for it.

11. Getting and checking a key

Keys are created by your organisation's admin in the gateway console (Keys → New key). Tell them the project name, the models you need, a sensible budget and rate limit, and the addresses the project runs from. If your address changes, or you do not know it, ask them to tick "add the next new address automatically" and make one request within ten minutes: that address is added to the key's list, once, and the window closes. The secret is shown once — put it straight into your project's secret store as AI_GATEWAY_KEY.

Call GET /v1/key at startup: it fails fast if the key or URL is wrong, and it shows your limits, budgets, remaining spend and which vendor key your requests will route to. It never reveals which addresses are allowed, only how many there are.

{"key_id":"muhr4dre","name":"settlement-worker","status":"active","environment":"production",
 "routing":{"anthropic":{"vendor_key":"own:anthropic-production","billing":"byok",
                         "environment":"production","models":["anthropic/claude-opus-5"]}},
 "allowed_models":[],
 "rate_limits":{"rpm":120,"max_concurrency":8,"rpd":null,"rpm_per_ip":null},
 "budgets":[{"period":"monthly","cap_usd":10,"spent_usd":0.000411,"resets_at":"2026-10-01T00:00:00.000Z"}],
 "ip_mode":"allowlist","ip_allowlist_count":2,"ip_capture_open":false,"expires_at":null}

routing has an entry for each provider — anthropic, openai, xai, elevenlabs, assemblyai — saying which vendor key a request would use, who pays, which environment that key belongs to, and models: what that key allows, or "all". That list is the authoritative answer to "why was my model refused?" — a model outside it is 403 model_not_allowed naming the vendor key. A provider with no usable key shows "vendor_key": null and an error code: expect the same refusal from the endpoint.

In allowed_models, an entry ending /*"openai/*" — means every model that provider's vendor key allows, whatever that becomes.

12. Telling your AI assistant

Most projects are written with an AI coding assistant. Two things make it integrate correctly the first time:

  1. Give it docs/INTEGRATION.md — the machine-readable version of this guide, with exact request and response shapes.
  2. Paste the block below into your project's CLAUDE.md (or AGENTS.md), so the rules apply to every future change without anyone remembering to mention them.
## AI calls go through A1 API Gateway
- All LLM calls use A1 API Gateway, never a vendor API directly. Never add a vendor API key.
- Base URL: `AI_GATEWAY_URL` env var. Key: `AI_GATEWAY_KEY` env var (`sk-gw-…`). Read both from the environment.
- OpenAI SDK: `baseURL = AI_GATEWAY_URL + "/v1"`. Anthropic SDK: `baseURL = AI_GATEWAY_URL` (no `/v1`).
- Model names are `provider/model`, e.g. `anthropic/claude-opus-5`. Prefer the alias the admin gave us if one exists.
- Always use `/v1/chat/completions` (OpenAI format). It supports PDFs/documents (`file` parts as base64 data URLs),
  images, tools, JSON output, reasoning, streaming, and prompt caching is automatic. `/v1/messages` only with admin approval.
- `GET /v1/models` lists the models we may use, each with a `capabilities` block (images, documents, tools, …) — check it
  rather than guessing. `POST /v1/messages/count_tokens` counts tokens for free before an expensive call.
- Put stable content first (system prompt, tools), the changing message last — that is what gets cached.
- Each model answers on one endpoint; the wrong one is `400 invalid_request` naming the right one.
- Image generation: `POST /v1/images/generations` with `gpt-image-2.5-flare` (or `-sunburst`) — only if our key allows it.
  No streaming, generation only, and quality drives the cost steeply (`low` → `max` is about 40x).
- Embeddings: `POST /v1/embeddings` with `openai/text-embedding-3-small` (or `-3-large`), OpenAI's body and reply unchanged.
- Speech: `POST /v1/audio/speech` returns raw audio bytes. ElevenLabs models need the `elevenlabs/` prefix and a voice
  name our organisation set up (or a 20-character ElevenLabs voice id); OpenAI names like `alloy` only work on `tts-1`.
- Transcription: `POST /v1/audio/transcriptions` (multipart, ≤ 25 MB) with `assemblyai/universal-3-5-pro`,
  `elevenlabs/scribe_v2` or `whisper-1`; all five response formats work. AssemblyAI can take up to 10 minutes: set the
  client timeout above that. A transcription 504 is charged and says `x-should-retry: false` — never retry it.
- Not available: Files API (`file_id`), batches, image editing, streaming audio, Responses API, web search.
- Retry only 429/502/503/504 with `retry-after` and backoff (never a transcription 504). 4xx other than 429 means fix the request or config.
- Every response has `x-gw-cost-usd` (exact cost) and `x-gw-request-id` (quote it in bug reports).
- Full reference: `docs/INTEGRATION.md` in the AIGateway repository.
Changelog — 2026-09-19 (later): models are chosen per vendor key, so GET /v1/keyrouting.<provider>.models is what a key may actually ask for and 403 model_not_allowed names the vendor key that refused it; new 402 vendor_key_cap_reached when a vendor key reaches its monthly cap; tokens per minute is gone from keys, with the x-ratelimit-*-tokens headers; addresses are a list only — "pin to the first address used" became "add the next new address automatically", one address within ten minutes; every refusal is recorded for the organisation's admin, and five distinct new addresses within an hour suspend a key. · 2026-09-19: added POST /v1/embeddings (OpenAI, forwarded untouched, encoding_format passed through), POST /v1/audio/speech (ElevenLabs, OpenAI tts-1 / tts-1-hd; organisation voice names) and POST /v1/audio/transcriptions (AssemblyAI, ElevenLabs Scribe, OpenAI whisper-1; all five response formats for every vendor); every model answers on one endpoint and the wrong one is 400 invalid_request naming the right one — image models at the wrong door were 404 model_not_found before; long-context requests billed at the higher rate for the whole request; catalogue re-read (gpt-5.6 $4 / $20, retired xAI names bill as grok-4.3, grok-3-mini switched off); tokens per minute does not apply to audio; a charged transcription 504 carries its cost headers and x-should-retry: false. · 2026-09-17: added POST /v1/images/generations (OpenAI GPT-Image, forwarded untouched), opt-in per key with ceilings on count, size and quality; generation only, no edits, variations or streaming. · 2026-09-07: first version, describing consumer API v1 as implemented (spec v0.10). · 2026-09-07: added POST /v1/messages/count_tokens and the capabilities block on every model in GET /v1/models. · 2026-09-08: /v1/chat/completions is the standard door — PDFs and documents via file parts, automatic prompt caching for Claude, strict tools, json_object as a system instruction, every ignored field reported, OpenAI fields the gateway cannot price refused, documents reserved per page, prompt_caching in capabilities.