---
title: OpenAI Chat Completions
description: 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](https://platform.openai.com/docs/api-reference/chat/create) 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 {#connect}

```bash title="cURL"
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?"}
    ]
  }'
```

```python title="Python"
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://halfoff.dev/v1",
    api_key=os.environ["HALFOFF_API_KEY"],
)
completion = client.chat.completions.create(
    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?"},
    ],
)
print(completion.choices[0].message.content)
```

```js title="Node.js"
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://halfoff.dev/v1',
  apiKey: process.env.HALFOFF_API_KEY,
});
const completion = await client.chat.completions.create({
  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?' },
  ],
});
console.log(completion.choices[0].message.content);
```

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](/docs/integrations/openai-compatible) lists a few.

## Which models you can call here {#models}

All text models in [`GET /v1/models`](/docs/models/pricing) 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](/docs/models/anthropic). |
| Gemini | `gemini-3.1-pro`, `gemini-3-flash` | For `thinkingConfig` and other Gemini-specific fields, use the [Gemini API](/docs/models/gemini). |

Options that belong to one vendor are only guaranteed in that vendor's format. See [native format per model family](/docs/models#native-formats).

## Streaming, with usage at the end {#streaming}

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.

```python title="Python"
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")
```

```js title="Node.js"
const stream = await client.chat.completions.create({
  model: 'gpt-6.1-sol',
  messages: [{ role: 'user', content: 'List the planets in order from the sun.' }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
  if (chunk.usage) console.log('\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](/docs/models/streaming).

## Function calling {#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`.

```python title="Python"
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)
```

```js title="Node.js"
const 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'],
    },
  },
}];
const messages = [{ role: 'user', content: 'Do we still have SKU KB-204 in stock?' }];

let completion = await client.chat.completions.create({ model: 'gpt-6.1-sol', messages, tools });
const msg = completion.choices[0].message;
if (msg.tool_calls) {
  messages.push(msg);
  for (const call of msg.tool_calls) {
    const args = JSON.parse(call.function.arguments);
    const result = { sku: args.sku, units: 17 }; // your code here
    messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
  }
  completion = await client.chat.completions.create({ model: 'gpt-6.1-sol', messages, tools });
}
console.log(completion.choices[0].message.content);
```

`tool_choice`, `parallel_tool_calls` and `response_format` are forwarded without changes.

## Reasoning effort {#reasoning}

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](https://platform.openai.com/docs/guides/reasoning) has them.

```python title="Python"
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)
```

```js title="Node.js"
const completion = await 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?' }],
});
console.log(completion.choices[0].message.content);
console.log('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 {#responses-api}

For GPT models, `POST /v1/responses` serves the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses), which is what Codex CLI speaks. Same base URL, same key; `client.responses.create()` needs no changes.

```bash title="cURL"
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?"
  }'
```

```python title="Python"
response = client.responses.create(
    model="gpt-6.1-sol",
    instructions="Reply with a single word.",
    input="What is the opposite of latency-bound?",
)
print(response.output_text)
```

```js title="Node.js"
const response = await client.responses.create({
  model: 'gpt-6.1-sol',
  instructions: 'Reply with a single word.',
  input: 'What is the opposite of latency-bound?',
});
console.log(response.output_text);
```

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`, `DELETE` and cancel on `/v1/responses/{id}` return `404`, and `previous_response_id` only works while the conversation happens to stay on one channel. Send the full history in `input` with `"store": false`, as Codex does.
- **GPT only.** Any other model answers `404` with `not served in responses format`; use Chat Completions or its native API instead.
- **`instructions` in the response** echoes what you sent, or `null` when you sent none.
- **`prompt_cache_key`** keeps a conversation on one channel, as in Chat Completions.

## Prompt caching {#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](/docs/models/caching#openai).

## Not supported {#not-supported}

Chat Completions and the [Responses API](#responses-api) are the OpenAI-format endpoints. The following answer `404`:

- **Stored responses**: `GET`, `DELETE` or 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 {#headers}

| Header | Behavior |
| --- | --- |
| `Authorization: Bearer` | Your Half Off Dev key. Required. |
| `openai-beta` | Forwarded unchanged. |
| `x-halfoff-group` | Optional. Picks the [channel group](/docs/models/channel-groups) for this request, unless the key is already bound to one. |
| `x-session-id` | Optional. Marks requests as one conversation for [cache-aware routing](/docs/models/caching#explicit-session-id). `prompt_cache_key` or `user` in the body has the same effect. |

Errors use OpenAI's shape, `{"error": {"message": "...", "type": "...", "code": "..."}}`; see [Errors](/docs/api/errors).
