# Threads and memory

> Keep a conversation on the server, continue it on any model, and read it back.

A thread is a conversation stored on the server, under your account. You send new messages; the server holds the history, rebuilds it for whichever model you name, and tells you the thread's id so you can continue it.

## Choosing a thread

The API takes the thread id from the `thread_id` field in the body or, when that is empty, from the `X-Thread-Id` request header. The header is a convenience for clients that can set a header but not add a field to the body, or whose request types reject unknown fields; the official OpenAI SDKs can do either (`extra_body` in Python, extra keys in TypeScript), so use whichever suits your code. When both are present, the body wins.

| You send | What happens |
|---|---|
| No thread id | A new thread is started. Its id comes back in the `X-Thread-Id` response header. |
| The id of one of your threads | That thread is continued. |
| An id that does not exist yet | A thread is created with that id. A client can generate its own id, a UUID for example, and start sending straight away. |
| An id that belongs to someone else | `404` `thread not found`. The API does not reveal whether the id exists. |

The same `404` is returned when the thread store cannot be reached while loading or creating a thread, including on a request that sent no id. It is not always permanent: if nothing about the id has changed, retry once before treating the thread as gone.

Threads belong to your account, not to a key: any of your keys can continue them.

## Send only the new turn

> **The mistake every integration makes once**
>
>
> Every message in `messages` is appended to the thread before the model is called. With a thread id, send only the message you are adding. Resend the transcript and every earlier message is recorded a second time, and the model sees the conversation twice.
>

Many chat front ends, the Vercel AI SDK among them, post the whole transcript on every turn. When a thread id is set, forward only the last message.

## System prompts

A `system` message at the very start of a thread is always kept, however long the thread grows. Send it once, in the first request. A `system` message sent later is stored as an ordinary turn, and can fall out of the window like any other.

## What the model sees

On every request the thread is rendered for the model you named and fitted to that model's context window:

- The newest turns are kept and the oldest dropped first. A turn is never cut in half.
- Room is held back for the reply: a quarter of the model's input window, up to 8,192 tokens.
- Sizes are estimates, about four characters per token for text and 800 tokens per image. The catalogue spans many tokenizers, and no single count is exact for all of them.
- The window comes from the model registry. For a model whose window is unknown, the thread is fitted to 8,192 tokens, which leaves 6,144 tokens, about 24,000 characters, for the conversation.

Because turns are fitted newest first and never split, a single turn larger than what is left of the window is dropped, and every older turn with it: the model is sent only the thread's head system prompt, if there is one, or no messages at all. There is no error for this; the model's answer to what it was sent, or the provider's error, comes back as usual and is saved. Keep any one message well inside the window of the smallest model you will use on the thread, and split a large paste across turns.

## Switching models

Change `model` between requests and keep the thread id. The whole thread is rendered afresh for the new model; nothing is replayed in the previous model's format.

A model that cannot accept images is not sent them. Each image is replaced by the text `[image attachment: no description available]`, so later turns that refer to it still read sensibly. Images do not get generated descriptions yet.

## When a call fails

Your messages are saved before the model is called. If the model call then fails, or a [routing](https://app.alphaneural.io/docs/routing) ladder is exhausted (`422`), your question is already in the thread and no answer is. To retry without adding it twice, send the same thread id with an empty `messages` array: the model is called with the thread as it stands.

Failures caught before the thread is touched save nothing: a missing or unknown key, a zero balance, invalid JSON, a missing `model`, or a thread that is not yours. A `500` while recording your messages can leave some of them in the thread and not others; [read the thread](#read-a-thread) before you retry.

## List your threads

`GET https://backend.alfnrl.io/v1/threads`

Your threads, most recently active first.

| Query parameter | |
|---|---|
| `limit` | How many to return. Default 50, at most 200. A negative or non-numeric value gets `400` `limit must be a non-negative integer`. |

```bash
curl "https://backend.alfnrl.io/v1/threads?limit=20" \
  -H "Authorization: Bearer $ALPHANEURAL_API_KEY"
```

```json
{
  "threads": [
    {
      "id": "7f6f0f3c-2f0b-4f1e-9d9e-2a6f0b1c3d4e",
      "title": "",
      "preview": "My name is Ada. Please remember it.",
      "last_model": "openrouter/anthropic/claude-haiku-4.5",
      "created_at": "2026-09-10T11:02:13.418Z",
      "updated_at": "2026-09-10T11:02:41.907Z"
    }
  ]
}
```

| Field | |
|---|---|
| `id` | The thread id. |
| `title` | Always empty today; nothing sets a title yet. Show `preview` instead. |
| `preview` | The first 80 characters of the thread's first user message, text only. |
| `last_model` | The model that wrote the most recent reply. |
| `created_at`, `updated_at` | RFC 3339 timestamps. |

## Read a thread

`GET https://backend.alfnrl.io/v1/threads/{thread_id}`

The whole transcript, in order. Nothing is dropped or fitted to a window: this is the thread as stored, not as a model would see it.

```json
{
  "id": "7f6f0f3c-2f0b-4f1e-9d9e-2a6f0b1c3d4e",
  "title": "",
  "preview": "My name is Ada. Please remember it.",
  "last_model": "openrouter/anthropic/claude-haiku-4.5",
  "created_at": "2026-09-10T11:02:13.418Z",
  "updated_at": "2026-09-10T11:02:41.907Z",
  "turns": [
    {
      "id": "2b0c6a51-0f3e-4c7e-9a43-8d1d6f0e9b27",
      "seq": 0,
      "role": "user",
      "content": [{"type": "text", "text": "My name is Ada. Please remember it."}],
      "created_at": "2026-09-10T11:02:13.420Z"
    },
    {
      "id": "c4e1f9a0-5d2b-4b8e-8f61-3a7e2c9d0b14",
      "seq": 1,
      "role": "assistant",
      "model": "openrouter/openai/gpt-4o-mini",
      "content": [{"type": "text", "text": "Nice to meet you, Ada. I'll remember that."}],
      "created_at": "2026-09-10T11:02:14.731Z"
    }
  ]
}
```

| Turn field | |
|---|---|
| `seq` | Position in the thread, from 0. |
| `role` | `system`, `user`, `assistant` or `tool`. |
| `model` | On assistant turns, the model that wrote it. Absent on your own turns. |
| `content` | Parts. A text part is `{"type": "text", "text": "..."}`; an image is `{"type": "image", "uri": "..."}`. |

An id that does not exist and an id that is not yours both get `404` `thread not found`.

## Limits

- There is no API to rename or delete a thread yet.
- Tool calls are not stored in threads. See [Chat completions](https://app.alphaneural.io/docs/chat-completions#limitations).
