How the gateway works
One host serves the Anthropic, OpenAI and Gemini APIs natively. Endpoints, base URLs, accepted model ids, auth headers and what Half Off Dev touches.
The point of Half Off Dev is that you keep the code you already have. Claude, GPT and Gemini are each served in their own vendor's wire format, on a single host, at half the list price. Your SDK sends the same JSON it would send to the vendor; only the base URL and the key change.
There is no translation layer in the middle. An Anthropic-style request is handed to a channel that speaks Anthropic, an OpenAI-style request to one that speaks OpenAI, and so on. Apart from two narrow edits, the body arrives as you wrote it, so vendor features like thinking, cache_control, tools or generationConfig behave exactly as each vendor's own reference describes.
Endpoints at a glance#
Give your SDK the base URL from this table and let it build the path.
| Wire format | Paths | Base URL for the SDK | Guide |
|---|---|---|---|
| Anthropic Messages | POST /v1/messages, POST /v1/messages/count_tokens | https://halfoff.dev | Anthropic Messages |
| OpenAI Chat Completions | POST /v1/chat/completions | https://halfoff.dev/v1 | OpenAI |
| OpenAI Responses (GPT only) | POST /v1/responses | https://halfoff.dev/v1 | Responses API |
| Gemini | POST /v1beta/models/{model}:generateContent, :streamGenerateContent, :countTokens | https://halfoff.dev | Gemini |
| Model catalog | GET /v1/models, GET /v1beta/models | Pricing |
Anything outside this list is unsupported; see what you can't call.
Sending your key#
Each endpoint looks for the key in four places, so whatever an SDK does by default just works:
| Location | Typical sender |
|---|---|
Authorization: Bearer ho-... | OpenAI SDKs, Claude Code via ANTHROPIC_AUTH_TOKEN, plain HTTP clients |
x-api-key: ho-... | Anthropic SDKs |
x-goog-api-key: ho-... | Google Gen AI SDKs, Gemini CLI |
?key=ho-... | Gemini REST snippets |
The two catalog endpoints are public and need no key. Creating, limiting and rotating keys is covered in Authentication.
Native format per model family#
Each family was designed around one API. Calling it there gives you every feature the vendor ships.
| Family | Native format | Also accepted today |
|---|---|---|
Claude (claude-*) | Anthropic Messages | OpenAI Chat Completions, Gemini |
GPT (gpt-*) | OpenAI Chat Completions, OpenAI Responses | Anthropic Messages, Gemini |
Gemini (gemini-*) | Gemini | OpenAI Chat Completions, Anthropic Messages |
Using a non-native format means the channel converts your request for the model, and vendor-only options may be dropped or refused on the way. Anthropic's cache_control and thinking, OpenAI's reasoning_effort and Gemini's thinkingConfig are only guaranteed in the model's home format.
Which formats a model accepts follows from the channels serving it, so the list can move. The model marketplace shows the current formats per model. When no channel takes your format, you get 404 with a message such as Model 'gpt-6.1-sol' is not served in gemini format in any group.
Model ids#
The canonical ids are the ones in GET /v1/models, for example claude-opus-5.5, gpt-6.1-sol or gemini-3-flash. Tools that spell ids their own way still land on the right model, because the gateway normalizes what it receives:
| Sent | Treated as |
|---|---|
Claude-Opus-5-5 | claude-opus-5.5 |
anthropic/claude-opus-5.5 | claude-opus-5.5 |
claude-haiku-4-5-20251001 | claude-haiku-4.5 |
gemini-3.1-pro-preview | gemini-3.1-pro |
models/gemini-3-flash | gemini-3-flash |
Normalization lower-cases the id, strips a leading vendor/ or models/, treats . and - as the same character, and removes a trailing date (-20251001), -preview or -latest. Billing, logs and key allow-lists always record the canonical id.
An id that maps to nothing returns 404: Model '<id>' is not available. See GET /v1/models.
The path of one request#
- Key check. An inactive or expired key gets
401. - Model lookup. The id is normalized as above and compared with the key's model allow-list; a model outside it gets
403. - Balance check. A balance at or below zero, or a key that has used up its spend limit, gets
402. - Key limits. Per-key requests-per-minute and concurrency caps apply (
429). Details in Rate limits & failover. - Channel choice. A conversation that is already under way returns to the channel holding its prompt cache; a new one goes to the healthiest channel in its channel group. See Prompt caching.
- Model call. A channel that fails before any output is swapped for another, up to three attempts in total. Streams are held back until the first token so the swap is invisible. See Streaming.
- Metering. The token counts in the response decide the charge, at half the list price, and it leaves your balance within seconds. See Pricing.
What the gateway changes#
Two edits are made to the body, and nothing else:
model. The gateway writes in the spelling the serving channel expects. As a result, themodelfield you get back can use the vendor's form, such asclaude-opus-5-5.- Usage in OpenAI streams. Every streaming Chat Completions request gets
stream_options.include_usage: trueso the call can be billed. Your stream therefore ends with one extra chunk whosechoicesarray is empty and which carriesusage. Make sure your loop tolerates an emptychoices.
Headers you send are dropped, with three exceptions that are forwarded: anthropic-version, anthropic-beta and openai-beta. If anthropic-version is absent, 2023-06-01 is filled in. On Gemini paths, query parameters such as alt=sse go through, while key is stripped.
Response bodies are returned byte for byte. content-type and cache-control are kept and the gateway adds its own headers; vendor request ids and vendor rate-limit headers are not passed on.
If the model refuses the request as invalid (400, 404, 413, 422), you receive that status and reason in the error format of the API you called. Any other failure is first retried on another channel. More in Rate limits & failover and Errors.
Headers on every response#
| Header | Contents |
|---|---|
x-halfoff-request-id | This call's id, like req_6f1c.... It is the key to the entry in Logs; quote it when you write to support. |
x-halfoff-group | The channel group that answered, such as gpt-value. |
x-halfoff-channel | That group's channel label, URL-encoded, such as Pooled%20capacity. |
x-halfoff-queued-ms | Present only if the call waited for capacity: the wait in milliseconds. |
The first three are listed in access-control-expose-headers, so client-side JavaScript can read them.
What you can't call#
The endpoints in the table above are the whole surface for models. In particular, these are not served:
- OpenAI: stored responses (
/v1/responses/{id}), legacy Completions, embeddings, audio, files and batches. The Responses API itself works for GPT models only. - Anthropic: Message Batches, Files, and fetching a single model by id.
- Gemini:
cachedContents(explicit context caching), file uploads,embedContentand the Live API.
To wire up a specific client, see Integrations.