> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memory.anonalabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat

> An OpenAI-compatible chat endpoint that automatically recalls and stores memory.

`POST /v1/chat/completions` is an OpenAI-compatible chat endpoint with memory built in.
On every request Anona retrieves the most relevant memories from the space, injects them
into the prompt, and stores the exchange back, so the conversation stays grounded in
what came before without any extra calls.

Point an existing OpenAI-compatible client at the Anona base URL and pick a space.

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/chat/completions \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Anona-Space-Id: support-bot" \
  -d '{"messages": [{"role": "user", "content": "What plan did I say I was on?"}]}'
```

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    api_key="anona_live_YOUR_KEY",
    base_url="https://api.anonalabs.com/v1",
    default_headers={"X-Anona-Space-Id": "support-bot"},
)

reply = client.chat.completions.create(
    model="gpt-oss",
    messages=[{"role": "user", "content": "What plan did I say I was on?"}],
)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "anona_live_YOUR_KEY",
  baseURL: "https://api.anonalabs.com/v1",
  defaultHeaders: { "X-Anona-Space-Id": "support-bot" },
});

const reply = await client.chat.completions.create({
  model: "gpt-oss",
  messages: [{ role: "user", content: "What plan did I say I was on?" }],
});
```

Your own OpenAI SDK, your own request shape — only the base URL and the key change.

> [!NOTE]
>
> `model` is optional on the wire — the cURL call above omits it and gets
> whatever this deployment serves. The OpenAI Python SDK requires it client-side,
> though: omit it and `create()` raises `TypeError: Missing required arguments`
> before any request leaves the process. So name a model when you call through an
> SDK — `gpt-oss` is the short name of what this deployment currently serves, and
> [`GET /v1/models`](https://docs.memory.anonalabs.com/api-reference/model-selection) marks it `default: true`.

## Request

```bash theme={null}
POST https://api.anonalabs.com/v1/chat/completions
Authorization: Bearer anona_live_...
Content-Type: application/json
```

```json theme={null}
{
  "model": "gpt-oss",
  "space_id": "support-bot",
  "messages": [
    { "role": "user", "content": "What plan did I say I was on?" }
  ],
  "memory_limit": 5,
  "temperature": 0.7,
  "max_tokens": 512
}
```

| Field | Type | Default | Description |
| - | - | - | - |
| `messages` | array | required | Standard chat messages, each with `role` and `content`. |
| `model` | string | space default, then deployment default | Optional. Omit it to use whatever model this deployment serves; that is the recommended default. Name a tier (`fast`, `balanced`) or a model id to choose, and credits are charged at that model's rate. See [model selection](https://docs.memory.anonalabs.com/api-reference/model-selection.md). Every model is selectable on every plan. A model this deployment does not serve returns `400 unsupported_model`, listing what it does. |
| `stream` | boolean | `false` | Stream the reply as server-sent events. See [Streaming](#streaming). |
| `stream_options` | object | none | Streaming only. `{"include_usage": true}` appends the final usage chunk, exactly as on OpenAI. Omit it and no usage chunk is sent. |
| `max_tokens` | integer | none | Optional. |
| `temperature` | float | none | Optional. |

Fields Anona does not model (`top_p`, `n`, `stop`, `presence_penalty` and the rest of
the OpenAI request) are forwarded to the provider unchanged.

## Memory tunables

Every knob can be set in the body **or** as a header. The header form exists so a stock
client can set them once in `default_headers` and leave every call site alone; when both
are present, the body wins.

| Body field | Header | Type | Default | Description |
| - | - | - | - | - |
| `space_id` | `X-Anona-Space-Id` | string | `"default"` | Which space to recall from and store into. Omit it and the turn lands in a space literally named `default`, created on the first proxied turn if it does not exist yet, so a space you never created appears in your space list and in the dashboard. Set it once in `default_headers` to keep products apart. |
| `user_id` | `X-Anona-User-Id` | string | none | Scope the turn to one end user: it recalls only that user's memories and stores the turn under them. One space, many users. See [Scoping](https://docs.memory.anonalabs.com/api-reference/retrieve.md#scoping-within-a-space). |
| `agent_id` | `X-Anona-Agent-Id` | string | none | Scope the turn to one agent. |
| `session_id` | `X-Anona-Session-Id` | string | none | Scope the turn to one conversation or run. |
| `memory_limit` | `X-Anona-Memory-Limit` | integer, 1 to 20 | `5` | How many memories to inject. |
| `memory_token_budget` | `X-Anona-Memory-Tokens` | integer, 1 to 100,000 | none | Approximate token cap for the injected block. Whole memories are dropped, lowest-ranked first, until it fits; the text is never truncated mid-sentence. The count is an estimate, not a tokenizer. |
| `auto_record` | `X-Anona-Auto-Record` | boolean | `true` | Store this turn back into the space. Set `false` for read-only turns, such as evaluating against a space without polluting it. |
| `memory` | `X-Anona-Memory` | boolean | `true` | Set `false` to skip recall and injection entirely for one call. |
| `block_order` | `X-Anona-Block-Order` | `"relevance"` or `"stable"` | `"relevance"` | How the injected memory block is ordered. `"stable"` keeps your provider's prompt cache warm. See [Prompt caching](#prompt-caching) below. |
| `receipt` | `X-Anona-Receipt` | boolean | `false` | Build a fetchable [context receipt](https://docs.memory.anonalabs.com/api-reference/context-receipts.md) for this call. The response echoes the id back as `X-Anona-Receipt-Id`. |

A header that cannot be parsed returns `400 invalid_tunable` naming the header.

## Prompt caching

Every major provider discounts a prompt prefix it has already seen: Anthropic by
around 90%, OpenAI by around 50%. That discount matches on an **unchanged prefix**, so
it survives exactly as far into your prompt as the first byte that differs from last
time.

By default the memory block is ordered by relevance. Relevance is a property of *this
turn's question*, so the same memories come back in a different order on the next turn,
the block's first line changes, and the discount ends before your memories begin, and
because the block is prepended, before your own system prompt too.

`block_order: "stable"` changes three things together:

* **Ordered by record time**, not by score. A property of the memory rather than of the
  question, so an unchanged set of memories renders identically two turns running, and a
  newly recorded memory appends at the tail.
* **Bulleted, not numbered.** A numbered list renumbers every line below a memory that
  drops out of this turn's recall, so one dropout near the top rewrites the whole block.
* **Placed after your system message** instead of before it, so your own instructions,
  usually the most static text in the prompt, are the prefix.

```python Python theme={null}
client = OpenAI(
    base_url="https://api.anonalabs.com/v1",
    api_key="anona_live_YOUR_KEY",
    default_headers={
        "X-Anona-Space-Id": "support-bot",
        "X-Anona-Block-Order": "stable",
    },
)
```

```typescript TypeScript theme={null}
const client = new OpenAI({
  baseURL: "https://api.anonalabs.com/v1",
  apiKey: "anona_live_YOUR_KEY",
  defaultHeaders: {
    "X-Anona-Space-Id": "support-bot",
    "X-Anona-Block-Order": "stable",
  },
});
```

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/chat/completions \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "What plan am I on?"}],
    "block_order": "stable"
  }'
```

> [!NOTE]
>
> Stable order protects the **prefix**, which means it protects memories older than any
> that changed. If a memory drops out of recall, everything above it in record order still
> matches; everything below it does not. Adding a memory is free; it lands at the tail.

What it cannot do is make an unstable recall stable. If a turn recalls a completely
different set of memories, there is no ordering that keeps the block the same. That is
why we measure it rather than assert it: your dashboard's **Usage** page reports how many
of the tokens we injected were actually unchanged since your previous turn. The
measurement runs in both modes, so you can compare.

Two honest limits on those numbers. They are `len/4` estimates, not tokenizer counts. And
an unchanged prefix is what makes a cache hit *possible*. Whether your provider charged
the discounted rate also depends on their minimum cacheable length and their own cache
TTL, neither of which we can see. We report eligibility, never a saving.

### Space defaults

The same knobs can be stored on the space, so they apply to every proxied call
without touching a call site. Resolution is per field, in this order:

```
request body  →  request header  →  space default  →  platform default
```

| Method | Path | Description |
| - | - | - |
| `GET` | `/v1/spaces/{space_id}/chat-settings` | The space's stored defaults. An unconfigured space returns all nulls, not a 404. |
| `PUT` | `/v1/spaces/{space_id}/chat-settings` | Replace them. A field left out of the body is **cleared**, not kept. |
| `DELETE` | `/v1/spaces/{space_id}/chat-settings` | Remove every override. Idempotent. |

```bash cURL theme={null}
curl -X PUT https://api.anonalabs.com/v1/spaces/support-bot/chat-settings \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"memory_limit": 12, "auto_record": false}'
```

```python Python theme={null}
from anona import AnonaClient

anona = AnonaClient(api_key="anona_live_YOUR_KEY")

anona.set_chat_settings(
    space_id="support-bot",
    memory_limit=12,
    auto_record=False,
)
```

```typescript TypeScript theme={null}
import { Anona } from "@anona-labs/memory";

const anona = new Anona({ apiKey: "anona_live_YOUR_KEY" });

await anona.setChatSettings({
  spaceId: "support-bot",
  memoryLimit: 12,
  autoRecord: false,
});
```

Remember the `PUT` is a full replace in every language: a field left out is cleared.

```json theme={null}
{
  "space_id": "support-bot",
  "memory_limit": 12,
  "memory_token_budget": null,
  "auto_record": false,
  "memory": null,
  "chat_model": null
}
```

`chat_model` sets which LLM this space's proxied calls run on: a tier name or a
model id, stored resolved. A request that names its own `model` still wins. See
[model selection](https://docs.memory.anonalabs.com/api-reference/model-selection.md).

`space_id` is not a settable default: it is the key the defaults are stored
under. A `null` means *unset*, not "off": that field falls through to the
platform default, so a space can configure one knob and inherit the rest.

`block_order` is not a space default either. It belongs to a client and its prompt
rather than to a space, and the header form already sets it once per client.

These endpoints are free, cost no credits, and remain available when an
organization has run out of credits, because configuration should never be the thing
you cannot change.

## Response

OpenAI-shaped, plus two Anona fields.

```json theme={null}
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "openai.gpt-oss-120b-1:0",
  "choices": [ { "index": 0, "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" } ],
  "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 },
  "memories_injected": 3,
  "space_id": "support-bot"
}
```

| Field | Description |
| - | - |
| `memories_injected` | How many memories were pulled into this turn's prompt. |
| `space_id` | The space the turn was recalled from and stored into. |

The same two values are also returned as the `X-Anona-Memories-Injected` and
`X-Anona-Space-Id` response headers, which is how you read them on a stream.

`choices[].finish_reason` says why the reply stopped. It is normalised, not passed
through: every provider's own stop reason is mapped onto OpenAI's four values before
it reaches you, in streaming and non-streaming replies alike, so it is safe to
branch on.

| Value | Meaning |
| - | - |
| `stop` | The model finished on its own. Also what an unrecognised provider reason falls back to, since a value outside the enum breaks a typed client. |
| `length` | Cut off at the token limit. This is the one case the field exists to warn you about — `end_turn` and `max_tokens` from the provider both arrive here. |
| `tool_calls` | Stopped to call a tool. |
| `content_filter` | Stopped by a safety or guardrail filter. |

[`/v1/messages`](https://docs.memory.anonalabs.com/api-reference/messages) is the exception: it reports Anthropic's own
`stop_reason` vocabulary (`end_turn`, `max_tokens`, `tool_use`) instead, because it is
wire-compatible with the Anthropic SDK.

The user turn and the assistant reply are both stored back into `space_id`
automatically, unless `auto_record` is `false`.

## Streaming

Send `"stream": true` and the reply arrives as server-sent events in the standard
OpenAI chunk format, terminated by `data: [DONE]`.

```bash cURL theme={null}
curl -N https://api.anonalabs.com/v1/chat/completions \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Anona-Space-Id: support-bot" \
  -d '{
    "messages": [{"role": "user", "content": "What plan am I on?"}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'
```

```python Python theme={null}
stream = client.chat.completions.create(
    model="gpt-oss",
    messages=[{"role": "user", "content": "What plan am I on?"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")
```

```typescript TypeScript theme={null}
const stream = await client.chat.completions.create({
  model: "gpt-oss",
  messages: [{ role: "user", content: "What plan am I on?" }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```

Memory recall happens before the model is called, so injection is unaffected by
streaming. The first chunk carries the `memories_injected` and `space_id` fields
alongside the usual chunk payload, and both are on the response headers from the start.

The turn is recorded and billed after the last token, using the token counts from the
final chunk. If the connection drops mid-stream, whatever the model already produced is
still recorded and still billed, from an estimate of the text that was sent, since the
counts travel in a chunk that never arrived.

Token counts reach *you* only if you ask for them, as on OpenAI: set
`"stream_options": {"include_usage": true}` and the stream ends with a usage chunk.
Billing does not depend on it.

> [!NOTE]
>
> Errors raised before the first token (an unavailable model, an unreachable provider)
> come back as a normal JSON error with a real status code. Once the stream has started
> the status is already sent, so a later failure ends the stream early instead.

## Other request shapes

| Shape | Endpoint |
| - | - |
| OpenAI Chat Completions | `/v1/chat/completions` (this page) |
| OpenAI Responses | [`/v1/responses`](https://docs.memory.anonalabs.com/api-reference/responses) |
| Anthropic Messages | [`/v1/messages`](https://docs.memory.anonalabs.com/api-reference/messages) |

All three run the identical memory wrapper and accept the same tunables.

> [!NOTE]
>
> You never supply an LLM key. Anona runs the model for you, and billing is metered
> in Anona credits rather than by the underlying provider. `model` chooses from what
> this deployment serves: omit it for the default, or name one from
> [model selection](https://docs.memory.anonalabs.com/api-reference/model-selection.md). A model this deployment does not
> serve returns `400 unsupported_model`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.