---
title: OpenAI Chat Completions
description: Call GPT, Claude and Gemini models through the OpenAI Chat Completions API with the official OpenAI SDKs. Covers streaming with usage, tools, reasoning models and what is not supported.
---

Lovely Router serves `POST /v1/chat/completions` as OpenAI defines it. Point the OpenAI SDK at `https://lovelyrouter.com/v1` and use your Lovely Router key. The request body passes through unchanged, so [OpenAI's API reference](https://platform.openai.com/docs/api-reference/chat/create) applies to every parameter.

## Set up the SDK

```bash title="cURL"
curl https://lovelyrouter.com/v1/chat/completions \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-6.1-sol",
    "messages": [
      {"role": "system", "content": "Answer in one sentence."},
      {"role": "user", "content": "What does a load balancer do?"}
    ]
  }'
```

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

client = OpenAI(
    base_url="https://lovelyrouter.com/v1",
    api_key=os.environ["LOVELY_API_KEY"],
)
response = client.chat.completions.create(
    model="gpt-6.1-sol",
    messages=[
        {"role": "system", "content": "Answer in one sentence."},
        {"role": "user", "content": "What does a load balancer do?"},
    ],
)
print(response.choices[0].message.content)
```

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

const client = new OpenAI({
  baseURL: 'https://lovelyrouter.com/v1',
  apiKey: process.env.LOVELY_API_KEY,
});
const response = await client.chat.completions.create({
  model: 'gpt-6.1-sol',
  messages: [
    { role: 'system', content: 'Answer in one sentence.' },
    { role: 'user', content: 'What does a load balancer do?' },
  ],
});
console.log(response.choices[0].message.content);
```

The SDKs also read `OPENAI_BASE_URL` and `OPENAI_API_KEY`, so most OpenAI-compatible apps only need those two settings.

## Available models

Every text model in [`GET /v1/models`](/docs/models/pricing) currently accepts Chat Completions, including Claude and Gemini models. Image models (`gpt-image-*`) use the [Images API](/docs/models/images). That makes this API the simplest way to switch between vendors with one client.

| Family | Example ids | Notes |
| --- | --- | --- |
| GPT | `gpt-6.1-sol`, `gpt-6-astra`, `gpt-5.6-terra`, `gpt-5.6-luna` | Native API. |
| Claude | `claude-sonnet-5.5`, `claude-opus-5.5` | Use [Anthropic Messages](/docs/models/anthropic) for `cache_control` and thinking settings. |
| Gemini | `gemini-3.1-pro`, `gemini-3-flash` | Use the [Gemini API](/docs/models/gemini) for `thinkingConfig` and other Gemini-only options. |

Vendor-specific options are only certain to work in the model's native API. See [Which API each model speaks](/docs/models#which-api-each-model-speaks).

## Stream with usage

Set `stream: true`. The gateway always adds `stream_options.include_usage: true`, so the last chunk before `data: [DONE]` has an empty `choices` array and the call's `usage`. Check that `choices` is non-empty before reading a delta.

```python title="Python"
stream = client.chat.completions.create(
    model="gpt-6.1-sol",
    messages=[{"role": "user", "content": "Name five prime numbers."}],
    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: 'Name five prime numbers.' }],
  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');
}
```

See [Streaming](/docs/models/streaming) for the wire format and how errors appear mid-stream.

## Call functions

Describe functions in `tools`. When the model calls one, `finish_reason` is `tool_calls`; run the function and reply with a `tool` message that carries the same `tool_call_id`.

```python title="Python"
import json

tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Look up an order by id.",
        "parameters": {
            "type": "object",
            "properties": {"order_id": {"type": "string"}},
            "required": ["order_id"],
        },
    },
}]
messages = [{"role": "user", "content": "Where is order A-1042?"}]

response = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
msg = response.choices[0].message
if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = {"order_id": args["order_id"], "status": "shipped"}  # your code here
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
    response = client.chat.completions.create(model="gpt-6.1-sol", messages=messages, tools=tools)
print(response.choices[0].message.content)
```

```js title="Node.js"
const tools = [{
  type: 'function',
  function: {
    name: 'get_order_status',
    description: 'Look up an order by id.',
    parameters: {
      type: 'object',
      properties: { order_id: { type: 'string' } },
      required: ['order_id'],
    },
  },
}];
const messages = [{ role: 'user', content: 'Where is order A-1042?' }];

let response = await client.chat.completions.create({ model: 'gpt-6.1-sol', messages, tools });
const msg = response.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 = { order_id: args.order_id, status: 'shipped' }; // your code here
    messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
  }
  response = await client.chat.completions.create({ model: 'gpt-6.1-sol', messages, tools });
}
console.log(response.choices[0].message.content);
```

`tool_choice`, `parallel_tool_calls` and `response_format` pass through unchanged.

## Use reasoning models

GPT-5 and GPT-6 models reason before they answer. Control how much with `reasoning_effort`, and cap total output with `max_completion_tokens`, which includes reasoning tokens. Accepted effort values depend on the model; see [OpenAI's reasoning guide](https://platform.openai.com/docs/guides/reasoning).

```python title="Python"
response = client.chat.completions.create(
    model="gpt-6.1-sol",
    reasoning_effort="high",
    max_completion_tokens=8000,
    messages=[{"role": "user", "content": "Find the bug: for i in range(len(a)): a.pop(i)"}],
)
usage = response.usage
print(response.choices[0].message.content)
print("reasoning tokens:", usage.completion_tokens_details.reasoning_tokens)
```

```js title="Node.js"
const response = await client.chat.completions.create({
  model: 'gpt-6.1-sol',
  reasoning_effort: 'high',
  max_completion_tokens: 8000,
  messages: [{ role: 'user', content: 'Find the bug: for i in range(len(a)): a.pop(i)' }],
});
console.log(response.choices[0].message.content);
console.log('reasoning tokens:', response.usage.completion_tokens_details?.reasoning_tokens);
```

The reasoning itself is not returned in Chat Completions. Its tokens are counted in `usage.completion_tokens` and billed at the output rate.

## Use the Responses API {#responses-api}

GPT models are also served over the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) at `POST /v1/responses`, the API that Codex CLI and the Agents SDK use. Use the same base URL and key; the SDK's `client.responses.create()` works as is.

```bash title="cURL"
curl https://lovelyrouter.com/v1/responses \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-6.1-sol",
    "instructions": "Answer in one sentence.",
    "input": "What is a router?"
  }'
```

```python title="Python"
response = client.responses.create(
    model="gpt-6.1-sol",
    instructions="Answer in one sentence.",
    input="What is a router?",
)
print(response.output_text)
```

```js title="Node.js"
const response = await client.responses.create({
  model: 'gpt-6.1-sol',
  instructions: 'Answer in one sentence.',
  input: 'What is a router?',
});
console.log(response.output_text);
```

With `stream: true` you get the API's typed events (`response.created`, `response.output_text.delta`, …, `response.completed`); usage arrives in `response.completed` and is billed like any other call. Things to know:

- **Send the whole conversation.** Lovely Router doesn't store responses for you: `GET`, `DELETE` and cancel on `/v1/responses/{id}` return `404`, and a `previous_response_id` only works while the conversation stays on the same channel. Send the full history in `input` with `"store": false`, as Codex does.
- **GPT models only.** Other models return `404` with `not served in responses format`; call them with Chat Completions or their own API.
- **`instructions` in the response** shows what you sent, or `null` if you sent none.
- `prompt_cache_key` keeps a conversation on one channel, exactly as with Chat Completions.

## Prompt caching

OpenAI models cache long prompt prefixes automatically. Cached tokens show up in `usage.prompt_tokens_details.cached_tokens` and are billed at the cheaper cache-read rate. Setting `prompt_cache_key` improves hit rates and also tells the gateway which requests belong to one conversation. See [Prompt caching](/docs/models/caching#openai).

## What is not supported

Chat Completions, the [Responses API](#responses-api) and the [Images API](/docs/models/images) are available in OpenAI format. These endpoints return `404`:

- **Stored responses** (`GET`, `DELETE` or cancel on `/v1/responses/{id}`).
- **Embeddings** (`/v1/embeddings`).
- **Audio** (`/v1/audio/*`) and image variations (`/v1/images/variations`).
- **Legacy Completions, Assistants, files, batches, fine-tuning and moderation.**

`GET /v1/models` works with the OpenAI SDK's `client.models.list()`. Retrieving a single model by id does not.

## Headers

| Header | Notes |
| --- | --- |
| `Authorization: Bearer` | Your Lovely Router key. Required. |
| `openai-beta` | Forwarded as sent. |
| `x-lovely-group` | Optional. Pins the request to a [channel group](/docs/models/channel-groups) when your key is not bound to one. |
| `x-session-id` | Optional. Groups requests into one conversation for [cache-aware routing](/docs/models/caching#send-an-explicit-session-id). `prompt_cache_key` or `user` in the body does the same. |

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