# Chat completions

> POST /v1/chat/completions: the OpenAI request and response, plus the fields that add threads, routing and cost traces.

`POST https://backend.alfnrl.io/v1/chat/completions`

The OpenAI Chat Completions API, with the conversation kept on the server. Send the request you would send to OpenAI and you get the same response back; add a thread id and the conversation is kept for you.

## Example

```bash
curl https://backend.alfnrl.io/v1/chat/completions \
  -H "Authorization: Bearer $ALPHANEURAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openrouter/openai/gpt-4o-mini",
    "messages": [
      {"role": "system", "content": "Answer in one sentence."},
      {"role": "user", "content": "What is a context window?"}
    ],
    "temperature": 0.2
  }'
```

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://backend.alfnrl.io/v1",
    api_key=os.environ["ALPHANEURAL_API_KEY"],
)

completion = client.chat.completions.create(
    model="openrouter/openai/gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Answer in one sentence."},
        {"role": "user", "content": "What is a context window?"},
    ],
    temperature=0.2,
)
print(completion.choices[0].message.content)
```

```ts
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://backend.alfnrl.io/v1",
  apiKey: process.env.ALPHANEURAL_API_KEY,
});

const completion = await client.chat.completions.create({
  model: "openrouter/openai/gpt-4o-mini",
  messages: [
    { role: "system", content: "Answer in one sentence." },
    { role: "user", content: "What is a context window?" },
  ],
  temperature: 0.2,
});
console.log(completion.choices[0].message.content);
```

The body that comes back is the model's OpenAI-format completion, unchanged. The response also has an `X-Thread-Id` header naming the thread the exchange was saved to.

## Headers

| Header | | |
|---|---|---|
| `Authorization` | Required | `Bearer` followed by your API key. See [Authentication](https://app.alphaneural.io/docs/authentication). |
| `Content-Type` | Required | `application/json` |
| `X-Thread-Id` | Optional | The thread to continue, for clients that cannot add fields to the body. A `thread_id` in the body takes precedence. |

Every successful response, streamed or not, carries `X-Thread-Id`.

## Body

### Standard fields

| Field | Type | |
|---|---|---|
| `model` | string | Required. A model id from the [catalogue](https://app.alphaneural.io/models), such as `openrouter/openai/gpt-4o-mini`; the name of one of your deployments; or `alphaneural/auto` to let the server choose (see [Routing](https://app.alphaneural.io/docs/routing)). Without it: `400` `model is required`. |
| `messages` | array | The messages you are adding. With a thread, that is only the new turn; see [how messages are handled](#how-messages-are-handled). |
| `stream` | boolean | Stream the reply as server-sent events. See [Streaming](https://app.alphaneural.io/docs/streaming). |

Every other field you send is forwarded to the gateway unchanged: `temperature`, `top_p`, `max_tokens`, `tools`, `tool_choice`, `response_format`, `stop`, `seed` and provider-specific fields alike, with the two exceptions below. The gateway then drops any parameter the chosen provider does not support rather than refusing the request, so a `seed` or `logit_bias` sent to a model that cannot take it returns `200` with the parameter ignored, not an error. [`GET /v1/models`](https://app.alphaneural.io/docs/models) reports each model's `supported_parameters` where known.

### AlphaNeural fields

These are read by the API and are never forwarded to the model.

| Field | Type | |
|---|---|---|
| `thread_id` | string | The conversation to continue, or to start under this id. Omitted: a new thread. See [Threads and memory](https://app.alphaneural.io/docs/threads). |
| `task_id` | string | Groups requests into one task for a [cost trace](https://app.alphaneural.io/docs/traces). |
| `step_index` | integer | This request's position within the task. |
| `escalation` | object | A ladder of models and a JSON schema to check the answer against. See [Routing](https://app.alphaneural.io/docs/routing). Cannot be combined with `stream`, except with `alphaneural/auto`. |
| `max_spend` | number | A ceiling in US dollars, margin included, on what the request may be billed across every model tried. Used with `escalation` or `alphaneural/auto`. With your own ladder, `0` or omitted means no ceiling; with `alphaneural/auto`, omitted means the server's default and `0` is refused. |

A field of the wrong type, `step_index` sent as a string for example, makes the whole body invalid: `400` `invalid JSON body`.

## How messages are handled

Each message in `messages` is appended to the thread first. Then the thread, not your `messages` array, is what goes to the model: rendered for the model you named and fitted to its context window. Three things follow from that:

1. **`messages` is replaced.** The model receives the thread, which by then includes the messages you just sent.
2. **Send only the new turn.** Resending earlier messages appends them again. See [Threads and memory](https://app.alphaneural.io/docs/threads#send-only-the-new-turn).
3. **`metadata` is replaced when you send `task_id` or `step_index`.** The API sets `metadata` to those two values so the usage can be traced, and any `metadata` you sent in the same request is dropped. Without them, your `metadata` is forwarded as sent.

### Message content

`role` is `system`, `user`, `assistant` or `tool`. `content` is either a string or an array of parts, where each part is `{"type": "text", "text": "..."}` or `{"type": "image_url", "image_url": {"url": "..."}}`. Other part types are not supported on this endpoint.

`image_url.url` is stored exactly as sent. An `https` URL is a short reference; a `data:` URL carries the whole image in base64, and is stored in full, returned in full by [Read a thread](https://app.alphaneural.io/docs/threads#read-a-thread), and re-sent to every image-capable model on every later turn of the thread, while the context budget counts it as a flat 800 tokens. On a thread, send images by URL.

## Response

The model's response body is returned with status `200`, as the provider sent it, plus:

- the `X-Thread-Id` header;
- with `escalation` or `alphaneural/auto`, a top-level `escalation` object saying which models ran and what each cost, and `X-Escalation-*` headers repeating it. See [Routing](https://app.alphaneural.io/docs/routing#the-response).

If the model call itself fails, the provider's error comes back with its own status and body, unchanged. See [Errors](https://app.alphaneural.io/docs/errors).

## Limitations

- **Tool calling is single-step.** Tool definitions reach the model, and a reply containing `tool_calls` comes back intact. But a thread keeps only each message's `role` and `content`: an assistant message's `tool_calls` and a tool message's `tool_call_id` are not stored, so the follow-up request that returns tool results is not one a provider accepts. A reply made only of tool calls adds nothing to the thread. Run multi-step tool loops against the [raw gateway](https://app.alphaneural.io/docs/gateway).
- **Only the first choice is saved.** With `n` above 1 you receive every choice, but only `choices[0]` is added to the thread.
- **Every call is saved.** Without a thread id, each request starts and stores a new thread. If you do not want a conversation kept as a thread, call the [raw gateway](https://app.alphaneural.io/docs/gateway), which saves no thread; it does keep its own usage logs for metering, so that is not the same as nothing being stored. There is no API to delete a thread yet.
