OpenAI Chat Completions
Use the OpenAI SDK you already have to call GPT, Claude and Gemini at half price. Streaming usage, function calling, reasoning and the Responses API.
Any code written for the OpenAI SDK can run on Half Off Dev by changing the base URL to https://halfoff.dev/v1 and the key to yours. POST /v1/chat/completions follows OpenAI's definition and the body is forwarded as is, so OpenAI's API reference applies field for field. You pay half the list price for every token, and the same client can reach Claude and Gemini too.
Switch the client over#
curl https://halfoff.dev/v1/chat/completions \
-H "Authorization: Bearer $HALFOFF_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "gpt-6.1-sol",
"messages": [
{"role": "system", "content": "You are a terse SQL tutor."},
{"role": "user", "content": "When should I use a LEFT JOIN?"}
]
}'Because the SDKs read OPENAI_BASE_URL and OPENAI_API_KEY, most apps that speak the OpenAI protocol only need those two values set. OpenAI-compatible apps lists a few.
Which models you can call here#
All text models in GET /v1/models accept Chat Completions today, Claude and Gemini included. That makes this the easiest route if you want one client for every vendor.
| Family | Example ids | Good to know |
|---|---|---|
| GPT | gpt-6.1-sol, gpt-6-astra, gpt-5.6-terra, gpt-5.6-luna | Home format. |
| Claude | claude-sonnet-5.5, claude-opus-5.5 | For cache_control or thinking settings, call them through Anthropic Messages. |
| Gemini | gemini-3.1-pro, gemini-3-flash | For thinkingConfig and other Gemini-specific fields, use the Gemini API. |
Options that belong to one vendor are only guaranteed in that vendor's format. See native format per model family.
Streaming, with usage at the end#
Turn on stream: true. The gateway always adds stream_options.include_usage: true, which means the final chunk before data: [DONE] has an empty choices array and the call's usage. Guard against empty choices before you read a delta.
stream = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "List the planets in order from the sun."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\nTokens:", chunk.usage.prompt_tokens, "in,", chunk.usage.completion_tokens, "out")The exact wire format and how failures show up mid-stream are in Streaming.
Function calling#
List your functions in tools. When the model wants one, finish_reason becomes tool_calls; execute it and send back a tool message with the matching tool_call_id.
import json
tools = [{
"type": "function",
"function": {
"name": "check_stock",
"description": "Return how many units of a product are in stock.",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
},
},
}]
messages = [{"role": "user", "content": "Do we still have SKU KB-204 in stock?"}]
completion = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
msg = completion.choices[0].message
if msg.tool_calls:
messages.append(msg)
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = {"sku": args["sku"], "units": 17} # your code here
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
completion = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
print(completion.choices[0].message.content)tool_choice, parallel_tool_calls and response_format are forwarded without changes.
Reasoning effort#
GPT-5 and GPT-6 models think before answering. Set how hard with reasoning_effort, and bound the total output, reasoning included, with max_completion_tokens. The allowed effort values vary by model; OpenAI's reasoning guide has them.
completion = client.chat.completions.create(
model="gpt-6.1-sol",
reasoning_effort="high",
max_completion_tokens=8000,
messages=[{"role": "user", "content": "Why does 0.1 + 0.2 != 0.3 in most languages?"}],
)
print(completion.choices[0].message.content)
print("reasoning tokens:", completion.usage.completion_tokens_details.reasoning_tokens)Chat Completions does not return the reasoning text. Its tokens are included in usage.completion_tokens and billed at the output rate.
The Responses API#
For GPT models, POST /v1/responses serves the OpenAI Responses API, which is what Codex CLI speaks. Same base URL, same key; client.responses.create() needs no changes.
curl https://halfoff.dev/v1/responses \
-H "Authorization: Bearer $HALFOFF_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "gpt-6.1-sol",
"instructions": "Reply with a single word.",
"input": "What is the opposite of latency-bound?"
}'Streaming (stream: true) delivers the API's typed events, from response.created through response.output_text.delta to response.completed; the usage in response.completed is what gets billed. Points to keep in mind:
- Nothing is stored server-side.
GET,DELETEand cancel on/v1/responses/{id}return404, andprevious_response_idonly works while the conversation happens to stay on one channel. Send the full history ininputwith"store": false, as Codex does. - GPT only. Any other model answers
404withnot served in responses format; use Chat Completions or its native API instead. instructionsin the response echoes what you sent, ornullwhen you sent none.prompt_cache_keykeeps a conversation on one channel, as in Chat Completions.
Prompt caching#
OpenAI models cache long prompt prefixes without being asked. The cached part is reported in usage.prompt_tokens_details.cached_tokens and billed at the lower cache-read rate. A prompt_cache_key raises the hit rate and also tells the gateway which requests form one conversation. See Prompt caching.
Not supported#
Chat Completions and the Responses API are the OpenAI-format endpoints. The following answer 404:
- Stored responses:
GET,DELETEor cancel on/v1/responses/{id}. - Embeddings:
/v1/embeddings. - Audio:
/v1/audio/*. - Legacy Completions, Assistants, files, batches, fine-tuning and moderation.
client.models.list() works against GET /v1/models; fetching one model by id does not.
Request headers#
| Header | Behavior |
|---|---|
Authorization: Bearer | Your Half Off Dev key. Required. |
openai-beta | Forwarded unchanged. |
x-halfoff-group | Optional. Picks the channel group for this request, unless the key is already bound to one. |
x-session-id | Optional. Marks requests as one conversation for cache-aware routing. prompt_cache_key or user in the body has the same effect. |
Errors use OpenAI's shape, {"error": {"message": "...", "type": "...", "code": "..."}}; see Errors.