# Twigg: API reference

Base URL: https://api.twigg.ai. Customer API operations under `/api/v1` require `Authorization: Bearer <api key>`. Public catalogue operations under `/v1/catalogue` and the updates feed at `/api/v1/updates` require no API key.

Start at [API discovery](https://api.twigg.ai/) for links to this reference and the [OpenAPI specification](https://api.twigg.ai/openapi.json). Discovery and version information are public.

For system prompts, saved tools, context budgets and retention settings, see [Context control](https://twigg.ai/docs/context#configure-prompts-tools-and-context).

## Endpoints

## Core API

Create conversations, send turns, read history and discover models.

| Method | Path | Authentication | Purpose |
|---|---|---|---|
| GET | `/api/v1/chats` | API key | List chats |
| POST | `/api/v1/chats` | API key | Create a chat |
| PUT | `/api/v1/chats/external/{external_id}` | API key | Create or get a chat by external id |
| DELETE | `/api/v1/chats/{chat_id}` | API key | Delete a chat |
| GET | `/api/v1/chats/{chat_id}` | API key | Get a chat |
| GET | `/api/v1/chats/{chat_id}/history` | API key | Read chat history |
| POST | `/api/v1/chats/{chat_id}/responses` | API key | Submit a turn and get the answer |
| POST | `/api/v1/chats/external/{external_id}/responses` | API key | Submit a turn by external id |
| GET | `/v1/catalogue` | None | List public model prices |
| GET | `/v1/catalogue/{name}` | None | Get public model prices |
| GET | `/v1/catalogue/models.md` | None | Read model prices as Markdown |

## listChats

### GET /api/v1/chats

List chats newest first. Use after_id or before_id to page; they are mutually exclusive. Unknown query parameters are ignored.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no | Scope to a namespace **and everything beneath it**. `acme/proj-7` returns that scope's chats plus `acme/proj-7/user-99`'s and deeper. Absent lists the whole organisation. A namespace with no chats is not an error — there is no registry, so "empty" and "never used" are the same state and both return `[]`. |
| `after_id` | query | [ChatId](#chatid) | no | Page **older** than this id. Listings run newest-first, so "after" means further down the list, which is further back in time. |
| `before_id` | query | [ChatId](#chatid) | no | Page **newer** than this id. |
| `limit` | query | integer (int64) | no | 1–100. Out-of-range values are clamped. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ChatPage](#chatpage) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: listChats

```http
GET /api/v1/chats?namespace=acme%2Fproj-7&limit=20
Authorization: Bearer <api key>

200 →
{
  "data": [
    {
      "id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
      "namespace": "acme/proj-7",
      "title": "Support thread",
      "description": null,
      "external_id": null,
      "user_metadata": {},
      "created_at": "2026-09-06T10:24:00Z",
      "length": 42,
      "updated_at": "2026-09-06T11:02:13Z"
    }
  ],
  "first_id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "last_id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "has_more": false
}
```

## createChat

### POST /api/v1/chats

Create an empty chat in the authenticated organisation. All request fields are optional. An external_id another live chat in the organisation already holds returns 409 external_id_taken; use PUT /api/v1/chats/external/{external_id} to create or get by external id.

Authentication: Bearer API key required.

Request body: [CreateChatRequest](#createchatrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `description` | null or [ChatDescription](#chatdescription) | no |  |
| `external_id` | null or [ExternalChatId](#externalchatid) | no | Your own identifier for the chat, such as a phone number or another system's chat id. Unique within the organisation and case-sensitive. Once set, the chat can be addressed at `/api/v1/chats/external/{external_id}`. Not coerced from `""`, like `namespace`: it is an address, and a client whose interpolation produced an empty one has a bug a 422 should show. Stored in the database as a keyed hash plus a sealed copy. It does appear in request logs, because it travels in URL paths; see the external id schema. |
| `namespace` | null or [Namespace](#namespace) | no | The scope this chat's configuration resolves against, and the prefix it will be listed under. Immutable once set. Note this field does **not** coerce an empty string to "no namespace", where `title` and `description` below do. A namespace is load-bearing — it decides which system instructions and tools every future run on this chat receives — so a client whose interpolation produced `""` should get a 422 rather than a chat silently pinned to the org-global scope. The labels are decorative, and an untouched form field is not worth a rejection. |
| `title` | null or [ChatTitle](#chattitle) | no |  |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Arbitrary customer JSON, capped at a kilobyte. Twigg never reads it — it is somewhere to hang a ticket reference or a UI flag so the customer does not have to keep a parallel table keyed by our ids. Absent means an empty object, never `null`, so "no tags" is one stored value rather than two. |

| Status | Content type | Body |
|---|---|---|
| 201 | application/json | [ChatCreated](#chatcreated) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 413 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: createChat

```http
POST /api/v1/chats
Authorization: Bearer <api key>

{
  "namespace": "acme/proj-7",
  "title": "Support thread"
}

201 →
{
  "id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "namespace": "acme/proj-7",
  "title": "Support thread",
  "description": null,
  "external_id": null,
  "user_metadata": {},
  "created_at": "2026-09-06T10:24:00Z"
}
```

## openChatByExternalId

### PUT /api/v1/chats/external/{external_id}

Return the chat holding this external id, creating it from the body if there is none. 201 means it was created, 200 that it already existed. On an existing chat the body is ignored, except that a namespace you send that differs from the chat's (including null for a namespaced chat) returns 409 external_id_namespace_mismatch; leaving namespace out skips the check. External ids are unique per organisation and case-sensitive; percent-encode them in the path.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |

Request body: [OpenChatRequest](#openchatrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `description` | null or [ChatDescription](#chatdescription) | no |  |
| `namespace` | null or [Namespace](#namespace) | no | The scope the chat's configuration resolves against, fixed when the chat is created. On an existing chat it is checked only when sent: a different value (including `null` for a namespaced chat) is refused with 409 `external_id_namespace_mismatch`, and leaving it out skips the check. Two layers of `Option` so "left out" and "`null`" stay distinct; see `NamespaceClaim`(crate::models::domain::NamespaceClaim). |
| `title` | null or [ChatTitle](#chattitle) | no |  |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Customer JSON object, at most 1024 bytes when serialized compactly. Defaults to an empty object. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ChatSummary](#chatsummary) |
| 201 | application/json | [ChatSummary](#chatsummary) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 413 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: openByExternalId

```http
PUT /api/v1/chats/external/support%3A%2B447700900123
Authorization: Bearer <api key>

{
  "namespace": "acme/support-bot",
  "title": "WhatsApp +44 7700 900123"
}

201 →
{
  "id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "namespace": "acme/support-bot",
  "title": "WhatsApp +44 7700 900123",
  "description": null,
  "external_id": "support:+447700900123",
  "user_metadata": {},
  "length": 0,
  "created_at": "2026-09-06T10:24:00Z",
  "updated_at": "2026-09-06T10:24:00Z"
}
```

## deleteChat

### DELETE /api/v1/chats/{chat_id}

Delete a chat and its ledger. Repeating the deletion returns 404.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `chat_id` | path | string (uuid) | yes | Resource identifier |

| Status | Content type | Body |
|---|---|---|
| 204 | — | No body |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getChat

### GET /api/v1/chats/{chat_id}

Fetch a chat owned by the authenticated organisation. Missing chats and chats owned by another organisation both return 404.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `chat_id` | path | string (uuid) | yes | Resource identifier |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ChatSummary](#chatsummary) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getChatHistory

### GET /api/v1/chats/{chat_id}/history

Read the newest page by default, with rows ordered oldest first. Use before_ordinal or after_ordinal, not both. Unknown query parameters return 422.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `chat_id` | path | string (uuid) | yes | Resource identifier |
| `before_ordinal` | query | integer (int64) | no | Page **backwards** from here: the parts immediately older than this ordinal. What a reader scrolling up sends, using the `first_ordinal` of the page they already have. |
| `after_ordinal` | query | integer (int64) | no | Page **forwards** from here: the parts immediately newer than this ordinal. What a UI already open sends to pick up what has arrived since, using `last_ordinal`. An **ordinal**, not a part id, because ordinals are the ledger's only total order and a client paging already holds the one it saw last. They are never renumbered, so a cursor stays valid; deletes leave gaps, so a cursor is not an index and must not be incremented by hand. |
| `limit` | query | integer (int64) | no | 1–100, or 1–20 with `include=full`. Out-of-range values are clamped, not rejected. |
| `include` | query | `"stubs"` or `"none"` or `"full"` | no | How much of an attachment travels: `stubs` (the default), `none`, or `full`. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [HistoryPageResponse](#historypageresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: history

```http
GET /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/history?after_ordinal=40&include=stubs
Authorization: Bearer <api key>

200 →
{
  "data": [
    {
      "id": "01912d51-4e7c-7a90-b3d2-8c5f1a0e6742",
      "ordinal": 41,
      "created_at": "2026-09-06T11:02:09Z",
      "role": "assistant",
      "part": {
        "type": "tool_call",
        "tool_use_id": "toolu_example",
        "tool_name": "read_file",
        "arguments": "{\"path\":\"src/main.rs\"}"
      },
      "user_metadata": {}
    }
  ],
  "first_ordinal": 41,
  "last_ordinal": 41,
  "has_more": false
}
```

## createResponse

### POST /api/v1/chats/{chat_id}/responses

Submit a prompt or tool results. By default the answer streams as server-sent events: failures before the first byte use HTTP errors and later failures use the error event. With "stream": false the answer is one JSON object and every failure is an HTTP error; failures after the turn started carry details.run_id, and reusing an idempotency_key whose run finished, with the same input, returns that run's result with replayed: true. Dropping the connection does not cancel the turn. A chat already running a turn returns 409 chat_busy with Retry-After. A reused idempotency key otherwise returns 409 idempotency_key_reused with details.run_id. With wait_for_pending_tool_calls set, a submission that would close a recent unanswered tool call returns 409 tool_calls_pending with Retry-After.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `chat_id` | path | string (uuid) | yes | Resource identifier |

Request body: [CreateResponseRequest](#createresponserequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `chat` | null or [OpenChatRequest](#openchatrequest) | no | Only on `/api/v1/chats/external/{external_id}/responses`: how to create the chat if no chat holds that external id yet. Omit it to create one with no namespace or labels. On an existing chat it is ignored, except that a stated `namespace` that differs is refused with 409 `external_id_namespace_mismatch`. |
| `idempotency_key` | null or [IdempotencyKey](#idempotencykey) | no | Prevents a second run for the same key. Org-scoped and honoured for 24 hours. Letters, digits and `-._~` only. A key belongs to the input it was first sent with; the same key with different input is always refused. Reusing a key returns HTTP 409 `idempotency_key_reused` with the original run in `details.run_id`; it does not replay the SSE stream. After a dropped connection, inspect that run with GET /api/v1/runs/{run_id} and read chat history for persisted output. Do not generate a new key merely to retry an uncertain submission. |
| `input` | array of ([SubmittedPart](#submittedpart)); minimum 0 items; maximum 128 items | yes |  |
| `max_tokens` | integer,null (int32); minimum 1 | no | Output ceiling. Only ever **lowered** — by the model's own ceiling, and by what the balance can pay for. |
| `model` | string | yes | The catalogue `name`, as `GET /api/v1/models` lists it — **not** the string the provider spells it with. The two are separate columns precisely so this one can stay stable: the wire name carries snapshot dates that vendors move under us, and a customer who pinned one would break the day an alias was re-pointed. Only an **active** model resolves; a withdrawn one would open a turn nothing can continue. |
| `reasoning_effort` | null or [ReasoningEffort](#reasoningeffort) | no | How hard the model should think, where it can. Coerced with a warning rather than rejected when the model cannot honour it. |
| `retry_of` | null or [RunId](#runid) | no | Retry the latest failed customer run on this chat without appending input. Send input: [] and a new idempotency key. Supply the model, any request-level tools and any system prompt again; the retry uses the current configuration. |
| `stream` | boolean | no | `true` (the default) streams the answer as server-sent events. `false` waits for the turn to finish and returns one JSON object: the ordered content, the text concatenated, the pending tool calls with their arguments, usage and cost. Errors are then HTTP statuses throughout, including failures after the turn started, which carry `details.run_id`. With `false`, reusing an `idempotency_key` whose run finished, with the same `input`, returns that run's result again, with `replayed: true`, instead of a 409. Send a key with every non-streaming request: a turn that outlasts the request returns `504 response_timeout`, and only a key makes retrying it safe. |
| `system` | null or [SystemPrompt](#systemprompt) | no | A system prompt for this request only. Appended after the instructions configured for the chat's namespace and the organisation, never in place of them, so configured instructions still apply. It is not stored in the chat's history, so send it on every request it should apply to, retries included. It is counted against the context budget like the configured instructions. An empty string is treated as absent. |
| `tools` | array,null | no | Tools for this request alone, in the same shape a published bundle uses. The **nearest** level there is, so one of these shadows a namespace or org-global tool of the same name. They are charged against the context budget like any other tool, because the provider charges for them. |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Arbitrary customer JSON, attached to every part this turn writes. |
| `wait_for_pending_tool_calls` | null or [PendingCallWindow](#pendingcallwindow) | no | Protect unanswered tool calls younger than this many seconds (1 to 600). By default, a submission that does not answer the chat's open tool calls closes them, recording each as abandoned. With this set, a submission that would close a call younger than the window is refused with 409 `tool_calls_pending` and a `Retry-After` header instead, and nothing is written. Use it when tools run elsewhere, such as in an automation workflow, and a second user message can arrive while a tool is running. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ResponseBody](#responsebody) |
| 200 | text/event-stream | [ResponseEvent](#responseevent) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 402 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 413 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |
| 504 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: prompt

```http
POST /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/responses
Authorization: Bearer <api key>

{
  "model": "claude-opus-5",
  "input": [
    {
      "type": "prompt",
      "text": "What changed in the billing module?"
    }
  ],
  "idempotency_key": "billing-turn-1"
}
```

Example: toolResult

```http
POST /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/responses
Authorization: Bearer <api key>

{
  "model": "claude-opus-5",
  "input": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_example",
      "tool_name": "read_file",
      "text": "The billing module now records refunds.",
      "is_error": false,
      "trust": "customer_data"
    }
  ]
}
```

Example: landingFirst

```http
POST /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/responses
Authorization: Bearer <api key>

{
  "model": "claude-opus-5",
  "input": [
    {
      "type": "prompt",
      "text": "What changed in billing?"
    }
  ]
}
```

Example: landingNext

```http
POST /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/responses
Authorization: Bearer <api key>

{
  "model": "gpt-5-6-luna",
  "input": [
    {
      "type": "prompt",
      "text": "And in refunds?"
    }
  ]
}
```

## createResponseByExternalId

### POST /api/v1/chats/external/{external_id}/responses

The same as POST /api/v1/chats/{chat_id}/responses, for the chat holding this external id, which is created on first use. The optional chat object describes the chat to create; on an existing chat it is ignored, except that a namespace you send that differs from the chat's returns 409 external_id_namespace_mismatch; leaving namespace out skips the check. Send "stream": false to receive one JSON object instead of an event stream.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |

Request body: [CreateResponseRequest](#createresponserequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `chat` | null or [OpenChatRequest](#openchatrequest) | no | Only on `/api/v1/chats/external/{external_id}/responses`: how to create the chat if no chat holds that external id yet. Omit it to create one with no namespace or labels. On an existing chat it is ignored, except that a stated `namespace` that differs is refused with 409 `external_id_namespace_mismatch`. |
| `idempotency_key` | null or [IdempotencyKey](#idempotencykey) | no | Prevents a second run for the same key. Org-scoped and honoured for 24 hours. Letters, digits and `-._~` only. A key belongs to the input it was first sent with; the same key with different input is always refused. Reusing a key returns HTTP 409 `idempotency_key_reused` with the original run in `details.run_id`; it does not replay the SSE stream. After a dropped connection, inspect that run with GET /api/v1/runs/{run_id} and read chat history for persisted output. Do not generate a new key merely to retry an uncertain submission. |
| `input` | array of ([SubmittedPart](#submittedpart)); minimum 0 items; maximum 128 items | yes |  |
| `max_tokens` | integer,null (int32); minimum 1 | no | Output ceiling. Only ever **lowered** — by the model's own ceiling, and by what the balance can pay for. |
| `model` | string | yes | The catalogue `name`, as `GET /api/v1/models` lists it — **not** the string the provider spells it with. The two are separate columns precisely so this one can stay stable: the wire name carries snapshot dates that vendors move under us, and a customer who pinned one would break the day an alias was re-pointed. Only an **active** model resolves; a withdrawn one would open a turn nothing can continue. |
| `reasoning_effort` | null or [ReasoningEffort](#reasoningeffort) | no | How hard the model should think, where it can. Coerced with a warning rather than rejected when the model cannot honour it. |
| `retry_of` | null or [RunId](#runid) | no | Retry the latest failed customer run on this chat without appending input. Send input: [] and a new idempotency key. Supply the model, any request-level tools and any system prompt again; the retry uses the current configuration. |
| `stream` | boolean | no | `true` (the default) streams the answer as server-sent events. `false` waits for the turn to finish and returns one JSON object: the ordered content, the text concatenated, the pending tool calls with their arguments, usage and cost. Errors are then HTTP statuses throughout, including failures after the turn started, which carry `details.run_id`. With `false`, reusing an `idempotency_key` whose run finished, with the same `input`, returns that run's result again, with `replayed: true`, instead of a 409. Send a key with every non-streaming request: a turn that outlasts the request returns `504 response_timeout`, and only a key makes retrying it safe. |
| `system` | null or [SystemPrompt](#systemprompt) | no | A system prompt for this request only. Appended after the instructions configured for the chat's namespace and the organisation, never in place of them, so configured instructions still apply. It is not stored in the chat's history, so send it on every request it should apply to, retries included. It is counted against the context budget like the configured instructions. An empty string is treated as absent. |
| `tools` | array,null | no | Tools for this request alone, in the same shape a published bundle uses. The **nearest** level there is, so one of these shadows a namespace or org-global tool of the same name. They are charged against the context budget like any other tool, because the provider charges for them. |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Arbitrary customer JSON, attached to every part this turn writes. |
| `wait_for_pending_tool_calls` | null or [PendingCallWindow](#pendingcallwindow) | no | Protect unanswered tool calls younger than this many seconds (1 to 600). By default, a submission that does not answer the chat's open tool calls closes them, recording each as abandoned. With this set, a submission that would close a call younger than the window is refused with 409 `tool_calls_pending` and a `Retry-After` header instead, and nothing is written. Use it when tools run elsewhere, such as in an automation workflow, and a second user message can arrive while a tool is running. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ResponseBody](#responsebody) |
| 200 | text/event-stream | [ResponseEvent](#responseevent) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 402 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 413 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |
| 504 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: externalMessage

```http
POST /api/v1/chats/external/support%3A%2B447700900123/responses
Authorization: Bearer <api key>

{
  "model": "claude-opus-5",
  "input": [
    {
      "type": "prompt",
      "text": "Where is my order 1042?"
    }
  ],
  "stream": false,
  "system": "The customer is messaging on WhatsApp. Keep replies short.",
  "tools": [
    {
      "name": "get_order_status",
      "description": "Look up an order by number",
      "input_schema": {
        "type": "object",
        "properties": {
          "order_id": {
            "type": "string"
          }
        },
        "required": [
          "order_id"
        ],
        "additionalProperties": false
      }
    }
  ],
  "wait_for_pending_tool_calls": 60,
  "idempotency_key": "n8n-exec-8812",
  "chat": {
    "namespace": "acme/support-bot"
  }
}

200 →
{
  "run_id": "01912d51-4e7c-7a90-b3d2-8c5f1a0e6742",
  "chat_id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "content": [
    {
      "type": "text",
      "text": "Let me check that for you."
    },
    {
      "type": "tool_call",
      "tool_use_id": "toolu_01ORDER",
      "name": "get_order_status",
      "arguments": {
        "order_id": "1042"
      }
    }
  ],
  "text": "Let me check that for you.",
  "pending_tool_calls": [
    {
      "tool_use_id": "toolu_01ORDER",
      "name": "get_order_status",
      "arguments": {
        "order_id": "1042"
      }
    }
  ],
  "closed_tool_calls": [],
  "stop_reason": "tool_use",
  "usage": {
    "input_tokens": 412,
    "output_tokens": 58,
    "cache_read_tokens": 0,
    "cache_write_tokens": 0,
    "reasoning_tokens": 0
  },
  "cost": "0.00211300",
  "cost_currency": "USD",
  "model_served": "claude-opus-5",
  "provider_response_id": null,
  "server_tools": null,
  "compaction": null,
  "config_warnings": [],
  "translation_warnings": [],
  "replayed": false
}
```

Example: externalToolResult

```http
POST /api/v1/chats/external/support%3A%2B447700900123/responses
Authorization: Bearer <api key>

{
  "model": "claude-opus-5",
  "input": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01ORDER",
      "tool_name": "get_order_status",
      "text": "{\"status\":\"shipped\",\"eta\":\"2026-09-08\"}"
    }
  ],
  "stream": false,
  "system": "The customer is messaging on WhatsApp. Keep replies short.",
  "wait_for_pending_tool_calls": 60,
  "idempotency_key": "n8n-exec-8812-tool-1"
}
```

## listCatalogue

### GET /v1/catalogue

No API key required. Anonymous requests use platform-default prices. A valid console session uses that organisation's markup; an invalid session is treated as anonymous.

Authentication: Public; no API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [Catalogue](#catalogue) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getCatalogueModel

### GET /v1/catalogue/{name}

No API key required. Anonymous requests use platform-default prices. A valid console session uses that organisation's markup; an invalid session is treated as anonymous.

Authentication: Public; no API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `name` | path | string | yes | Public model name; may include slashes. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [CatalogueModelResponse](#cataloguemodelresponse) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getCatalogueMarkdown

### GET /v1/catalogue/models.md

No API key required. Anonymous requests use platform-default prices. A valid console session uses that organisation's markup; an invalid session is treated as anonymous.

Authentication: Public; no API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | text/markdown | string |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## Supplementary API

Finer control and recall options for deeper Twigg implementations.

| Method | Path | Authentication | Purpose |
|---|---|---|---|
| DELETE | `/api/v1/chats/external/{external_id}` | API key | Delete a chat by external id |
| GET | `/api/v1/chats/external/{external_id}` | API key | Get a chat by external id |
| POST | `/api/v1/chats/external/{external_id}/abort` | API key | Abort the turn in progress by external id |
| GET | `/api/v1/chats/external/{external_id}/history` | API key | Read chat history by external id |
| POST | `/api/v1/chats/{chat_id}/abort` | API key | Abort the turn in progress |
| GET | `/api/v1/config/budgets` | API key | List active context budgets |
| POST | `/api/v1/config/budgets` | API key | Publish context budgets |
| DELETE | `/api/v1/config/budgets/active` | API key | Withdraw context budget |
| GET | `/api/v1/config/budgets/versions` | API key | List context budget versions |
| POST | `/api/v1/config/budgets/{id}/activate` | API key | Restore Budget |
| GET | `/api/v1/config/defaults` | API key | Read default policies |
| GET | `/api/v1/config/instructions` | API key | List Instruction Versions |
| POST | `/api/v1/config/instructions` | API key | Publish system instructions |
| DELETE | `/api/v1/config/instructions/active` | API key | Withdraw Instructions |
| GET | `/api/v1/config/instructions/{id}` | API key | Read instruction version |
| POST | `/api/v1/config/instructions/{id}/activate` | API key | Restore Instructions |
| GET | `/api/v1/config/namespaces` | API key | List namespaces used by chats or configuration |
| GET | `/api/v1/config/resolved` | API key | Resolve effective configuration |
| GET | `/api/v1/config/resolved-budget` | API key | Resolve effective context budget |
| GET | `/api/v1/config/retention` | API key | List Retention Versions |
| POST | `/api/v1/config/retention` | API key | Publish retention policy |
| DELETE | `/api/v1/config/retention/active` | API key | Withdraw Retention |
| POST | `/api/v1/config/retention/{id}/activate` | API key | Restore Retention |
| GET | `/api/v1/config/tools` | API key | List Tool Versions |
| POST | `/api/v1/config/tools` | API key | Publish saved tools |
| DELETE | `/api/v1/config/tools/active` | API key | Withdraw Tools |
| GET | `/api/v1/config/tools/{id}` | API key | Read tool version |
| POST | `/api/v1/config/tools/{id}/activate` | API key | Restore Tools |
| GET | `/api/v1/models` | API key | List available models |
| GET | `/api/v1/namespaces` | API key | List namespaces in use |
| GET | `/api/v1/runs/{run_id}` | API key | Inspect a run |
| GET | `/api/v1/updates` | None | List published updates |
| GET | `/api/v1/usage` | API key | Query organisation or namespace usage |

## deleteChatByExternalId

### DELETE /api/v1/chats/external/{external_id}

Delete the chat holding this external id. The external id is cleared from the chat and freed, so a later message to it starts a new chat. Repeating the deletion returns 404.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |

| Status | Content type | Body |
|---|---|---|
| 204 | — | No body |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getChatByExternalId

### GET /api/v1/chats/external/{external_id}

Fetch the chat holding this external id. Unknown ids, deleted chats and another organisation's ids all return 404.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ChatSummary](#chatsummary) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## abortResponseByExternalId

### POST /api/v1/chats/external/{external_id}/abort

The same as POST /api/v1/chats/{chat_id}/abort, for the chat holding this external id.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |
| `run_id` | query | [RunId](#runid) | no | The run you mean to stop: the `run_id` from the stream's `run` event. Optional. When given, nothing else is stopped, so an abort that arrives late cannot stop the chat's *next* turn by mistake. |

| Status | Content type | Body |
|---|---|---|
| 202 | application/json | [AbortAccepted](#abortaccepted) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getChatHistoryByExternalId

### GET /api/v1/chats/external/{external_id}/history

The same as GET /api/v1/chats/{chat_id}/history, for the chat holding this external id.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `external_id` | path | [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat |
| `before_ordinal` | query | integer (int64) | no | Page **backwards** from here: the parts immediately older than this ordinal. What a reader scrolling up sends, using the `first_ordinal` of the page they already have. |
| `after_ordinal` | query | integer (int64) | no | Page **forwards** from here: the parts immediately newer than this ordinal. What a UI already open sends to pick up what has arrived since, using `last_ordinal`. An **ordinal**, not a part id, because ordinals are the ledger's only total order and a client paging already holds the one it saw last. They are never renumbered, so a cursor stays valid; deletes leave gaps, so a cursor is not an index and must not be incremented by hand. |
| `limit` | query | integer (int64) | no | 1–100, or 1–20 with `include=full`. Out-of-range values are clamped, not rejected. |
| `include` | query | `"stubs"` or `"none"` or `"full"` | no | How much of an attachment travels: `stubs` (the default), `none`, or `full`. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [HistoryPageResponse](#historypageresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## abortResponse

### POST /api/v1/chats/{chat_id}/abort

Stop the chat's in-progress turn. Completed blocks and a cut-off text block are kept; a cut-off reasoning block or tool call is dropped. Returns 409 no_active_turn when nothing is running, and 409 run_not_active when run_id names a run that is not the chat's live turn.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `chat_id` | path | string (uuid) | yes | Resource identifier |
| `run_id` | query | [RunId](#runid) | no | The run you mean to stop: the `run_id` from the stream's `run` event. Optional. When given, nothing else is stopped, so an abort that arrives late cannot stop the chat's *next* turn by mistake. |

| Status | Content type | Body |
|---|---|---|
| 202 | application/json | [AbortAccepted](#abortaccepted) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 409 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: abort

```http
POST /api/v1/chats/01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a/abort?run_id=01912d4f-0a7c-7d21-9e3b-2c4d5e6f7a8b
Authorization: Bearer <api key>

202 →
{
  "run_id": "01912d4f-0a7c-7d21-9e3b-2c4d5e6f7a8b",
  "chat_id": "01912d4e-9c1a-7b3f-8a21-6f1e2c3d4b5a",
  "status": "aborting"
}
```

## listActiveBudgets

### GET /api/v1/config/budgets

List active context budgets

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([ContextBudgetResponse](#contextbudgetresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## publishBudgets

### POST /api/v1/config/budgets

Publish context budgets

Authentication: Bearer API key required.

Request body: [PublishContextBudgetRequest](#publishcontextbudgetrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `budget` | [ContextBudget](#contextbudget) | yes |  |
| `model_ids` | array of ([ModelId](#modelid)) | no |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([ContextBudgetResponse](#contextbudgetresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: publishContextBudget

```http
POST /api/v1/config/budgets
Authorization: Bearer <api key>

{
  "namespace": "acme/proj-7",
  "model_ids": [],
  "budget": {
    "version": 3,
    "headroom": 0.1,
    "history_mode": "summarize",
    "eager_compaction": {
      "mode": "at_fill",
      "at": 0.8
    },
    "max_history_tokens": null
  }
}
```

## withdrawBudget

### DELETE /api/v1/config/budgets/active

Withdraw context budget

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |
| `model` | query | [ModelId](#modelid) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [DeactivateResponse](#deactivateresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listBudgetVersions

### GET /api/v1/config/budgets/versions

List context budget versions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |
| `model` | query | [ModelId](#modelid) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([ContextBudgetResponse](#contextbudgetresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## restoreBudget

### POST /api/v1/config/budgets/{id}/activate

restoreBudget

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [ContextBudgetPolicyId](#contextbudgetpolicyid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ContextBudgetResponse](#contextbudgetresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getConfigurationDefaults

### GET /api/v1/config/defaults

Read default policies

Authentication: Bearer API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [DefaultPoliciesResponse](#defaultpoliciesresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listInstructionVersions

### GET /api/v1/config/instructions

listInstructionVersions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([ConfigVersionResponse](#configversionresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## publishInstructions

### POST /api/v1/config/instructions

Publish system instructions

Authentication: Bearer API key required.

Request body: [PublishInstructionRequest](#publishinstructionrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes |  |
| `mode` | [ConfigMode](#configmode) | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ConfigVersionResponse](#configversionresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: publishInstructions

```http
POST /api/v1/config/instructions
Authorization: Bearer <api key>

{
  "namespace": "acme/proj-7",
  "mode": "append",
  "body": "You are a helpful support assistant. Ask before changing customer data."
}
```

## withdrawInstructions

### DELETE /api/v1/config/instructions/active

withdrawInstructions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [DeactivateResponse](#deactivateresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getInstructions

### GET /api/v1/config/instructions/{id}

Read instruction version

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [SystemInstructionId](#systeminstructionid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [InstructionResponse](#instructionresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## restoreInstructions

### POST /api/v1/config/instructions/{id}/activate

restoreInstructions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [SystemInstructionId](#systeminstructionid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ConfigVersionResponse](#configversionresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listConfigurationNamespaces

### GET /api/v1/config/namespaces

List namespaces used by chats or configuration

Authentication: Bearer API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([Namespace](#namespace)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## resolveConfiguration

### GET /api/v1/config/resolved

Resolve effective configuration

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ResolvedConfig](#resolvedconfig) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## resolveContextBudget

### GET /api/v1/config/resolved-budget

Resolve effective context budget

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |
| `model` | query | [ModelId](#modelid) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ResolvedBudget](#resolvedbudget) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listRetentionVersions

### GET /api/v1/config/retention

listRetentionVersions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([RetentionPolicyResponse](#retentionpolicyresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## publishRetention

### POST /api/v1/config/retention

Publish retention policy

Authentication: Bearer API key required.

Request body: [PublishRetentionPolicyRequest](#publishretentionpolicyrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `namespace` | null or [Namespace](#namespace) | no |  |
| `rules` | [RetentionRules](#retentionrules) | yes | The stored document. Private fields: holding one is proof it parsed. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [RetentionPolicyResponse](#retentionpolicyresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: publishRetentionPolicy

```http
POST /api/v1/config/retention
Authorization: Bearer <api key>

{
  "namespace": "acme/proj-7",
  "rules": {
    "version": 3,
    "media": {
      "default": 0,
      "rules": [
        {
          "mime": "image/*",
          "retain": 0
        },
        {
          "mime": "application/pdf",
          "retain": 0
        }
      ]
    },
    "tools": {
      "default": {
        "result": {
          "mode": "keep"
        },
        "arguments": {
          "mode": "keep"
        }
      },
      "rules": [],
      "errors": null,
      "mark_truncations": true
    },
    "text": {
      "prompt": {
        "mode": "keep"
      },
      "message": {
        "mode": "keep"
      }
    },
    "reasoning": 0.3,
    "verbatim_within": 0.3,
    "anchor_parts": 0
  }
}
```

## withdrawRetention

### DELETE /api/v1/config/retention/active

withdrawRetention

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [DeactivateResponse](#deactivateresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## restoreRetention

### POST /api/v1/config/retention/{id}/activate

restoreRetention

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [RetentionPolicyId](#retentionpolicyid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [RetentionPolicyResponse](#retentionpolicyresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listToolVersions

### GET /api/v1/config/tools

listToolVersions

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([ConfigVersionResponse](#configversionresponse)) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## publishTools

### POST /api/v1/config/tools

Publish saved tools

Authentication: Bearer API key required.

Request body: [PublishToolsRequest](#publishtoolsrequest).

| Field | Type | Required | Description |
|---|---|---|---|
| `definitions` | array of ([ToolDefinition](#tooldefinition)) | yes |  |
| `mode` | [ConfigMode](#configmode) | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ConfigVersionResponse](#configversionresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: publishSavedTools

```http
POST /api/v1/config/tools
Authorization: Bearer <api key>

{
  "namespace": "acme/proj-7",
  "mode": "append",
  "definitions": [
    {
      "name": "lookup_order",
      "description": "Find an order by its ID.",
      "input_schema": {
        "type": "object",
        "properties": {
          "order_id": {
            "type": "string"
          }
        },
        "required": [
          "order_id"
        ]
      }
    }
  ]
}
```

## withdrawTools

### DELETE /api/v1/config/tools/active

withdrawTools

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `namespace` | query | [Namespace](#namespace) | no |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [DeactivateResponse](#deactivateresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## getTools

### GET /api/v1/config/tools/{id}

Read tool version

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [ToolSchemaId](#toolschemaid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ToolSchemaResponse](#toolschemaresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## restoreTools

### POST /api/v1/config/tools/{id}/activate

restoreTools

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | [ToolSchemaId](#toolschemaid) | yes |  |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [ConfigVersionResponse](#configversionresponse) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listModels

### GET /api/v1/models

Returns active models available for new requests, with the caller's prices.

Authentication: Bearer API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([CatalogueModel](#cataloguemodel)) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listNamespaces

### GET /api/v1/namespaces

Unpaged, deliberately. A namespace count is bounded by how a customer scopes
rather than by how much they use the product, and a cursor on a list nobody
will page is API surface with no reader.

Authentication: Bearer API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [NamespacePage](#namespacepage) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: namespaces

```http
GET /api/v1/namespaces
Authorization: Bearer <api key>

200 →
{
  "data": [
    "acme/proj-7",
    "acme/prdo"
  ]
}
```

## getRun

### GET /api/v1/runs/{run_id}

`404` for a run in another organisation, exactly as for one that does not
exist. The scoping is in the query rather than a check after it, so there is
no timing difference and no way to probe for someone else's run id.

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `run_id` | path | string (uuid) | yes | Resource identifier |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [RunInspection](#runinspection) |
| 400 | application/json | [ErrorEnvelope](#errorenvelope) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 404 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

## listPublicUpdates

### GET /api/v1/updates

List published updates

Authentication: Public; no API key required.

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | array of ([PublicPlatformUpdate](#publicplatformupdate)) |
| 500 | application/json | [ErrorEnvelope](#errorenvelope) |

## queryUsage

### GET /api/v1/usage

Query organisation or namespace usage

Authentication: Bearer API key required.

| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| `range` | query | `"24h"` or `"7d"` or `"30d"` or `"90d"` | yes |  |
| `bin` | query | [UsageBin](#usagebin) | no | Absent means "do not bin" — the window's totals, or one row per group. |
| `group_by` | query | [UsageDimension](#usagedimension) | no | Absent means "do not split" — the organisation as a whole. |
| `namespace` | query | [Namespace](#namespace) | no | Absent is the whole organisation. Present reports the namespace **and its subtree**, which is a different question from the namespace alone and the one a customer with per-project paths is asking. |

| Status | Content type | Body |
|---|---|---|
| 200 | application/json | [UsageReport](#usagereport) |
| 401 | application/json | [ErrorEnvelope](#errorenvelope) |
| 422 | application/json | [ErrorEnvelope](#errorenvelope) |
| 502 | application/json | [ErrorEnvelope](#errorenvelope) |
| 503 | application/json | [ErrorEnvelope](#errorenvelope) |

Example: queryNamespaceUsage

```http
GET /api/v1/usage?range=7d&namespace=acme%2Fproj-7&group_by=namespace
Authorization: Bearer <api key>
```

## Streaming events

Frames use `event: <name>` and `data: <JSON>`, separated by a blank line. Content blocks can repeat; warnings may repeat after compaction.

| Event | Payload | Meaning |
|---|---|---|
| `run` | chat_id, closed_tool_calls, run_id | Always first: identifiers and tool calls closed by this submission. |
| `config_warnings` | warnings | Configuration warnings; emitted second, and may repeat after compaction. |
| `translation_warnings` | warnings | Model compatibility warnings before content starts. |
| `compacting` | blocks, in_progress_elsewhere | Compaction work the request is waiting for. |
| `block_start` | [BlockStart](#blockstart) |  |
| `delta` | [Delta](#delta) |  |
| `block_stop` | [BlockStopEventData](#blockstopeventdata) |  |
| `done` | compaction, cost, cost_currency, model_served, pending_tool_calls, provider_response_id, server_tools, stop_reason, usage | Terminal success. Null cost means settlement is pending; inspect the run later. |
| `error` | code, details, hint, message | Terminal failure after the stream has opened. |

## Schemas

## AbortAccepted

object

An abort that was accepted.

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | [ChatId](#chatid) | yes |  |
| `run_id` | [RunId](#runid) | yes |  |
| `status` | string | yes | Always `aborting`. The turn stops on the instance running it; its outcome arrives as the stream's `done` event with `stop_reason: aborted`, and on `GET /api/v1/runs/{run_id}` as status `cancelled`. An abort that arrives as the turn is finishing can lose that race: the turn then ends normally and `done` carries its own `stop_reason`. |

## ApiKeyId

string (uuid)



## ApiKeyUsageGroup

object

`None` covers events with no key: console-initiated work, and credits.

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key_id` | null or [ApiKeyId](#apikeyid) | no |  |
| `dimension` | `"api_key"` | yes |  |

## ApiVersion

object



| Field | Type | Required | Description |
|---|---|---|---|
| `api_version` | string | yes |  |
| `commit` | string | yes |  |
| `version` | string | yes |  |

## BalanceEntryType

`"usage"` or `"compaction"` or `"topup"` or `"refund"` or `"adjustment"` or `"promotional_credit"`



## BilledRates

object

Per-million rates as the customer pays them.

| Field | Type | Required | Description |
|---|---|---|---|
| `cache_read` | null or [RatePerMillion](#ratepermillion) | no | Absent when the provider does not meter it. |
| `cache_write` | null or [RatePerMillion](#ratepermillion) | no | Price in the stated currency per one million tokens. |
| `currency` | string | yes |  |
| `input` | [RatePerMillion](#ratepermillion) | yes | Price in the stated currency per one million tokens. |
| `output` | [RatePerMillion](#ratepermillion) | yes | Price in the stated currency per one million tokens. |

## BilledTier

object

One pricing condition, in the customer's terms. Rates here are billed too.

| Field | Type | Required | Description |
|---|---|---|---|
| `cache_read` | null or [RatePerMillion](#ratepermillion) | no | Price in the stated currency per one million tokens. |
| `cache_write` | null or [RatePerMillion](#ratepermillion) | no | Price in the stated currency per one million tokens. |
| `id` | string | yes |  |
| `input` | null or [RatePerMillion](#ratepermillion) | no | Price in the stated currency per one million tokens. |
| `max` | integer,null (int64); minimum 0 | no |  |
| `metric` | string | yes |  |
| `min` | integer (int64); minimum 0 | yes |  |
| `mode` | [TierMode](#tiermode) | yes |  |
| `output` | null or [RatePerMillion](#ratepermillion) | no | Price in the stated currency per one million tokens. |

## BlockStart

[TextBlockStart](#textblockstart) or [ReasoningBlockStart](#reasoningblockstart) or [RefusalBlockStart](#refusalblockstart) or [ToolCallBlockStart](#toolcallblockstart)



oneOf alternative 1: [TextBlockStart](#textblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |

oneOf alternative 2: [ReasoningBlockStart](#reasoningblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |

oneOf alternative 3: [RefusalBlockStart](#refusalblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |

oneOf alternative 4: [ToolCallBlockStart](#toolcallblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_call"` | yes |  |
| `tool_name` | [ToolName](#toolname) | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |

## BlockStartEvent

object



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [BlockStart](#blockstart) | yes |  |
| `event` | `"block_start"` | yes |  |

Event data:

oneOf alternative 1: [TextBlockStart](#textblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |

oneOf alternative 2: [ReasoningBlockStart](#reasoningblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |

oneOf alternative 3: [RefusalBlockStart](#refusalblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |

oneOf alternative 4: [ToolCallBlockStart](#toolcallblockstart)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_call"` | yes |  |
| `tool_name` | [ToolName](#toolname) | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |

## BlockStopEvent

object



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [BlockStopEventData](#blockstopeventdata) | yes |  |
| `event` | `"block_stop"` | yes |  |

## BlockStopEventData

object



## BudgetFraction

number (float)



## BudgetSource

[PlatformDefaultBudgetSource](#platformdefaultbudgetsource) or [PolicyBudgetSource](#policybudgetsource)

The configuration scope and model tag that supplied the resolved context budget.

oneOf alternative 1: [PlatformDefaultBudgetSource](#platformdefaultbudgetsource)

No level published one. There is no row, which is why this is a variant
and not a null id.

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"platform_default"` | yes |  |

oneOf alternative 2: [PolicyBudgetSource](#policybudgetsource)

A published policy won, whole.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | [ContextBudgetPolicyId](#contextbudgetpolicyid) | yes |  |
| `kind` | `"policy"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The level it was published at. `None` is org-global. |
| `tagged_model` | null or [ModelId](#modelid) | no | `Some` when the level's tag for this model won over its untagged policy; `None` when the level's untagged policy is what won. |

## Catalogue

object

The whole catalogue plus the margin its prices include.

| Field | Type | Required | Description |
|---|---|---|---|
| `markup_percent` | [MarkupPercent](#markuppercent) | yes | The platform-default markup the listed prices already include. Zero means the prices are provider cost. |
| `models` | array of ([CatalogueModel](#cataloguemodel)) | yes |  |
| `organisation_specific` | boolean | yes | Present when a signed-in caller's organisation has its own rate; the prices then reflect that rate rather than the default. |

## CatalogueModel

object

One catalogue entry, in full. The list and the detail share it: a detail
page is not a different fact, only a bigger frame around the same one.

| Field | Type | Required | Description |
|---|---|---|---|
| `avg_time_to_first_token_ms` | number,null (double) | no | Mean milliseconds from provider request to first nonempty output delta, over successful requests completed in the last 24 hours. Includes retry waits. Null when no timing samples exist. Refreshed every 15 minutes. |
| `context_window` | integer (int32) | yes |  |
| `display_name` | string | yes |  |
| `icon_url` | string,null | no |  |
| `id` | [ModelId](#modelid) | yes |  |
| `max_output_tokens` | integer (int32) | yes |  |
| `max_reasoning_effort` | string,null | no |  |
| `max_tool_definitions` | integer,null (int32) | no |  |
| `media` | [MediaCapabilities](#mediacapabilities) | yes |  |
| `model_family` | string | yes |  |
| `name` | string | yes | The identifier to send as `model` on a run. |
| `provider` | [Provider](#provider) | yes |  |
| `provider_label` | string | yes |  |
| `rates` | [BilledRates](#billedrates) | yes | Per-million rates as the customer pays them. |
| `supports_reasoning` | boolean | yes |  |
| `supports_tools` | boolean | yes |  |
| `thinking` | string | yes |  |
| `tiers` | array of ([BilledTier](#billedtier)) | yes |  |
| `uptime` | null or [Uptime](#uptime) | no | Observed provider request success rate over the past 24 hours, after retries, as a fraction from 0 to 1. Refreshed every 15 minutes. Null means no eligible completed requests. Local validation failures, interrupted runs and unclassified historical failures are excluded; this is not time-based uptime. |

## CatalogueModelResponse

[CatalogueModel](#cataloguemodel) and object



allOf alternative 1: [CatalogueModel](#cataloguemodel)

One catalogue entry, in full. The list and the detail share it: a detail
page is not a different fact, only a bigger frame around the same one.

| Field | Type | Required | Description |
|---|---|---|---|
| `avg_time_to_first_token_ms` | number,null (double) | no | Mean milliseconds from provider request to first nonempty output delta, over successful requests completed in the last 24 hours. Includes retry waits. Null when no timing samples exist. Refreshed every 15 minutes. |
| `context_window` | integer (int32) | yes |  |
| `display_name` | string | yes |  |
| `icon_url` | string,null | no |  |
| `id` | [ModelId](#modelid) | yes |  |
| `max_output_tokens` | integer (int32) | yes |  |
| `max_reasoning_effort` | string,null | no |  |
| `max_tool_definitions` | integer,null (int32) | no |  |
| `media` | [MediaCapabilities](#mediacapabilities) | yes |  |
| `model_family` | string | yes |  |
| `name` | string | yes | The identifier to send as `model` on a run. |
| `provider` | [Provider](#provider) | yes |  |
| `provider_label` | string | yes |  |
| `rates` | [BilledRates](#billedrates) | yes | Per-million rates as the customer pays them. |
| `supports_reasoning` | boolean | yes |  |
| `supports_tools` | boolean | yes |  |
| `thinking` | string | yes |  |
| `tiers` | array of ([BilledTier](#billedtier)) | yes |  |
| `uptime` | null or [Uptime](#uptime) | no | Observed provider request success rate over the past 24 hours, after retries, as a fraction from 0 to 1. Refreshed every 15 minutes. Null means no eligible completed requests. Local validation failures, interrupted runs and unclassified historical failures are excluded; this is not time-based uptime. |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `markup_percent` | [MarkupPercent](#markuppercent) | yes |  |

## ChatCreated

object

The new chat, including its immutable namespace, labels, metadata and creation time.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `description` | null or [ChatDescription](#chatdescription) | yes |  |
| `external_id` | null or [ExternalChatId](#externalchatid) | yes | Your own identifier for the chat, such as a phone number or a chat id from another system. Unique within your organisation, case-sensitive, printable ASCII without spaces, and without `%`, `/`, `\`, `?`, `#` or a leading `.`, which change what a URL means. Percent-encode it in a URL path (`+` as `%2B`). It travels in URL paths, so it appears in request logs: if it identifies a person and that matters to you, send a hash or another opaque value instead of the raw identifier. |
| `id` | [ChatId](#chatid) | yes |  |
| `namespace` | null or [Namespace](#namespace) | yes |  |
| `title` | null or [ChatTitle](#chattitle) | yes |  |
| `user_metadata` | [UserMetadata](#usermetadata) | yes | Echoed from the request, like the labels above — but for a different reason. This one *is* readable from the row; it is echoed so the response reflects what the customer sent rather than what `jsonb` normalisation made of it, and so all three optional fields behave the same way here. |

## ChatDescription

string



Constraints:
```json
{
  "maxLength": 200,
  "minLength": 1
}
```

## ChatId

string (uuid)



## ChatPage

object

One page of chats.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of ([ChatSummary](#chatsummary)) | yes | Newest first, whichever direction was paged. Named `data` rather than `chats` so one pagination helper works across every list endpoint we add — the same reason the upstream APIs do it. |
| `first_id` | null or [ChatId](#chatid) | yes | Id of the first item in `data`. Pass as `before_id` to page backwards. `null` when the page is empty. |
| `has_more` | boolean | yes | Whether more chats exist **in the direction being paged**. |
| `last_id` | null or [ChatId](#chatid) | yes | Id of the last item in `data`. Pass as `after_id` for the next page. |

## ChatPartId

string (uuid)



## ChatSummary

object

One chat in a listing.

Everything optional here is optional in the customer's data, not in our
response: `null` means they did not set it, and the field is always present
so a client never has to distinguish "absent" from "unset".

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `description` | null or [ChatDescription](#chatdescription) | yes |  |
| `external_id` | null or [ExternalChatId](#externalchatid) | yes | The customer's own id for the chat, when it was opened with one. |
| `id` | [ChatId](#chatid) | yes |  |
| `length` | integer (int64) | yes | How many parts the chat's ledger holds. A count of rows, not of tokens, characters or messages — one assistant turn can be several parts (reasoning, then text, then a tool call), so this is larger than the number of exchanges a customer would count by eye. It is meant as an "is there anything in here, and roughly how much" signal, not as an addressing scheme: ordinals are never renumbered and deletes leave gaps, so the last part's ordinal is not `length - 1`. |
| `namespace` | null or [Namespace](#namespace) | yes |  |
| `title` | null or [ChatTitle](#chattitle) | yes |  |
| `updated_at` | string (date-time) | yes |  |
| `user_metadata` | [UserMetadata](#usermetadata) | yes | Customer JSON object, at most 1024 bytes when serialized compactly. Defaults to an empty object. |

## ChatTitle

string



Constraints:
```json
{
  "maxLength": 50,
  "minLength": 1
}
```

## CompactingEvent

object

Compaction work the request is waiting for.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [CompactingEventData](#compactingeventdata) | yes | Compaction work the request is waiting for. |
| `event` | `"compacting"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `blocks` | integer; minimum 0 | yes |  |
| `in_progress_elsewhere` | integer; minimum 0 | yes |  |

## CompactingEventData

object

Compaction work the request is waiting for.

| Field | Type | Required | Description |
|---|---|---|---|
| `blocks` | integer; minimum 0 | yes |  |
| `in_progress_elsewhere` | integer; minimum 0 | yes |  |

## Compaction

object



| Field | Type | Required | Description |
|---|---|---|---|
| `blocks_generated` | integer; minimum 0 | yes |  |
| `blocks_reused` | integer; minimum 0 | yes |  |
| `in_progress_elsewhere` | integer; minimum 0 | yes |  |

## ConfigMode

`"append"` or `"replace"`



## ConfigVersionResponse

object

One published version, without its payload. The body of an instruction and
the definitions of a bundle are fetched per version, not listed — a history
endpoint that returned every body would fetch and decrypt a blob per row.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `id` | string | yes |  |
| `is_active` | boolean | yes |  |
| `mode` | null or [ConfigMode](#configmode) | no | How this version combines with the org-global one — `None` for the two surfaces that do not combine at all (retention policies and context budgets), where a mode would be a field the customer could set and nothing would read. |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `version` | integer (int32) | yes |  |

## ConfigWarning

[ToolShadowedWarning](#toolshadowedwarning) or [GlobalInstructionReplacedWarning](#globalinstructionreplacedwarning) or [GlobalToolsReplacedWarning](#globaltoolsreplacedwarning) or [RetentionPolicyOverriddenWarning](#retentionpolicyoverriddenwarning) or [ContextBudgetResolvedAtWarning](#contextbudgetresolvedatwarning) or [HistoryDroppedWarning](#historydroppedwarning) or [ProviderKeyPausedWarning](#providerkeypausedwarning) or [ProviderKeyFellThroughWarning](#providerkeyfellthroughwarning)

Something legal but worth saying out loud.

oneOf alternative 1: [ToolShadowedWarning](#toolshadowedwarning)

A namespace tool replaced a global tool of the same name. The union is
keyed on name, so this is how an override is *meant* to work — but it is
also exactly what a typo in a tool name looks like from the outside.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_shadowed"` | yes |  |
| `name` | [ToolName](#toolname) | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 2: [GlobalInstructionReplacedWarning](#globalinstructionreplacedwarning)

The namespace's instruction is in `replace` mode, so the org-global
instruction is not being sent at all.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_instruction_replaced"` | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 3: [GlobalToolsReplacedWarning](#globaltoolsreplacedwarning)

The namespace's tool bundle is in `replace` mode, so the global bundle
is not being sent at all — including tools the customer may assume are
always present.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_tools_replaced"` | yes |  |
| `dropped` | integer; minimum 0 | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 4: [RetentionPolicyOverriddenWarning](#retentionpolicyoverriddenwarning)

A nearer level's retention policy displaced a farther level's. This is how
the override is *meant* to work: a policy is a complete decision table and
is selected whole, so the farther one contributes nothing — not its rules
and not its defaults. It still warns, because the result looks identical
either way, and "my org-wide rule stopped applying" is otherwise
undiagnosable.

Both levels are named because at depth neither is guessable from the
other: "a namespace policy won" does not say which of four ancestors
stopped applying.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"retention_policy_overridden"` | yes |  |
| `displaced` | null or [Namespace](#namespace) | no | The nearest level whose policy is not being applied. |
| `namespace` | null or [Namespace](#namespace) | no | The level whose policy is in force. `None` is org-global. |

oneOf alternative 5: [ContextBudgetResolvedAtWarning](#contextbudgetresolvedatwarning)

The budget in force did not come from the plainest place: either an
ancestor level supplied it, or a model tag at the winning level displaced
that level's untagged policy — or both.

Two facts, one warning, because either alone misleads. "The budget came
from `acme`" and "the budget came from `acme`, tagged to this model" send
an admin to different rows, and the second is the only one that explains
why a sibling model in the same namespace behaves differently.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"context_budget_resolved_at"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The level that supplied it. `None` is the org-global scope. |
| `tagged_model` | null or [ModelId](#modelid) | no | `Some` when that level's tag for this model beat its untagged policy. |

oneOf alternative 6: [HistoryDroppedWarning](#historydroppedwarning)

Oldest ledger parts were omitted because no complete context fitted.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"history_dropped"` | yes |  |
| `dropped` | integer (int64) | yes |  |
| `end_ordinal` | integer (int64) | yes |  |
| `start_ordinal` | integer (int64) | yes |  |

oneOf alternative 7: [ProviderKeyPausedWarning](#providerkeypausedwarning)

The organisation's own provider key for this model is paused, so the
turn runs on Twigg's key and is billed at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_paused"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The scope the paused key belongs to. `None` is org-global. |

oneOf alternative 8: [ProviderKeyFellThroughWarning](#providerkeyfellthroughwarning)

The organisation's own provider key failed before any output, and the
model allows falling back, so the turn runs on Twigg's key and is billed
at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_fell_through"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `reason` | [LlmErrorKind](#llmerrorkind) | yes | How the provider refused the key: `auth`, `rate_limited`, … |

## ConfigWarningsEvent

object

Configuration warnings; emitted second, and may repeat after compaction.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [ConfigWarningsEventData](#configwarningseventdata) | yes | Configuration warnings; emitted second, and may repeat after compaction. |
| `event` | `"config_warnings"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `warnings` | array of ([Warning_ConfigWarning](#warning-configwarning)) | yes |  |

## ConfigWarningsEventData

object

Configuration warnings; emitted second, and may repeat after compaction.

| Field | Type | Required | Description |
|---|---|---|---|
| `warnings` | array of ([Warning_ConfigWarning](#warning-configwarning)) | yes |  |

## ContextBudget

object



| Field | Type | Required | Description |
|---|---|---|---|
| `eager_compaction` | [EagerCompaction](#eagercompaction) | no |  |
| `headroom` | [BudgetFraction](#budgetfraction) | yes |  |
| `history_mode` | [HistoryMode](#historymode) | no |  |
| `max_history_tokens` | integer,null (int32); minimum 32000 | no |  |
| `version` | integer (int32); minimum 3; maximum 3 | yes |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## ContextBudgetPolicyId

string (uuid)



## ContextBudgetResolvedAtWarning

object

The budget in force did not come from the plainest place: either an
ancestor level supplied it, or a model tag at the winning level displaced
that level's untagged policy — or both.

Two facts, one warning, because either alone misleads. "The budget came
from `acme`" and "the budget came from `acme`, tagged to this model" send
an admin to different rows, and the second is the only one that explains
why a sibling model in the same namespace behaves differently.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"context_budget_resolved_at"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The level that supplied it. `None` is the org-global scope. |
| `tagged_model` | null or [ModelId](#modelid) | no | `Some` when that level's tag for this model beat its untagged policy. |

## ContextBudgetResponse

[ConfigVersionResponse](#configversionresponse) and object

One context-budget version with its document and the model it is tagged to.

allOf alternative 1: [ConfigVersionResponse](#configversionresponse)

One published version, without its payload. The body of an instruction and
the definitions of a bundle are fetched per version, not listed — a history
endpoint that returned every body would fetch and decrypt a blob per row.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `id` | string | yes |  |
| `is_active` | boolean | yes |  |
| `mode` | null or [ConfigMode](#configmode) | no | How this version combines with the org-global one — `None` for the two surfaces that do not combine at all (retention policies and context budgets), where a mode would be a field the customer could set and nothing would read. |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `version` | integer (int32) | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `budget` | [ContextBudget](#contextbudget) | yes |  |
| `model_id` | null or [ModelId](#modelid) | no | `None` is the scope default, not an untagged leftover. |

## CreateChatRequest

object

Body for `create`. Every field is optional — the minimum viable request is
`{}`, which opens a chat in the org's default scope with no labels.

The org is deliberately absent: it comes off the authenticated key, so a
caller cannot open a chat anywhere but their own organisation.

| Field | Type | Required | Description |
|---|---|---|---|
| `description` | null or [ChatDescription](#chatdescription) | no |  |
| `external_id` | null or [ExternalChatId](#externalchatid) | no | Your own identifier for the chat, such as a phone number or another system's chat id. Unique within the organisation and case-sensitive. Once set, the chat can be addressed at `/api/v1/chats/external/{external_id}`. Not coerced from `""`, like `namespace`: it is an address, and a client whose interpolation produced an empty one has a bug a 422 should show. Stored in the database as a keyed hash plus a sealed copy. It does appear in request logs, because it travels in URL paths; see the external id schema. |
| `namespace` | null or [Namespace](#namespace) | no | The scope this chat's configuration resolves against, and the prefix it will be listed under. Immutable once set. Note this field does **not** coerce an empty string to "no namespace", where `title` and `description` below do. A namespace is load-bearing — it decides which system instructions and tools every future run on this chat receives — so a client whose interpolation produced `""` should get a 422 rather than a chat silently pinned to the org-global scope. The labels are decorative, and an untouched form field is not worth a rejection. |
| `title` | null or [ChatTitle](#chattitle) | no |  |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Arbitrary customer JSON, capped at a kilobyte. Twigg never reads it — it is somewhere to hang a ticket reference or a UI flag so the customer does not have to keep a parallel table keyed by our ids. Absent means an empty object, never `null`, so "no tags" is one stored value rather than two. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## CreateResponseRequest

object

Submit a turn or retry the latest failed run. The answer streams as server-sent events by default; send "stream": false to receive one JSON object instead.

| Field | Type | Required | Description |
|---|---|---|---|
| `chat` | null or [OpenChatRequest](#openchatrequest) | no | Only on `/api/v1/chats/external/{external_id}/responses`: how to create the chat if no chat holds that external id yet. Omit it to create one with no namespace or labels. On an existing chat it is ignored, except that a stated `namespace` that differs is refused with 409 `external_id_namespace_mismatch`. |
| `idempotency_key` | null or [IdempotencyKey](#idempotencykey) | no | Prevents a second run for the same key. Org-scoped and honoured for 24 hours. Letters, digits and `-._~` only. A key belongs to the input it was first sent with; the same key with different input is always refused. Reusing a key returns HTTP 409 `idempotency_key_reused` with the original run in `details.run_id`; it does not replay the SSE stream. After a dropped connection, inspect that run with GET /api/v1/runs/{run_id} and read chat history for persisted output. Do not generate a new key merely to retry an uncertain submission. |
| `input` | array of ([SubmittedPart](#submittedpart)); minimum 0 items; maximum 128 items | yes |  |
| `max_tokens` | integer,null (int32); minimum 1 | no | Output ceiling. Only ever **lowered** — by the model's own ceiling, and by what the balance can pay for. |
| `model` | string | yes | The catalogue `name`, as `GET /api/v1/models` lists it — **not** the string the provider spells it with. The two are separate columns precisely so this one can stay stable: the wire name carries snapshot dates that vendors move under us, and a customer who pinned one would break the day an alias was re-pointed. Only an **active** model resolves; a withdrawn one would open a turn nothing can continue. |
| `reasoning_effort` | null or [ReasoningEffort](#reasoningeffort) | no | How hard the model should think, where it can. Coerced with a warning rather than rejected when the model cannot honour it. |
| `retry_of` | null or [RunId](#runid) | no | Retry the latest failed customer run on this chat without appending input. Send input: [] and a new idempotency key. Supply the model, any request-level tools and any system prompt again; the retry uses the current configuration. |
| `stream` | boolean | no | `true` (the default) streams the answer as server-sent events. `false` waits for the turn to finish and returns one JSON object: the ordered content, the text concatenated, the pending tool calls with their arguments, usage and cost. Errors are then HTTP statuses throughout, including failures after the turn started, which carry `details.run_id`. With `false`, reusing an `idempotency_key` whose run finished, with the same `input`, returns that run's result again, with `replayed: true`, instead of a 409. Send a key with every non-streaming request: a turn that outlasts the request returns `504 response_timeout`, and only a key makes retrying it safe. |
| `system` | null or [SystemPrompt](#systemprompt) | no | A system prompt for this request only. Appended after the instructions configured for the chat's namespace and the organisation, never in place of them, so configured instructions still apply. It is not stored in the chat's history, so send it on every request it should apply to, retries included. It is counted against the context budget like the configured instructions. An empty string is treated as absent. |
| `tools` | array,null | no | Tools for this request alone, in the same shape a published bundle uses. The **nearest** level there is, so one of these shadows a namespace or org-global tool of the same name. They are charged against the context budget like any other tool, because the provider charges for them. |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Arbitrary customer JSON, attached to every part this turn writes. |
| `wait_for_pending_tool_calls` | null or [PendingCallWindow](#pendingcallwindow) | no | Protect unanswered tool calls younger than this many seconds (1 to 600). By default, a submission that does not answer the chat's open tool calls closes them, recording each as abandoned. With this set, a submission that would close a call younger than the window is refused with 409 `tool_calls_pending` and a `Retry-After` header instead, and nothing is written. Use it when tools run elsewhere, such as in an automation workflow, and a second user message can arrive while a tool is running. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## DeactivateResponse

object

Whether anything was actually withdrawn. The call is idempotent, so `false`
means "there was no override", not a failure.

| Field | Type | Required | Description |
|---|---|---|---|
| `was_active` | boolean | yes |  |

## DefaultPoliciesResponse

object

The policies in force for an org that has published nothing.

Both are the platform documents themselves, not a copy: the console seeds its
editors from this so the form a customer first sees is the policy actually
being applied to their chats. Editing the shipped documents moves both at
once, which is the point — a hard-coded copy in the client is a second
statement of the same fact, and the two only ever agree until someone edits
one of them.

| Field | Type | Required | Description |
|---|---|---|---|
| `budget` | [ContextBudget](#contextbudget) | yes |  |
| `retention` | [RetentionRules](#retentionrules) | yes | The stored document. Private fields: holding one is proof it parsed. |

## Delta

[TextDelta](#textdelta) or [ReasoningDelta](#reasoningdelta) or [ToolInputDelta](#toolinputdelta) or [RefusalDelta](#refusaldelta)



oneOf alternative 1: [TextDelta](#textdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 2: [ReasoningDelta](#reasoningdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 3: [ToolInputDelta](#toolinputdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_input"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 4: [RefusalDelta](#refusaldelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |
| `text` | string | yes |  |

## DeltaEvent

object



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [Delta](#delta) | yes |  |
| `event` | `"delta"` | yes |  |

Event data:

oneOf alternative 1: [TextDelta](#textdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 2: [ReasoningDelta](#reasoningdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 3: [ToolInputDelta](#toolinputdelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_input"` | yes |  |
| `text` | string | yes |  |

oneOf alternative 4: [RefusalDelta](#refusaldelta)



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |
| `text` | string | yes |  |

## Discovery

object



| Field | Type | Required | Description |
|---|---|---|---|
| `docs` | string | yes |  |
| `llms_txt` | string | yes |  |
| `openapi` | string | yes |  |
| `service` | string | yes |  |
| `versions` | object | yes |  |

## DoneEvent

object

Terminal success. Null cost means settlement is pending; inspect the run later.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [DoneEventData](#doneeventdata) | yes | Terminal success. Null cost means settlement is pending; inspect the run later. |
| `event` | `"done"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `compaction` | null or [Compaction](#compaction) | yes |  |
| `cost` | null or [Money](#money) | yes | A signed monetary amount in the stated currency. Negative amounts debit a balance; positive amounts credit it. |
| `cost_currency` | string | yes |  |
| `model_served` | string,null | yes | The model identifier reported by the upstream provider, when available. This may differ from the catalogue name used in the request (for example, gpt-5-6-luna can return gpt-5.6-luna). Do not use this as a catalogue lookup key. |
| `pending_tool_calls` | array of ([PendingCall](#pendingcall)) | yes |  |
| `provider_response_id` | string,null | yes |  |
| `server_tools` | null or [ServerToolUsage](#servertoolusage) | yes | Provider-executed operations. Counts are successful billable operations, taken from terminal usage, not inferred from streamed attempts. |
| `stop_reason` | [StopReason](#stopreason) | yes |  |
| `usage` | [Usage](#usage) | yes |  |

## DoneEventData

object

Terminal success. Null cost means settlement is pending; inspect the run later.

| Field | Type | Required | Description |
|---|---|---|---|
| `compaction` | null or [Compaction](#compaction) | yes |  |
| `cost` | null or [Money](#money) | yes | A signed monetary amount in the stated currency. Negative amounts debit a balance; positive amounts credit it. |
| `cost_currency` | string | yes |  |
| `model_served` | string,null | yes | The model identifier reported by the upstream provider, when available. This may differ from the catalogue name used in the request (for example, gpt-5-6-luna can return gpt-5.6-luna). Do not use this as a catalogue lookup key. |
| `pending_tool_calls` | array of ([PendingCall](#pendingcall)) | yes |  |
| `provider_response_id` | string,null | yes |  |
| `server_tools` | null or [ServerToolUsage](#servertoolusage) | yes | Provider-executed operations. Counts are successful billable operations, taken from terminal usage, not inferred from streamed attempts. |
| `stop_reason` | [StopReason](#stopreason) | yes |  |
| `usage` | [Usage](#usage) | yes |  |

## EagerCompaction

[EagerCompactionOff](#eagercompactionoff) or [EagerCompactionAtFill](#eagercompactionatfill)



oneOf alternative 1: [EagerCompactionOff](#eagercompactionoff)



| Field | Type | Required | Description |
|---|---|---|---|
| `mode` | `"off"` | yes |  |

oneOf alternative 2: [EagerCompactionAtFill](#eagercompactionatfill)



| Field | Type | Required | Description |
|---|---|---|---|
| `at` | [BudgetFraction](#budgetfraction) | yes |  |
| `mode` | `"at_fill"` | yes |  |

## EagerCompactionAtFill

object



| Field | Type | Required | Description |
|---|---|---|---|
| `at` | [BudgetFraction](#budgetfraction) | yes |  |
| `mode` | `"at_fill"` | yes |  |

## EagerCompactionOff

object



| Field | Type | Required | Description |
|---|---|---|---|
| `mode` | `"off"` | yes |  |

## EntryTypeUsageGroup

object



| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"entry_type"` | yes |  |
| `entry_type` | [BalanceEntryType](#balanceentrytype) | yes |  |

## ErrorBody

object

The customer-visible error payload, shared by HTTP errors and SSE failures.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | yes |  |
| `details` | any JSON | no |  |
| `hint` | string,null | no |  |
| `message` | string | yes |  |

## ErrorEnvelope

object



| Field | Type | Required | Description |
|---|---|---|---|
| `error` | [ErrorBody](#errorbody) | yes | The customer-visible error payload, shared by HTTP errors and SSE failures. |

## ErrorEvent

object

Terminal failure after the stream has opened.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [ErrorBody](#errorbody) | yes | Terminal failure after the stream has opened. |
| `event` | `"error"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | yes |  |
| `details` | any JSON | no |  |
| `hint` | string,null | no |  |
| `message` | string | yes |  |

## ExternalChatId

string

Your own identifier for the chat, such as a phone number or a chat id from another system. Unique within your organisation, case-sensitive, printable ASCII without spaces, and without `%`, `/`, `\`, `?`, `#` or a leading `.`, which change what a URL means. Percent-encode it in a URL path (`+` as `%2B`). It travels in URL paths, so it appears in request logs: if it identifies a person and that matters to you, send a hash or another opaque value instead of the raw identifier.

Constraints:
```json
{
  "maxLength": 255,
  "minLength": 1,
  "pattern": "^[!\"$&--0-9:->@-\\[\\]-~][!\"$&-.0-9:->@-\\[\\]-~]*$"
}
```

## ForeignArtifactsStrippedWarning

object

Replay artifacts the target cannot accept were stripped: ones authored by
a different model family, or Gemini signatures minted on the other Google
endpoint (Vertex and the Gemini API reject each other's). A signature
from elsewhere is meaningless, or a hard rejection, to the target.
Expected whenever a chat hot-swaps providers, or a Gemini chat moves
between Twigg's key and the customer's own, and reported so a customer
comparing two runs of the same chat can see why the second one lost its
reasoning continuity.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"foreign_artifacts_stripped"` | yes |  |
| `count` | integer; minimum 0 | yes |  |

## GlobalInstructionReplacedWarning

object

The namespace's instruction is in `replace` mode, so the org-global
instruction is not being sent at all.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_instruction_replaced"` | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

## GlobalToolsReplacedWarning

object

The namespace's tool bundle is in `replace` mode, so the global bundle
is not being sent at all — including tools the customer may assume are
always present.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_tools_replaced"` | yes |  |
| `dropped` | integer; minimum 0 | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

## HistoryBody

[HistoryPrompt](#historyprompt) or [HistoryToolResult](#historytoolresult) or [HistoryMessage](#historymessage) or [HistoryReasoning](#historyreasoning) or [HistoryToolCall](#historytoolcall)

History content: a user prompt, tool result, assistant message, reasoning or tool call.

oneOf alternative 1: [HistoryPrompt](#historyprompt)

A user prompt.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"prompt"` | yes |  |

oneOf alternative 2: [HistoryToolResult](#historytoolresult)

A tool result. `tombstone_reason` is present only when Twigg closed a
pending call whose result never arrived; omit it when submitting results.

| Field | Type | Required | Description |
|---|---|---|---|
| `is_error` | boolean | yes |  |
| `text` | string | yes |  |
| `tombstone_reason` | null or [TombstoneReason](#tombstonereason) | no |  |
| `tool_name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `trust` | [TrustLevel](#trustlevel) | yes |  |
| `type` | `"tool_result"` | yes |  |

oneOf alternative 3: [HistoryMessage](#historymessage)

The model's own turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `refusal` | string,null | no |  |
| `text` | string | yes |  |
| `type` | `"message"` | yes |  |

oneOf alternative 4: [HistoryReasoning](#historyreasoning)

Reasoning text exposed by the provider. Text may be empty when the
provider hides its reasoning. Replay signatures are not returned.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"reasoning"` | yes |  |

oneOf alternative 5: [HistoryToolCall](#historytoolcall)

A tool call the model made, for the customer to execute.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | string | yes | JSON-encoded tool arguments. Parse this string to obtain the arguments object. |
| `tool_name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `type` | `"tool_call"` | yes |  |

## HistoryDroppedWarning

object

Oldest ledger parts were omitted because no complete context fitted.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"history_dropped"` | yes |  |
| `dropped` | integer (int64) | yes |  |
| `end_ordinal` | integer (int64) | yes |  |
| `start_ordinal` | integer (int64) | yes |  |

## HistoryMedia

object

An attachment, as a customer reads it back.

| Field | Type | Required | Description |
|---|---|---|---|
| `byte_len` | integer (int64); minimum 0 | yes |  |
| `data_base64` | string,null | no | Standard base64, present only under `include=full`. |
| `filename` | string,null | no |  |
| `height` | integer,null (int32); minimum 0 | no |  |
| `media_id` | [MediaId](#mediaid) | yes |  |
| `mime` | string | yes |  |
| `page_count` | integer,null (int32); minimum 0 | no |  |
| `width` | integer,null (int32); minimum 0 | no |  |

## HistoryMessage

object

The model's own turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `refusal` | string,null | no |  |
| `text` | string | yes |  |
| `type` | `"message"` | yes |  |

## HistoryMode

`"summarize"` or `"drop_oldest"`



## HistoryPageResponse

object

One page of a chat's parts.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of ([HistoryPart](#historypart)) | yes | **Oldest first**, whichever direction was paged — a transcript is read in the order it happened, so a UI appending to the bottom never reverses a page. |
| `first_ordinal` | integer,null (int64) | yes | Ordinal of the first part in `data`. Pass as `before_ordinal` to page further back. `null` when the page is empty. |
| `has_more` | boolean | yes | Whether more parts exist **in the direction this page was paged** — older, by default and when paging back; newer, when paging forward. |
| `last_ordinal` | integer,null (int64) | yes | Ordinal of the last part in `data`. Pass as `after_ordinal` to pick up what arrives after it. `null` when the page is empty. |

## HistoryPart

object

A stored chat part with its ID, position, metadata and content. Prompt and tool-result content can be reused in a new submission; assistant-authored parts cannot be submitted.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `id` | [ChatPartId](#chatpartid) | yes |  |
| `media` | array,null | no | Attachments, when the part has any and `include` asked for them. Returned on the history envelope, separately from `part`. To resubmit an attachment, request `include=full` and copy its MIME type, base64 bytes and optional filename into the submitted prompt or tool result's `media` list. Generated IDs, sizes and dimensions are read-only history metadata. |
| `ordinal` | integer (int64) | yes |  |
| `part` | [HistoryBody](#historybody) | yes | History content: a user prompt, tool result, assistant message, reasoning or tool call. |
| `role` | string | yes | `user` or `assistant`, derived from the kind — there is no stored role. Present because a UI's first decision is which side of the transcript a part belongs on, and deriving it from five kind names is work every client would otherwise repeat. |
| `user_metadata` | any JSON | yes |  |

## HistoryPrompt

object

A user prompt.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"prompt"` | yes |  |

## HistoryReasoning

object

Reasoning text exposed by the provider. Text may be empty when the
provider hides its reasoning. Replay signatures are not returned.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"reasoning"` | yes |  |

## HistoryToolCall

object

A tool call the model made, for the customer to execute.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | string | yes | JSON-encoded tool arguments. Parse this string to obtain the arguments object. |
| `tool_name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `type` | `"tool_call"` | yes |  |

## HistoryToolResult

object

A tool result. `tombstone_reason` is present only when Twigg closed a
pending call whose result never arrived; omit it when submitting results.

| Field | Type | Required | Description |
|---|---|---|---|
| `is_error` | boolean | yes |  |
| `text` | string | yes |  |
| `tombstone_reason` | null or [TombstoneReason](#tombstonereason) | no |  |
| `tool_name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `trust` | [TrustLevel](#trustlevel) | yes |  |
| `type` | `"tool_result"` | yes |  |

## IdempotencyKey

string



Constraints:
```json
{
  "maxLength": 128,
  "minLength": 1
}
```

## InstructionResponse

[ConfigVersionResponse](#configversionresponse) and object

An instruction version with its body — what the editor loads.

allOf alternative 1: [ConfigVersionResponse](#configversionresponse)

One published version, without its payload. The body of an instruction and
the definitions of a bundle are fetched per version, not listed — a history
endpoint that returned every body would fetch and decrypt a blob per row.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `id` | string | yes |  |
| `is_active` | boolean | yes |  |
| `mode` | null or [ConfigMode](#configmode) | no | How this version combines with the org-global one — `None` for the two surfaces that do not combine at all (retention policies and context budgets), where a mode would be a field the customer could set and nothing would read. |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `version` | integer (int32) | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes |  |

## LlmErrorKind

`"auth"` or `"rate_limited"` or `"overloaded"` or `"model_not_found"` or `"invalid_request"` or `"unknown"`



## MarkupPercent

string



## MediaCapabilities

object



| Field | Type | Required | Description |
|---|---|---|---|
| `assistant` | [MediaPolicy](#mediapolicy) | yes |  |
| `configured` | boolean | yes | False means legacy adapter defaults, not verified model-level support. |
| `max_attachments_per_part` | integer; minimum 0 | yes |  |
| `tool_result` | [MediaPolicy](#mediapolicy) | yes |  |
| `user` | [MediaPolicy](#mediapolicy) | yes |  |

## MediaCarrier

`"user_message"` or `"assistant_message"` or `"tool_result"`



## MediaId

string (uuid)



## MediaLimitExceededWarning

object

Historical attachments exceeded the target model's byte or request-count
limit and were replaced with text markers. Aggregated per (MIME, carrier).

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_limit_exceeded"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

## MediaPolicy

object



| Field | Type | Required | Description |
|---|---|---|---|
| `max_attachments_per_request` | integer,null (int32); minimum 0 | no | Count across this carrier in the entire context, not per message. |
| `max_bytes_per_attachment` | integer (int64); minimum 0 | yes | Effective byte ceiling, including the platform inline attachment limit. |
| `mime_types` | array of ([MimeType](#mimetype)) | yes |  |

## MediaRetentionRule

object

One media rule.

| Field | Type | Required | Description |
|---|---|---|---|
| `mime` | [MimePattern](#mimepattern) | yes |  |
| `retain` | [RecencyThreshold](#recencythreshold) | yes | How recent a part must be for media of this type to be replayed. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## MediaRules

object

The media section: which attachments are replayed.

| Field | Type | Required | Description |
|---|---|---|---|
| `default` | [RecencyThreshold](#recencythreshold) | no | Applied when no rule matches. |
| `rules` | array of ([MediaRetentionRule](#mediaretentionrule)) | no |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## MediaUnsupportedWarning

object

Media the target model cannot receive in this position was replaced with
a text marker. Aggregated per (MIME, carrier), separately from retention.

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_unsupported"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

## MimePattern

string



## MimeType

string



## ModelId

string (uuid)



## ModelUsageGroup

object



| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"model"` | yes |  |
| `model_id` | null or [ModelId](#modelid) | no | Absent on a credit, which names no model. |
| `model_name` | string,null | no |  |

## Money

string

A signed monetary amount in the stated currency. Negative amounts debit a balance; positive amounts credit it.

## Namespace

string



Constraints:
```json
{
  "maxLength": 255,
  "minLength": 1,
  "pattern": "^[a-z0-9_-]{1,64}(/[a-z0-9_-]{1,64})*$"
}
```

## NamespacePage

object

Every namespace in use, ascending.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of ([Namespace](#namespace)) | yes | Named `data` like every other listing, so one client-side helper works across the whole surface. **Chats with no namespace contribute nothing.** Absent is the org's default scope rather than a value, and returning it as one would invite a client to send it back as a namespace — which is exactly the empty-string namespace the create endpoint rejects. |

## NamespaceUsageGroup

object

`None` is the org-global scope — chats created without a namespace — not
an unknown one.

| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"namespace"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

## OpenChatRequest

object

How to open a chat addressed by external id, when it does not exist yet.

The fields of a create, minus the external id, which is in the path.

| Field | Type | Required | Description |
|---|---|---|---|
| `description` | null or [ChatDescription](#chatdescription) | no |  |
| `namespace` | null or [Namespace](#namespace) | no | The scope the chat's configuration resolves against, fixed when the chat is created. On an existing chat it is checked only when sent: a different value (including `null` for a namespaced chat) is refused with 409 `external_id_namespace_mismatch`, and leaving it out skips the check. Two layers of `Option` so "left out" and "`null`" stay distinct; see `NamespaceClaim`(crate::models::domain::NamespaceClaim). |
| `title` | null or [ChatTitle](#chattitle) | no |  |
| `user_metadata` | [UserMetadata](#usermetadata) | no | Customer JSON object, at most 1024 bytes when serialized compactly. Defaults to an empty object. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## OrganisationId

string (uuid)



## OrganisationUsageGroup

object

Usage grouped by organisation. Labels are null when unavailable.

| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"organisation"` | yes |  |
| `name` | string,null | no |  |
| `organisation_id` | [OrganisationId](#organisationid) | yes |  |
| `slug` | string,null | no |  |

## PendingCall

object



| Field | Type | Required | Description |
|---|---|---|---|
| `tool_name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |

## PendingCallWindow

integer; minimum 1; maximum 600



Constraints:
```json
{
  "maximum": 600,
  "minimum": 1
}
```

## PendingToolCallBody

object

A tool call the chat is now waiting on, with its arguments.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | object | yes |  |
| `name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |

## PlatformDefaultBudgetSource

object

No level published one. There is no row, which is why this is a variant
and not a null id.

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"platform_default"` | yes |  |

## PolicyBudgetSource

object

A published policy won, whole.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | [ContextBudgetPolicyId](#contextbudgetpolicyid) | yes |  |
| `kind` | `"policy"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The level it was published at. `None` is org-global. |
| `tagged_model` | null or [ModelId](#modelid) | no | `Some` when the level's tag for this model won over its untagged policy; `None` when the level's untagged policy is what won. |

## Provider

`"anthropic"` or `"openai"` or `"google"` or `"xai"` or `"fireworks"` or `"openrouter"`



## ProviderKeyFellThroughWarning

object

The organisation's own provider key failed before any output, and the
model allows falling back, so the turn runs on Twigg's key and is billed
at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_fell_through"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `reason` | [LlmErrorKind](#llmerrorkind) | yes | How the provider refused the key: `auth`, `rate_limited`, … |

## ProviderKeyPausedWarning

object

The organisation's own provider key for this model is paused, so the
turn runs on Twigg's key and is billed at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_paused"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The scope the paused key belongs to. `None` is org-global. |

## PublicPlatformUpdate

object

Published site feed: no account-specific read receipts or draft metadata.

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | [UpdateBody](#updatebody) | yes |  |
| `id` | [UpdateId](#updateid) | yes |  |
| `important` | boolean | yes |  |
| `kind` | [UpdateKind](#updatekind) | yes |  |
| `published_at` | string (date-time) | yes |  |
| `title` | [UpdateTitle](#updatetitle) | yes |  |
| `version` | null or [UpdateVersion](#updateversion) | no |  |

## PublishContextBudgetRequest

object

Body for publishing a context budget.

`model_ids` is the tag list: one document applied to each of those models,
one version row each. An **empty** list publishes the scope default instead —
the budget every model without a tag of its own uses. Publishing again for the same scope and model creates a new version and makes
it active; the previous version remains available for restoration.

| Field | Type | Required | Description |
|---|---|---|---|
| `budget` | [ContextBudget](#contextbudget) | yes |  |
| `model_ids` | array of ([ModelId](#modelid)) | no |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## PublishInstructionRequest

object

Body for publishing an instruction. `mode` is ignored on the global scope,
where there is nothing to combine with.

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes |  |
| `mode` | [ConfigMode](#configmode) | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## PublishRetentionPolicyRequest

object

Body for publishing a retention policy. `rules` is validated into
`RetentionRules` at the boundary, so an unreachable rule, a truncation that
could never cut anything, or an unknown schema version is a 422 before the
handler runs.

No `mode`: a policy is a complete decision table and is selected whole, so
there is nothing to combine and nothing to configure about combining.

| Field | Type | Required | Description |
|---|---|---|---|
| `namespace` | null or [Namespace](#namespace) | no |  |
| `rules` | [RetentionRules](#retentionrules) | yes | The stored document. Private fields: holding one is proof it parsed. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## PublishToolsRequest

object

Body for publishing a tool bundle. `definitions` is validated into a
`ToolBundle` at the boundary, so a malformed bundle is a 422 before the
handler runs.

| Field | Type | Required | Description |
|---|---|---|---|
| `definitions` | array of ([ToolDefinition](#tooldefinition)) | yes |  |
| `mode` | [ConfigMode](#configmode) | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## RatePerMillion

string

Price in the stated currency per one million tokens.

## ReasoningBlockStart

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |

## ReasoningCannotBeDisabledWarning

object

Reasoning was switched **off** and the provider has no way to express
that, so thinking remained enabled.

The mirror of `ReasoningNotSupported`(Self::ReasoningNotSupported), and
the reason it is a warning rather than something to swallow: the customer
gets more than they asked for *and is billed for it*. The Responses API
has no "off" every vendor behind it accepts — the candidates are
model-specific and rejected elsewhere — so omitting the block is the only
universally legal shape, and omitting it means the default applies.
Silence here would make an explicit `off` look honoured while it quietly
costs money on every turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_cannot_be_disabled"` | yes |  |
| `model` | string | yes |  |

## ReasoningDelta

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"reasoning"` | yes |  |
| `text` | string | yes |  |

## ReasoningEffort

`"off"` or `"low"` or `"medium"` or `"high"` or `"x_high"` or `"max"`



## ReasoningEffortClampedWarning

object

The requested effort tier is above what this model accepts and was
lowered. The run still happened, at less depth than asked for.

| Field | Type | Required | Description |
|---|---|---|---|
| `applied` | [ReasoningEffort](#reasoningeffort) | yes |  |
| `code` | `"reasoning_effort_clamped"` | yes |  |
| `model` | string | yes |  |
| `requested` | [ReasoningEffort](#reasoningeffort) | yes |  |

## ReasoningNotSupportedWarning

object

Reasoning was requested but the target model does not support it, so the
request went without a thinking configuration.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_not_supported"` | yes |  |
| `model` | string | yes |  |

## RecencyThreshold

number (float)



## RefusalBlockStart

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |

## RefusalDelta

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"refusal"` | yes |  |
| `text` | string | yes |  |

## ResolvedBudget

object

The budget half of a resolution.

Separate from `ResolvedConfig` because it is resolved at a different moment
and over one axis more: config resolution answers "what is configured for
this namespace", which a customer can ask without naming a model, while a
budget is resolved over namespace *and* model and only means something once a
run has picked one.

| Field | Type | Required | Description |
|---|---|---|---|
| `budget` | [ContextBudget](#contextbudget) | yes | Always present: an org that has published nothing gets the platform default rather than no budget at all. |
| `model_id` | null or [ModelId](#modelid) | no | The model this was resolved for. `None` asks what an untagged model gets. |
| `namespace` | null or [Namespace](#namespace) | no | The scope this was resolved for — what was asked, not what answered. `BudgetSource::Policy::namespace` is the level that actually supplied it, and the two differ whenever a budget is inherited. |
| `source` | [BudgetSource](#budgetsource) | yes | The configuration scope and model tag that supplied the resolved context budget. |
| `warnings` | array of ([ConfigWarning](#configwarning)) | yes |  |

## ResolvedConfig

object

The resolution result.

| Field | Type | Required | Description |
|---|---|---|---|
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | Instruction plus bundle, as budgeted against. Summed from the frozen per-row estimates rather than recomputed, so it matches what the packer will use. |
| `instructions` | string,null | no | `None` when neither scope has an active instruction — distinct from an empty string, which would be an instruction that says nothing. |
| `namespace` | null or [Namespace](#namespace) | no | The scope this was resolved for. `None` is org-global. |
| `retention` | [RetentionRules](#retentionrules) | yes | What a replayed part looks like: which media comes back, how tool payloads are truncated, whether reasoning is replayed, what is anchored. Always present — when no scope has published a policy this is the platform default, because "no configuration" must not mean "replay every image forever". It contributes nothing to `estimated_tokens`: a policy is never sent to a provider, it only decides what of the ledger is, and that cost is counted per part. |
| `tools` | array of ([ToolDefinition](#tooldefinition)) | yes |  |
| `versions` | [ResolvedConfigVersions](#resolvedconfigversions) | yes | Which version rows produced a resolution. Persisted on a run's manifest so "what configuration did this request use" keeps its answer after newer versions are published. |
| `warnings` | array of ([ConfigWarning](#configwarning)) | yes |  |

## ResolvedConfigVersions

object

Which version rows produced a resolution. Persisted on a run's manifest so
"what configuration did this request use" keeps its answer after newer
versions are published.

| Field | Type | Required | Description |
|---|---|---|---|
| `instructions` | array of ([SystemInstructionId](#systeminstructionid)) | yes | Every instruction version that contributes, **root first** — the order they are concatenated in. A list rather than a global/namespace pair. The pair could not express the truth at any depth past one: a chain is arbitrarily deep, every level on `append` contributes, and a `replace` truncates at whichever level issued it. Reporting two ids meant a middle level either vanished from the answer or was misreported as the namespace's. |
| `retention_policy` | null or [RetentionPolicyId](#retentionpolicyid) | no | One id, not a list: at most one level's retention policy is ever in force, because a policy is selected whole rather than combined. |
| `tools` | array of ([ToolSchemaId](#toolschemaid)) | yes | Every tool bundle version that contributes, root first. Same reasoning. |

## ResponseBody

object

A completed turn: the model's output in order, its text concatenated, the tool calls now waiting for results, and the run's usage and cost.

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | [ChatId](#chatid) | yes |  |
| `closed_tool_calls` | array of ([ToolUseId](#tooluseid)) | yes | Tool calls this submission closed because they were never answered. |
| `compaction` | null or [Compaction](#compaction) | yes |  |
| `config_warnings` | array of ([Warning_ConfigWarning](#warning-configwarning)) | yes |  |
| `content` | array of ([ResponseContent](#responsecontent)) | yes | Every block the model produced, in order. |
| `cost` | null or [Money](#money) | yes | Settled charge in USD for this response. `null` while settlement is pending; read it later from `GET /api/v1/runs/{run_id}`. |
| `cost_currency` | string | yes |  |
| `model_served` | string,null | yes | The model identifier the provider reported, when it reported one. May differ in spelling from the catalogue name sent in the request. |
| `pending_tool_calls` | array of ([PendingToolCallBody](#pendingtoolcallbody)) | yes | Tool calls this turn opened, which the chat now waits on. Send a `tool_result` for each in the next request. |
| `provider_response_id` | string,null | yes |  |
| `replayed` | boolean | yes | `true` when this is the stored result of an earlier request with the same `idempotency_key`, returned instead of running the turn again. Warnings and compaction are not stored, so a replay reports none. |
| `run_id` | [RunId](#runid) | yes |  |
| `server_tools` | null or [ServerToolUsage](#servertoolusage) | yes | Provider-executed operations. Counts are successful billable operations, taken from terminal usage, not inferred from streamed attempts. |
| `stop_reason` | [StopReason](#stopreason) | yes |  |
| `text` | string | yes | The `text` blocks joined with a blank line. What most integrations map straight into a reply. |
| `translation_warnings` | array of ([Warning_TranslationWarning](#warning-translationwarning)) | yes |  |
| `usage` | [Usage](#usage) | yes |  |

## ResponseContent

[ResponseText](#responsetext) or [ResponseReasoning](#responsereasoning) or [ResponseRefusal](#responserefusal) or [ResponseToolCall](#responsetoolcall)

One block of the model's output, in the order the model produced it.

oneOf alternative 1: [ResponseText](#responsetext)

Text for the user.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"text"` | yes |  |

oneOf alternative 2: [ResponseReasoning](#responsereasoning)

Reasoning text the provider exposed. May be empty when the provider
hides its reasoning.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"reasoning"` | yes |  |

oneOf alternative 3: [ResponseRefusal](#responserefusal)

A refusal, in place of text.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"refusal"` | yes |  |

oneOf alternative 4: [ResponseToolCall](#responsetoolcall)

A tool call for the caller to execute. Answer it with a `tool_result`
part carrying the same `tool_use_id`.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | object | yes | The arguments object. When the model's output did not parse as JSON (it was cut off by `max_tokens`, for example), the raw string. |
| `name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `type` | `"tool_call"` | yes |  |

## ResponseEvent

[RunEvent](#runevent) or [ConfigWarningsEvent](#configwarningsevent) or [TranslationWarningsEvent](#translationwarningsevent) or [CompactingEvent](#compactingevent) or [BlockStartEvent](#blockstartevent) or [DeltaEvent](#deltaevent) or [BlockStopEvent](#blockstopevent) or [DoneEvent](#doneevent) or [ErrorEvent](#errorevent)

Each variant is one SSE frame: `event: <event>` followed by `data: <JSON>`.
The wrapper documents the framing; only its `data` is sent as the JSON payload.

oneOf alternative 1: [RunEvent](#runevent)

Always first: identifiers and tool calls closed by this submission.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [RunEventData](#runeventdata) | yes | Always first: identifiers and tool calls closed by this submission. |
| `event` | `"run"` | yes |  |

oneOf alternative 2: [ConfigWarningsEvent](#configwarningsevent)

Configuration warnings; emitted second, and may repeat after compaction.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [ConfigWarningsEventData](#configwarningseventdata) | yes | Configuration warnings; emitted second, and may repeat after compaction. |
| `event` | `"config_warnings"` | yes |  |

oneOf alternative 3: [TranslationWarningsEvent](#translationwarningsevent)

Model compatibility warnings before content starts.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [TranslationWarningsEventData](#translationwarningseventdata) | yes | Model compatibility warnings before content starts. |
| `event` | `"translation_warnings"` | yes |  |

oneOf alternative 4: [CompactingEvent](#compactingevent)

Compaction work the request is waiting for.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [CompactingEventData](#compactingeventdata) | yes | Compaction work the request is waiting for. |
| `event` | `"compacting"` | yes |  |

oneOf alternative 5: [BlockStartEvent](#blockstartevent)



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [BlockStart](#blockstart) | yes |  |
| `event` | `"block_start"` | yes |  |

oneOf alternative 6: [DeltaEvent](#deltaevent)



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [Delta](#delta) | yes |  |
| `event` | `"delta"` | yes |  |

oneOf alternative 7: [BlockStopEvent](#blockstopevent)



| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [BlockStopEventData](#blockstopeventdata) | yes |  |
| `event` | `"block_stop"` | yes |  |

oneOf alternative 8: [DoneEvent](#doneevent)

Terminal success. Null cost means settlement is pending; inspect the run later.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [DoneEventData](#doneeventdata) | yes | Terminal success. Null cost means settlement is pending; inspect the run later. |
| `event` | `"done"` | yes |  |

oneOf alternative 9: [ErrorEvent](#errorevent)

Terminal failure after the stream has opened.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [ErrorBody](#errorbody) | yes | Terminal failure after the stream has opened. |
| `event` | `"error"` | yes |  |

## ResponseReasoning

object

Reasoning text the provider exposed. May be empty when the provider
hides its reasoning.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"reasoning"` | yes |  |

## ResponseRefusal

object

A refusal, in place of text.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"refusal"` | yes |  |

## ResponseText

object

Text for the user.

| Field | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes |  |
| `type` | `"text"` | yes |  |

## ResponseToolCall

object

A tool call for the caller to execute. Answer it with a `tool_result`
part carrying the same `tool_use_id`.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | object | yes | The arguments object. When the model's output did not parse as JSON (it was cut off by `max_tokens`, for example), the raw string. |
| `name` | string | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `type` | `"tool_call"` | yes |  |

## RetentionPolicyId

string (uuid)



## RetentionPolicyOverriddenWarning

object

A nearer level's retention policy displaced a farther level's. This is how
the override is *meant* to work: a policy is a complete decision table and
is selected whole, so the farther one contributes nothing — not its rules
and not its defaults. It still warns, because the result looks identical
either way, and "my org-wide rule stopped applying" is otherwise
undiagnosable.

Both levels are named because at depth neither is guessable from the
other: "a namespace policy won" does not say which of four ancestors
stopped applying.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"retention_policy_overridden"` | yes |  |
| `displaced` | null or [Namespace](#namespace) | no | The nearest level whose policy is not being applied. |
| `namespace` | null or [Namespace](#namespace) | no | The level whose policy is in force. `None` is org-global. |

## RetentionPolicyResponse

[ConfigVersionResponse](#configversionresponse) and object

One retention-policy version with its rules.

No `estimated_tokens`: a policy is never sent to a provider, so unlike an
instruction or a bundle it has no token cost of its own. The version envelope
is shared with the other surfaces, so the field is reported as zero rather
than given a separate response shape for one absent number.

allOf alternative 1: [ConfigVersionResponse](#configversionresponse)

One published version, without its payload. The body of an instruction and
the definitions of a bundle are fetched per version, not listed — a history
endpoint that returned every body would fetch and decrypt a blob per row.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `id` | string | yes |  |
| `is_active` | boolean | yes |  |
| `mode` | null or [ConfigMode](#configmode) | no | How this version combines with the org-global one — `None` for the two surfaces that do not combine at all (retention policies and context budgets), where a mode would be a field the customer could set and nothing would read. |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `version` | integer (int32) | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `rules` | [RetentionRules](#retentionrules) | yes | The stored document. Private fields: holding one is proof it parsed. |

## RetentionRules

object

The stored document. Private fields: holding one is proof it parsed.

| Field | Type | Required | Description |
|---|---|---|---|
| `anchor_parts` | integer (int32); minimum 0 | no | How many parts at the head of the chat are pinned verbatim, never compacted and never truncated. `0` disables it. Counted in **parts**, not tokens, because the value a customer wants to express is "the task spec", which is a part boundary they can see — not a token count they would have to estimate and re-tune whenever they edited the spec. |
| `media` | [MediaRules](#mediarules) | no | The media section: which attachments are replayed. |
| `reasoning` | [RecencyThreshold](#recencythreshold) | no | How recent a part must be for its reasoning to be replayed. Defaults to the same window as media: a reasoning block's replay token is worthless to a model of another family anyway, and old reasoning is the least load-bearing, most expensive thing in a long agentic history. |
| `text` | [TextRules](#textrules) | no | Truncation policies for older user prompts and assistant messages. The current exchange and configured anchor remain protected. |
| `tools` | [ToolRules](#toolrules) | no | The tool section: how tool payloads are cut down. |
| `verbatim_within` | [RecencyThreshold](#recencythreshold) | no | How recent a payload must be to escape truncation entirely — tool results, tool arguments and prose alike. One setting for the whole document, deliberately, and the direct descendant of the old `truncate_within_frontier` flag. "Don't cut the tool output I just received" is not a per-tool judgement, and giving each family its own would put three definitions of "recent" back in a document whose entire point is that there is one. Hoisted out of the tool section when prose truncation arrived: the alternative was a second identical field on `TextRules`, which is how a shared metric quietly becomes two that drift. |
| `version` | integer (int32); minimum 0 | yes |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## RunEvent

object

Always first: identifiers and tool calls closed by this submission.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [RunEventData](#runeventdata) | yes | Always first: identifiers and tool calls closed by this submission. |
| `event` | `"run"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | [ChatId](#chatid) | yes |  |
| `closed_tool_calls` | array of ([ToolUseId](#tooluseid)) | yes |  |
| `run_id` | [RunId](#runid) | yes |  |

## RunEventData

object

Always first: identifiers and tool calls closed by this submission.

| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | [ChatId](#chatid) | yes |  |
| `closed_tool_calls` | array of ([ToolUseId](#tooluseid)) | yes |  |
| `run_id` | [RunId](#runid) | yes |  |

## RunId

string (uuid)



## RunInspection

object



| Field | Type | Required | Description |
|---|---|---|---|
| `chat_id` | [ChatId](#chatid) | yes |  |
| `completed_at` | string,null (date-time) | yes |  |
| `cost` | string,null | yes | What it was charged, once settled. `None` covers both "still running" and "finished but not yet settled" — a sweep may be seconds or hours behind, and the two are the same fact to a reader: no charge has landed yet. |
| `created_at` | string (date-time) | yes |  |
| `error` | string,null | yes | The failure, when there was one. Present on `failed` runs only. |
| `id` | [RunId](#runid) | yes |  |
| `is_live` | boolean | yes | Whether a running call has an unexpired durable ownership lease. False on expiry, before recovery necessarily closes the run. A paused worker is indistinguishable from a dead one and is fenced in either case. |
| `latency_ms` | integer,null (int32) | yes |  |
| `manifest` | object | yes | The context and configuration this call actually used. Opaque JSON on purpose: it is a record, not an API — the shape follows what the planner happened to record, and freezing it into typed fields would make every planner change a breaking change here. |
| `model` | string | yes | Our catalogue `name`, never the provider's wire string — the same identifier the customer sent and `/api/v1/models` lists. |
| `namespace` | null or [Namespace](#namespace) | yes | The scope the run's configuration resolved against. `None` is org-global. |
| `origin` | [RunOrigin](#runorigin) | yes | Whether the customer asked for this call or we did. A `compaction` run is one the customer never made and is still billed for, which is exactly why it is visible here rather than folded into inference. |
| `provider_request_id` | string,null | yes | The provider's own id for the call, for quoting at us in a support conversation. Already visible on the turn's terminal SSE event. |
| `settled_at` | string,null (date-time) | yes |  |
| `status` | [RunStatus](#runstatus) | yes | **The durable answer**, from the `runs` row and never from the cache. |
| `usage` | null or [Usage](#usage) | yes | `None` while in flight, or after interruption when usage was lost. Missing counts must never be presented as confirmed zero usage. |

## RunOrigin

`"customer"` or `"compaction"`



## RunStatus

`"running"` or `"succeeded"` or `"failed"` or `"cancelled"`



## RunUsageGroup

object



| Field | Type | Required | Description |
|---|---|---|---|
| `api_key_id` | null or [ApiKeyId](#apikeyid) | no |  |
| `dimension` | `"run"` | yes |  |
| `entry_type` | [BalanceEntryType](#balanceentrytype) | yes |  |
| `last_occurred_at` | string,null (date-time) | no | When this run last recorded an event — what the list is ordered by, since a run has no single instant of its own here. |
| `model_name` | string,null | no |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `run_id` | null or [RunId](#runid) | no |  |

## ServerToolUsage

object

Provider-executed operations. Counts are successful billable operations,
taken from terminal usage, not inferred from streamed attempts.

| Field | Type | Required | Description |
|---|---|---|---|
| `document_search_calls` | integer (int32); minimum 0 | yes |  |
| `provider_cost` | null or [Money](#money) | no | Total provider charge, including tokens, when document tools were used. |

## StopReason

`"end_turn"` or `"max_tokens"` or `"tool_use"` or `"stop_sequence"` or `"refusal"` or `"aborted"`



## SubmittedMedia

object

An inline attachment. Supply standard base64 bytes without a data-URI prefix and the matching MIME type. Model-specific format, byte and count limits apply.

| Field | Type | Required | Description |
|---|---|---|---|
| `data_base64` | string | yes | Standard base64, no data-URI prefix. Named for its encoding because the field is what a customer has to get right, and `data` alone invites a raw string or a `data:` URL — both of which would decode to something, quietly, and be sealed as an image nothing can open. |
| `filename` | string,null | no | Shown in a UI, and named in the stub that replaces this attachment once retention drops it — which is why it is worth carrying even though no provider requires it. |
| `mime` | string | yes | Explicit MIME type, such as `image/png` or `application/pdf`. Validated against the selected model's configured media capabilities before the turn is persisted. Supported formats differ by model and carrier. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## SubmittedPart

[SubmittedPrompt](#submittedprompt) or [SubmittedToolResult](#submittedtoolresult)

One part the customer is submitting.

Only the two **user-role** kinds are representable. A customer cannot author
a `message`, a `reasoning` block or a `tool_call` — those come back from a
model — so leaving them out makes the impossible submission unconstructible
rather than merely rejected, and there is no branch anywhere below that has
to check for one.

oneOf alternative 1: [SubmittedPrompt](#submittedprompt)

The customer's own turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `media` | array of ([SubmittedMedia](#submittedmedia)) | no |  |
| `text` | string | no |  |
| `type` | `"prompt"` | yes |  |

oneOf alternative 2: [SubmittedToolResult](#submittedtoolresult)

The answer to a tool call the model made.

| Field | Type | Required | Description |
|---|---|---|---|
| `is_error` | boolean | no | Anthropic's `tool_result.is_error`. Dropped when translating to a provider with no equivalent. |
| `media` | array of ([SubmittedMedia](#submittedmedia)) | no | What the tool returned besides text — a screenshot, most often, which is the canonical case the media tier exists for. |
| `text` | string | no |  |
| `tool_name` | string | yes | The tool's name, as it was called. Required because Google puts the function name on the response and nothing else on the row could supply it without joining back to the call. |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `trust` | [TrustLevel](#trustlevel) | no | How far this content is trusted, driving the write-time hygiene limits. Defaults to `customer_data`, the safe assumption. |
| `type` | `"tool_result"` | yes |  |

## SubmittedPrompt

object

The customer's own turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `media` | array of ([SubmittedMedia](#submittedmedia)) | no |  |
| `text` | string | no |  |
| `type` | `"prompt"` | yes |  |

## SubmittedToolResult

object

The answer to a tool call the model made.

| Field | Type | Required | Description |
|---|---|---|---|
| `is_error` | boolean | no | Anthropic's `tool_result.is_error`. Dropped when translating to a provider with no equivalent. |
| `media` | array of ([SubmittedMedia](#submittedmedia)) | no | What the tool returned besides text — a screenshot, most often, which is the canonical case the media tier exists for. |
| `text` | string | no |  |
| `tool_name` | string | yes | The tool's name, as it was called. Required because Google puts the function name on the response and nothing else on the row could supply it without joining back to the call. |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |
| `trust` | [TrustLevel](#trustlevel) | no | How far this content is trusted, driving the write-time hygiene limits. Defaults to `customer_data`, the safe assumption. |
| `type` | `"tool_result"` | yes |  |

## SystemInstructionId

string (uuid)



## SystemPrompt

string



Constraints:
```json
{
  "maxLength": 100000,
  "minLength": 1
}
```

## TextBlockStart

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |

## TextDelta

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"text"` | yes |  |
| `text` | string | yes |  |

## TextRules

object

Truncation policies for older user prompts and assistant messages. The current exchange and configured anchor remain protected.

| Field | Type | Required | Description |
|---|---|---|---|
| `message` | [Truncation](#truncation) | no | Applied to an assistant `message`'s text. Separate from `prompt` for the same reason a tool's arguments and result are separate: they are different sizes with different value. A customer pasting logs wants their own input cut; a customer whose model writes long answers wants the replies cut. |
| `prompt` | [Truncation](#truncation) | no | Applied to a `prompt` part's text. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## TierMode

`"cliff"` or `"graduated"`



## TokenEstimate

integer (int32)

A count of estimated tokens. Always non-negative; saturates rather than
overflowing, because a budget calculation must never wrap into a small
number and wave through a request that cannot fit.

## TombstoneReason

`"abandoned"` or `"cancelled"` or `"expired"`



## ToolCallBlockStart

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_call"` | yes |  |
| `tool_name` | [ToolName](#toolname) | yes |  |
| `tool_use_id` | [ToolUseId](#tooluseid) | yes |  |

## ToolDefinition

object

A named tool with a description and JSON Schema parameters. Parameters must be a valid schema describing an object; external schema references are not supported.

| Field | Type | Required | Description |
|---|---|---|---|
| `description` | string,null | no |  |
| `input_schema` | object | yes |  |
| `name` | [ToolName](#toolname) | yes |  |
| `strict` | boolean,null | no | Ask the provider to guarantee the tool call validates against the schema. `None` means the customer did not say, which is distinct from `false`: the providers' own defaults differ, and translating an unstated preference into an explicit `false` would silently opt a customer out of a guarantee they never declined. Dropped when targeting Google, which has no equivalent. |

## ToolFieldDroppedWarning

object

A tool definition carried a field the target provider has no equivalent
for (Google has no `strict`; only Anthropic has document `title` /
`context`). Dropped on translation.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_field_dropped"` | yes |  |
| `field` | string | yes |  |
| `tool` | [ToolName](#toolname) | yes |  |

## ToolInputDelta

object



| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `"tool_input"` | yes |  |
| `text` | string | yes |  |

## ToolName

string



## ToolNamePattern

string



## ToolRetentionRule

[ToolTreatment](#tooltreatment) and object

One tool rule.

allOf alternative 1: [ToolTreatment](#tooltreatment)

How one tool's two payloads are treated. Arguments and results are separate
because they are separate parts with separate sizes — a `write_file` call's
arguments routinely dwarf its result, and a `search` call's result dwarfs its
arguments.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | [Truncation](#truncation) | no | Applied to the `tool_call` arguments. |
| `result` | [Truncation](#truncation) | no | Applied to the `tool_result` payload. |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `tool` | [ToolNamePattern](#toolnamepattern) | yes |  |

## ToolRules

object

The tool section: how tool payloads are cut down.

| Field | Type | Required | Description |
|---|---|---|---|
| `default` | [ToolTreatment](#tooltreatment) | no | Applied when no rule matches. |
| `errors` | null or [Truncation](#truncation) | no | Replaces the matched `result` treatment on a part flagged `is_error`. `None` means errors follow the ordinary rule. Worth its own knob because failures split the customer base cleanly: for one, a stack trace is the most valuable thing in the history; for the next, it is a retry loop's worth of noise. |
| `mark_truncations` | boolean | no | Whether a cut payload carries a visible marker of what was elided. Defaults **on**, and deliberately not a per-rule choice: a model that cannot tell output was truncated will confidently invent the missing middle, and that failure is not a per-tool judgement call. |
| `rules` | array of ([ToolRetentionRule](#toolretentionrule)) | no |  |

Constraints:
```json
{
  "additionalProperties": false
}
```

## ToolSchemaId

string (uuid)



## ToolSchemaResponse

[ConfigVersionResponse](#configversionresponse) and object

One tool-bundle version with its definitions — the single-version shape, the
sibling of `InstructionResponse`. The history endpoint returns bare
`ConfigVersionResponse`s, because definitions are sealed and listing them
would be a fetch and a decrypt per row.

allOf alternative 1: [ConfigVersionResponse](#configversionresponse)

One published version, without its payload. The body of an instruction and
the definitions of a bundle are fetched per version, not listed — a history
endpoint that returned every body would fetch and decrypt a blob per row.

| Field | Type | Required | Description |
|---|---|---|---|
| `created_at` | string (date-time) | yes |  |
| `estimated_tokens` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `id` | string | yes |  |
| `is_active` | boolean | yes |  |
| `mode` | null or [ConfigMode](#configmode) | no | How this version combines with the org-global one — `None` for the two surfaces that do not combine at all (retention policies and context budgets), where a mode would be a field the customer could set and nothing would read. |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `version` | integer (int32) | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `definitions` | array of ([ToolDefinition](#tooldefinition)) | yes |  |

## ToolShadowedWarning

object

A namespace tool replaced a global tool of the same name. The union is
keyed on name, so this is how an override is *meant* to work — but it is
also exactly what a typo in a tool name looks like from the outside.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_shadowed"` | yes |  |
| `name` | [ToolName](#toolname) | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

## ToolTreatment

object

How one tool's two payloads are treated. Arguments and results are separate
because they are separate parts with separate sizes — a `write_file` call's
arguments routinely dwarf its result, and a `search` call's result dwarfs its
arguments.

| Field | Type | Required | Description |
|---|---|---|---|
| `arguments` | [Truncation](#truncation) | no | Applied to the `tool_call` arguments. |
| `result` | [Truncation](#truncation) | no | Applied to the `tool_result` payload. |

Constraints:
```json
{
  "additionalProperties": false
}
```

## ToolUseId

string



## TranslationWarning

[MediaLimitExceededWarning](#medialimitexceededwarning) or [MediaUnsupportedWarning](#mediaunsupportedwarning) or [ReasoningNotSupportedWarning](#reasoningnotsupportedwarning) or [ReasoningCannotBeDisabledWarning](#reasoningcannotbedisabledwarning) or [ReasoningEffortClampedWarning](#reasoningeffortclampedwarning) or [ToolFieldDroppedWarning](#toolfielddroppedwarning) or [ForeignArtifactsStrippedWarning](#foreignartifactsstrippedwarning)



oneOf alternative 1: [MediaLimitExceededWarning](#medialimitexceededwarning)

Historical attachments exceeded the target model's byte or request-count
limit and were replaced with text markers. Aggregated per (MIME, carrier).

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_limit_exceeded"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

oneOf alternative 2: [MediaUnsupportedWarning](#mediaunsupportedwarning)

Media the target model cannot receive in this position was replaced with
a text marker. Aggregated per (MIME, carrier), separately from retention.

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_unsupported"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

oneOf alternative 3: [ReasoningNotSupportedWarning](#reasoningnotsupportedwarning)

Reasoning was requested but the target model does not support it, so the
request went without a thinking configuration.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_not_supported"` | yes |  |
| `model` | string | yes |  |

oneOf alternative 4: [ReasoningCannotBeDisabledWarning](#reasoningcannotbedisabledwarning)

Reasoning was switched **off** and the provider has no way to express
that, so thinking remained enabled.

The mirror of `ReasoningNotSupported`(Self::ReasoningNotSupported), and
the reason it is a warning rather than something to swallow: the customer
gets more than they asked for *and is billed for it*. The Responses API
has no "off" every vendor behind it accepts — the candidates are
model-specific and rejected elsewhere — so omitting the block is the only
universally legal shape, and omitting it means the default applies.
Silence here would make an explicit `off` look honoured while it quietly
costs money on every turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_cannot_be_disabled"` | yes |  |
| `model` | string | yes |  |

oneOf alternative 5: [ReasoningEffortClampedWarning](#reasoningeffortclampedwarning)

The requested effort tier is above what this model accepts and was
lowered. The run still happened, at less depth than asked for.

| Field | Type | Required | Description |
|---|---|---|---|
| `applied` | [ReasoningEffort](#reasoningeffort) | yes |  |
| `code` | `"reasoning_effort_clamped"` | yes |  |
| `model` | string | yes |  |
| `requested` | [ReasoningEffort](#reasoningeffort) | yes |  |

oneOf alternative 6: [ToolFieldDroppedWarning](#toolfielddroppedwarning)

A tool definition carried a field the target provider has no equivalent
for (Google has no `strict`; only Anthropic has document `title` /
`context`). Dropped on translation.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_field_dropped"` | yes |  |
| `field` | string | yes |  |
| `tool` | [ToolName](#toolname) | yes |  |

oneOf alternative 7: [ForeignArtifactsStrippedWarning](#foreignartifactsstrippedwarning)

Replay artifacts the target cannot accept were stripped: ones authored by
a different model family, or Gemini signatures minted on the other Google
endpoint (Vertex and the Gemini API reject each other's). A signature
from elsewhere is meaningless, or a hard rejection, to the target.
Expected whenever a chat hot-swaps providers, or a Gemini chat moves
between Twigg's key and the customer's own, and reported so a customer
comparing two runs of the same chat can see why the second one lost its
reasoning continuity.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"foreign_artifacts_stripped"` | yes |  |
| `count` | integer; minimum 0 | yes |  |

## TranslationWarningsEvent

object

Model compatibility warnings before content starts.

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | [TranslationWarningsEventData](#translationwarningseventdata) | yes | Model compatibility warnings before content starts. |
| `event` | `"translation_warnings"` | yes |  |

Event data:

| Field | Type | Required | Description |
|---|---|---|---|
| `warnings` | array of ([Warning_TranslationWarning](#warning-translationwarning)) | yes |  |

## TranslationWarningsEventData

object

Model compatibility warnings before content starts.

| Field | Type | Required | Description |
|---|---|---|---|
| `warnings` | array of ([Warning_TranslationWarning](#warning-translationwarning)) | yes |  |

## Truncation

[TruncationKeep](#truncationkeep) or [TruncationAlways](#truncationalways) or [TruncationOver](#truncationover)

Keep content unchanged, always truncate to a token target, or truncate only when it exceeds a threshold. Choose the head, tail, or both ends to retain.

oneOf alternative 1: [TruncationKeep](#truncationkeep)

Replay it whole, however large.

| Field | Type | Required | Description |
|---|---|---|---|
| `mode` | `"keep"` | yes |  |

oneOf alternative 2: [TruncationAlways](#truncationalways)

Always cut to `to` tokens.

| Field | Type | Required | Description |
|---|---|---|---|
| `keep` | [TruncationShape](#truncationshape) | no |  |
| `mode` | `"always"` | yes |  |
| `to` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |

oneOf alternative 3: [TruncationOver](#truncationover)

Cut to `to` tokens, but only once the payload exceeds `over`. The common
shape: small results replay verbatim, a runaway one is clipped.

| Field | Type | Required | Description |
|---|---|---|---|
| `keep` | [TruncationShape](#truncationshape) | no |  |
| `mode` | `"over"` | yes |  |
| `over` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `to` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |

## TruncationAlways

object

Always cut to `to` tokens.

| Field | Type | Required | Description |
|---|---|---|---|
| `keep` | [TruncationShape](#truncationshape) | no |  |
| `mode` | `"always"` | yes |  |
| `to` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |

## TruncationKeep

object

Replay it whole, however large.

| Field | Type | Required | Description |
|---|---|---|---|
| `mode` | `"keep"` | yes |  |

## TruncationOver

object

Cut to `to` tokens, but only once the payload exceeds `over`. The common
shape: small results replay verbatim, a runaway one is clipped.

| Field | Type | Required | Description |
|---|---|---|---|
| `keep` | [TruncationShape](#truncationshape) | no |  |
| `mode` | `"over"` | yes |  |
| `over` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |
| `to` | [TokenEstimate](#tokenestimate) | yes | A count of estimated tokens. Always non-negative; saturates rather than overflowing, because a budget calculation must never wrap into a small number and wave through a request that cannot fit. |

## TruncationShape

`"head"` or `"tail"` or `"head_and_tail"`



## TrustLevel

`"trusted"` or `"customer_data"` or `"untrusted"`



## UpdateBody

string



## UpdateId

string (uuid)



## UpdateKind

`"release"` or `"notice"` or `"maintenance"`



## UpdateTitle

string



## UpdateVersion

string



## Uptime

string

A measured ratio from 0 to 1; multiply by 100 to display a percentage.

## Usage

object



| Field | Type | Required | Description |
|---|---|---|---|
| `cache_read_tokens` | integer (int32); minimum 0 | yes | Served from the provider's prompt cache, billed below base rate. |
| `cache_write_tokens` | integer (int32); minimum 0 | yes | Written to the provider's prompt cache, billed above base rate. The Responses API does not meter this and reports zero. |
| `input_tokens` | integer (int32); minimum 0 | yes | Billable input at the base rate. Excludes both cache figures. |
| `output_tokens` | integer (int32); minimum 0 | yes | Everything the model produced, reasoning included. |
| `reasoning_tokens` | integer (int32); minimum 0 | yes | The thinking share of `Self::output_tokens`. **A subset, not an addition** — reported for visibility, never summed into the total. |

## UsageBin

`"hour"` or `"day"`



## UsageDimension

`"model"` or `"namespace"` or `"api_key"` or `"entry_type"` or `"organisation"` or `"run"`



## UsageGroup

[ModelUsageGroup](#modelusagegroup) or [NamespaceUsageGroup](#namespaceusagegroup) or [ApiKeyUsageGroup](#apikeyusagegroup) or [OrganisationUsageGroup](#organisationusagegroup) or [EntryTypeUsageGroup](#entrytypeusagegroup) or [RunUsageGroup](#runusagegroup)

Which slice a row is, and everything needed to label it.

A tagged union rather than a `key: String`, because the labels are not
interchangeable: a model row needs the id *and* the human name, a namespace
row's key can legitimately be absent (the org-global scope), and a run row
carries the context you would drill from.

oneOf alternative 1: [ModelUsageGroup](#modelusagegroup)



| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"model"` | yes |  |
| `model_id` | null or [ModelId](#modelid) | no | Absent on a credit, which names no model. |
| `model_name` | string,null | no |  |

oneOf alternative 2: [NamespaceUsageGroup](#namespaceusagegroup)

`None` is the org-global scope — chats created without a namespace — not
an unknown one.

| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"namespace"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |

oneOf alternative 3: [ApiKeyUsageGroup](#apikeyusagegroup)

`None` covers events with no key: console-initiated work, and credits.

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key_id` | null or [ApiKeyId](#apikeyid) | no |  |
| `dimension` | `"api_key"` | yes |  |

oneOf alternative 4: [OrganisationUsageGroup](#organisationusagegroup)

Usage grouped by organisation. Labels are null when unavailable.

| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"organisation"` | yes |  |
| `name` | string,null | no |  |
| `organisation_id` | [OrganisationId](#organisationid) | yes |  |
| `slug` | string,null | no |  |

oneOf alternative 5: [EntryTypeUsageGroup](#entrytypeusagegroup)



| Field | Type | Required | Description |
|---|---|---|---|
| `dimension` | `"entry_type"` | yes |  |
| `entry_type` | [BalanceEntryType](#balanceentrytype) | yes |  |

oneOf alternative 6: [RunUsageGroup](#runusagegroup)



| Field | Type | Required | Description |
|---|---|---|---|
| `api_key_id` | null or [ApiKeyId](#apikeyid) | no |  |
| `dimension` | `"run"` | yes |  |
| `entry_type` | [BalanceEntryType](#balanceentrytype) | yes |  |
| `last_occurred_at` | string,null (date-time) | no | When this run last recorded an event — what the list is ordered by, since a run has no single instant of its own here. |
| `model_name` | string,null | no |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `run_id` | null or [RunId](#runid) | no |  |

## UsageReport

object

A report: the rows, plus the shape of the question they answer.

The window is echoed because it is not what the caller literally asked for —
a range preset is resolved against the clock and rounded to the hour — and a
chart that cannot state its own axis is a chart nobody can check.

| Field | Type | Required | Description |
|---|---|---|---|
| `bin` | null or [UsageBin](#usagebin) | no |  |
| `cached` | boolean | yes | Whether these rows came from the cache. Surfaced rather than hidden: the console tells the customer their usage lags by minutes, and this is the honest version of that sentence. |
| `group_by` | null or [UsageDimension](#usagedimension) | no |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `rows` | array of ([UsageRow](#usagerow)) | yes |  |
| `window` | [UsageWindow](#usagewindow) | yes | A validated `[from, to)` range over `occurred_at`. Half-open, so consecutive windows tile without double-counting the instant on the boundary — an event at exactly midnight belongs to one day, not two. |

## UsageRow

[UsageTotals](#usagetotals) and object

One row of a report: an optional bucket, an optional group, and the measures.

Both are `Option` because the same row type serves all four query shapes, and
which of them are populated is a property of the query rather than of the
row — the report echoes the query shape beside the rows so a reader never has
to infer it.

allOf alternative 1: [UsageTotals](#usagetotals)

The measures every row carries, whatever it is grouped by.

`runs` counts **distinct run ids**, not events: a run that was compacted
produces two events naming it, and counting rows would report the busy runs
twice. Credits share the analytics source table but are excluded from this
usage report.

| Field | Type | Required | Description |
|---|---|---|---|
| `cache_read_tokens` | integer (int64) | yes |  |
| `cache_write_tokens` | integer (int64) | yes |  |
| `cost` | [Money](#money) | yes | Model-call spend in the requested window. Credits share the source table but are excluded from usage reports. |
| `input_tokens` | integer (int64) | yes |  |
| `output_tokens` | integer (int64) | yes |  |
| `reasoning_tokens` | integer (int64) | yes | A subset of `output_tokens`, never an addition — and priced at zero, so it explains a cost rather than adding to it. |
| `runs` | integer (int64) | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `bucket` | string,null (date-time) | no |  |
| `group` | null or [UsageGroup](#usagegroup) | no | Which slice a row is, and everything needed to label it. A tagged union rather than a `key: String`, because the labels are not interchangeable: a model row needs the id *and* the human name, a namespace row's key can legitimately be absent (the org-global scope), and a run row carries the context you would drill from. |

## UsageTotals

object

The measures every row carries, whatever it is grouped by.

`runs` counts **distinct run ids**, not events: a run that was compacted
produces two events naming it, and counting rows would report the busy runs
twice. Credits share the analytics source table but are excluded from this
usage report.

| Field | Type | Required | Description |
|---|---|---|---|
| `cache_read_tokens` | integer (int64) | yes |  |
| `cache_write_tokens` | integer (int64) | yes |  |
| `cost` | [Money](#money) | yes | Model-call spend in the requested window. Credits share the source table but are excluded from usage reports. |
| `input_tokens` | integer (int64) | yes |  |
| `output_tokens` | integer (int64) | yes |  |
| `reasoning_tokens` | integer (int64) | yes | A subset of `output_tokens`, never an addition — and priced at zero, so it explains a cost rather than adding to it. |
| `runs` | integer (int64) | yes |  |

## UsageWindow

object

A validated `[from, to)` range over `occurred_at`.

Half-open, so consecutive windows tile without double-counting the instant
on the boundary — an event at exactly midnight belongs to one day, not two.

| Field | Type | Required | Description |
|---|---|---|---|
| `from` | string (date-time) | yes |  |
| `to` | string (date-time) | yes |  |

## UserMetadata

object

Customer JSON object, at most 1024 bytes when serialized compactly. Defaults to an empty object.

## Warning_ConfigWarning

[ToolShadowedWarning](#toolshadowedwarning) or [GlobalInstructionReplacedWarning](#globalinstructionreplacedwarning) or [GlobalToolsReplacedWarning](#globaltoolsreplacedwarning) or [RetentionPolicyOverriddenWarning](#retentionpolicyoverriddenwarning) or [ContextBudgetResolvedAtWarning](#contextbudgetresolvedatwarning) or [HistoryDroppedWarning](#historydroppedwarning) or [ProviderKeyPausedWarning](#providerkeypausedwarning) or [ProviderKeyFellThroughWarning](#providerkeyfellthroughwarning) and object



allOf alternative 1: [ToolShadowedWarning](#toolshadowedwarning) or [GlobalInstructionReplacedWarning](#globalinstructionreplacedwarning) or [GlobalToolsReplacedWarning](#globaltoolsreplacedwarning) or [RetentionPolicyOverriddenWarning](#retentionpolicyoverriddenwarning) or [ContextBudgetResolvedAtWarning](#contextbudgetresolvedatwarning) or [HistoryDroppedWarning](#historydroppedwarning) or [ProviderKeyPausedWarning](#providerkeypausedwarning) or [ProviderKeyFellThroughWarning](#providerkeyfellthroughwarning)

Something legal but worth saying out loud.

oneOf alternative 1: [ToolShadowedWarning](#toolshadowedwarning)

A namespace tool replaced a global tool of the same name. The union is
keyed on name, so this is how an override is *meant* to work — but it is
also exactly what a typo in a tool name looks like from the outside.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_shadowed"` | yes |  |
| `name` | [ToolName](#toolname) | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 2: [GlobalInstructionReplacedWarning](#globalinstructionreplacedwarning)

The namespace's instruction is in `replace` mode, so the org-global
instruction is not being sent at all.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_instruction_replaced"` | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 3: [GlobalToolsReplacedWarning](#globaltoolsreplacedwarning)

The namespace's tool bundle is in `replace` mode, so the global bundle
is not being sent at all — including tools the customer may assume are
always present.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"global_tools_replaced"` | yes |  |
| `dropped` | integer; minimum 0 | yes |  |
| `namespace` | [Namespace](#namespace) | yes |  |

oneOf alternative 4: [RetentionPolicyOverriddenWarning](#retentionpolicyoverriddenwarning)

A nearer level's retention policy displaced a farther level's. This is how
the override is *meant* to work: a policy is a complete decision table and
is selected whole, so the farther one contributes nothing — not its rules
and not its defaults. It still warns, because the result looks identical
either way, and "my org-wide rule stopped applying" is otherwise
undiagnosable.

Both levels are named because at depth neither is guessable from the
other: "a namespace policy won" does not say which of four ancestors
stopped applying.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"retention_policy_overridden"` | yes |  |
| `displaced` | null or [Namespace](#namespace) | no | The nearest level whose policy is not being applied. |
| `namespace` | null or [Namespace](#namespace) | no | The level whose policy is in force. `None` is org-global. |

oneOf alternative 5: [ContextBudgetResolvedAtWarning](#contextbudgetresolvedatwarning)

The budget in force did not come from the plainest place: either an
ancestor level supplied it, or a model tag at the winning level displaced
that level's untagged policy — or both.

Two facts, one warning, because either alone misleads. "The budget came
from `acme`" and "the budget came from `acme`, tagged to this model" send
an admin to different rows, and the second is the only one that explains
why a sibling model in the same namespace behaves differently.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"context_budget_resolved_at"` | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The level that supplied it. `None` is the org-global scope. |
| `tagged_model` | null or [ModelId](#modelid) | no | `Some` when that level's tag for this model beat its untagged policy. |

oneOf alternative 6: [HistoryDroppedWarning](#historydroppedwarning)

Oldest ledger parts were omitted because no complete context fitted.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"history_dropped"` | yes |  |
| `dropped` | integer (int64) | yes |  |
| `end_ordinal` | integer (int64) | yes |  |
| `start_ordinal` | integer (int64) | yes |  |

oneOf alternative 7: [ProviderKeyPausedWarning](#providerkeypausedwarning)

The organisation's own provider key for this model is paused, so the
turn runs on Twigg's key and is billed at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_paused"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no | The scope the paused key belongs to. `None` is org-global. |

oneOf alternative 8: [ProviderKeyFellThroughWarning](#providerkeyfellthroughwarning)

The organisation's own provider key failed before any output, and the
model allows falling back, so the turn runs on Twigg's key and is billed
at the full rate.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"provider_key_fell_through"` | yes |  |
| `key_last4` | string | yes |  |
| `namespace` | null or [Namespace](#namespace) | no |  |
| `reason` | [LlmErrorKind](#llmerrorkind) | yes | How the provider refused the key: `auth`, `rate_limited`, … |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `message` | string | yes |  |

## Warning_TranslationWarning

[MediaLimitExceededWarning](#medialimitexceededwarning) or [MediaUnsupportedWarning](#mediaunsupportedwarning) or [ReasoningNotSupportedWarning](#reasoningnotsupportedwarning) or [ReasoningCannotBeDisabledWarning](#reasoningcannotbedisabledwarning) or [ReasoningEffortClampedWarning](#reasoningeffortclampedwarning) or [ToolFieldDroppedWarning](#toolfielddroppedwarning) or [ForeignArtifactsStrippedWarning](#foreignartifactsstrippedwarning) and object



allOf alternative 1: [MediaLimitExceededWarning](#medialimitexceededwarning) or [MediaUnsupportedWarning](#mediaunsupportedwarning) or [ReasoningNotSupportedWarning](#reasoningnotsupportedwarning) or [ReasoningCannotBeDisabledWarning](#reasoningcannotbedisabledwarning) or [ReasoningEffortClampedWarning](#reasoningeffortclampedwarning) or [ToolFieldDroppedWarning](#toolfielddroppedwarning) or [ForeignArtifactsStrippedWarning](#foreignartifactsstrippedwarning)



oneOf alternative 1: [MediaLimitExceededWarning](#medialimitexceededwarning)

Historical attachments exceeded the target model's byte or request-count
limit and were replaced with text markers. Aggregated per (MIME, carrier).

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_limit_exceeded"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

oneOf alternative 2: [MediaUnsupportedWarning](#mediaunsupportedwarning)

Media the target model cannot receive in this position was replaced with
a text marker. Aggregated per (MIME, carrier), separately from retention.

| Field | Type | Required | Description |
|---|---|---|---|
| `carrier` | [MediaCarrier](#mediacarrier) | yes |  |
| `code` | `"media_unsupported"` | yes |  |
| `count` | integer; minimum 0 | yes |  |
| `mime` | [MimeType](#mimetype) | yes |  |

oneOf alternative 3: [ReasoningNotSupportedWarning](#reasoningnotsupportedwarning)

Reasoning was requested but the target model does not support it, so the
request went without a thinking configuration.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_not_supported"` | yes |  |
| `model` | string | yes |  |

oneOf alternative 4: [ReasoningCannotBeDisabledWarning](#reasoningcannotbedisabledwarning)

Reasoning was switched **off** and the provider has no way to express
that, so thinking remained enabled.

The mirror of `ReasoningNotSupported`(Self::ReasoningNotSupported), and
the reason it is a warning rather than something to swallow: the customer
gets more than they asked for *and is billed for it*. The Responses API
has no "off" every vendor behind it accepts — the candidates are
model-specific and rejected elsewhere — so omitting the block is the only
universally legal shape, and omitting it means the default applies.
Silence here would make an explicit `off` look honoured while it quietly
costs money on every turn.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"reasoning_cannot_be_disabled"` | yes |  |
| `model` | string | yes |  |

oneOf alternative 5: [ReasoningEffortClampedWarning](#reasoningeffortclampedwarning)

The requested effort tier is above what this model accepts and was
lowered. The run still happened, at less depth than asked for.

| Field | Type | Required | Description |
|---|---|---|---|
| `applied` | [ReasoningEffort](#reasoningeffort) | yes |  |
| `code` | `"reasoning_effort_clamped"` | yes |  |
| `model` | string | yes |  |
| `requested` | [ReasoningEffort](#reasoningeffort) | yes |  |

oneOf alternative 6: [ToolFieldDroppedWarning](#toolfielddroppedwarning)

A tool definition carried a field the target provider has no equivalent
for (Google has no `strict`; only Anthropic has document `title` /
`context`). Dropped on translation.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"tool_field_dropped"` | yes |  |
| `field` | string | yes |  |
| `tool` | [ToolName](#toolname) | yes |  |

oneOf alternative 7: [ForeignArtifactsStrippedWarning](#foreignartifactsstrippedwarning)

Replay artifacts the target cannot accept were stripped: ones authored by
a different model family, or Gemini signatures minted on the other Google
endpoint (Vertex and the Gemini API reject each other's). A signature
from elsewhere is meaningless, or a hard rejection, to the target.
Expected whenever a chat hot-swaps providers, or a Gemini chat moves
between Twigg's key and the customer's own, and reported so a customer
comparing two runs of the same chat can see why the second one lost its
reasoning continuity.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | `"foreign_artifacts_stripped"` | yes |  |
| `count` | integer; minimum 0 | yes |  |

allOf alternative 2: object



| Field | Type | Required | Description |
|---|---|---|---|
| `message` | string | yes |  |
