> For the complete documentation index, see [llms.txt](https://docs.otari.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.otari.ai/reference/api-reference.md).

# API reference

Otari serves an OpenAPI document at `/api/v1/openapi.json` and interactive API docs at `/api/v1/docs` by default. The repository also commits the generated [OpenAPI specification](https://github.com/mozilla-ai/otari/blob/v0.37.0/docs/public/openapi.json) and [Postman collection](https://github.com/mozilla-ai/otari/blob/v0.37.0/docs/public/otari.postman_collection.json). Those generated artifacts are the source of truth for paths, parameters, and schemas.

## Base URL

| Where Otari runs                           | Base URL                                                                            |
| ------------------------------------------ | ----------------------------------------------------------------------------------- |
| otari.ai, an account in the United States  | `https://api.otari.ai/api`                                                          |
| otari.ai, an account in the European Union | `https://eu.api.otari.ai/api`                                                       |
| Your own gateway                           | `https://<your-otari-host>/api`, or `http://localhost:8000/api` on your own machine |

Every endpoint lives under the base URL, at `/v1/...`: an OpenAI-style request goes to `<base URL>/v1/chat/completions`, an Anthropic-style one to `<base URL>/v1/messages`. SDKs append their own paths, so each takes the base URL in the form it expects:

| Client                                  | Base URL setting                                                                              |
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
| OpenAI SDKs and OpenAI-compatible tools | the base URL plus `/v1`, for example `https://api.otari.ai/api/v1`                            |
| Anthropic SDKs and Claude Code          | the base URL itself, for example `https://api.otari.ai/api`, since they append `/v1/messages` |

Leaving `/api` off is the most common mistake. On otari.ai, a request to `https://api.otari.ai/v1/...` never reaches Otari: a small one answers `404 Not Found`, and a large one, such as any Claude Code request, answers `403 Forbidden`, which Claude Code reports as "Failed to authenticate". Your own gateway answers a request under `/v1` with a 404 whose detail names the same path under `/api/v1`.

An otari.ai key belongs to one region. Sent to the other region's URL, it answers `421` naming the host it belongs to.

OTLP ingest is a sibling of the API at `/otlp`: point `OTEL_EXPORTER_OTLP_ENDPOINT` at `http://localhost:8000/otlp` and the exporter appends `/v1/traces`, `/v1/logs` and `/v1/metrics`.

## Authentication

Otari accepts an API key or the master key in any of these forms:

```
Authorization: Bearer <token>
Otari-Key: <token>
Otari-Key: Bearer <token>
x-api-key: <token>
```

Use API keys for inference. The master key is a deployment-wide administrative credential. Dashboard sessions authenticate browser requests, but deployment-wide operations also require operator authority. Where a tenant needs one of those operations, a separately scoped endpoint serves it to the caller's own organization: `/api/v1/organizations/me/usage` for usage, and `/api/v1/organizations/me/keys` for a member's own API keys.

## Core inference APIs

Otari implements three completion surfaces:

* `POST /api/v1/chat/completions`, OpenAI Chat Completions
* `POST /api/v1/messages` and `/api/v1/messages/count_tokens`, Anthropic Messages
* `POST /api/v1/responses`, OpenAI Responses

Otari also serves embeddings, images, audio, files, batches, moderations, rerank, search, and decisions. Provider support differs by endpoint, so use `GET /api/v1/models` and the OpenAPI document for the deployment you are calling.

### Embedding input type

Some embedding models embed a query differently from a document, so `POST /api/v1/embeddings` takes an optional `input_type` beside the OpenAI fields and passes it to the provider: Cohere (`search_document`, `search_query`, `classification`, `clustering`, `image`) and Voyage (`query`, `document`). The provider checks the value, and its own default applies when the field is omitted. A request that sets `input_type` for any other provider is refused with a `400` rather than embedded as if the field were absent. With the OpenAI SDK, send it through `extra_body={"input_type": "search_query"}`.

### Responses on providers without a Responses API

`/api/v1/responses` also serves providers that only implement Chat Completions (Mistral, Anthropic, Bedrock, and most others). Otari translates the request into a chat completion and translates the answer back, streamed or not, so the client sees an ordinary Responses object or event stream. A provider with its own Responses API (OpenAI, Azure OpenAI, Gemini, Groq, and others) is called natively.

The translation covers text, image and file input, `instructions`, function tools and `tool_choice`, `max_output_tokens`, `reasoning.effort`, JSON output through `text.format`, and reasoning text a provider returns. Gateway-run tools (MCP, web search, code execution) work on top of it.

Some requests have no chat equivalent and are refused with a `400` that names the field:

* `previous_response_id`, `conversation`, `background`, `context_management` and `prompt`, which need the provider to keep state. Send the full conversation in `input` instead.
* Hosted tools such as `file_search` or `computer_use`, and any tool type other than `function`.
* Input items other than messages, function calls and their outputs, such as `item_reference`.
* Image or file parts in a `system` or `developer` message or in a function call output, where a chat completion only takes text.

Fields that only control what a provider stores or adds to its answer (`store`, `metadata`, `include`, `truncation` and similar) are ignored, and reasoning items on an inbound `input` are dropped, because Chat Completions has no way to send them back.

### Request ID and inline cost

Chat, Messages, and Responses responses carry an `Otari-Request-ID` header, streaming or not, and so do the responses of the embeddings, rerank, images, audio, moderations, decisions and search endpoints. The exception is a request rejected before the gateway handles it: a failed authentication (`401`) or a body that does not validate (`422`) carries none. The gateway mints it, sends it on its own refusals as well, and records it on the request's usage rows, so [`GET /api/v1/usage/requests/{request_id}`](#cost-of-a-failed-or-interrupted-request) finds the request's cost by it. The gateway also names the provider instance and model that served the request, whether a fallback did, and how long it took, in `Otari-Provider`, `Otari-Model`, `Otari-Attempt-Count`, `Otari-Fallback` and `Otari-Response-Duration-Ms`, and splits that duration into its own time and the provider's in `Server-Timing`; see [What is billed and what the caller sees](/reference/gateway/routing.md#what-is-billed-and-what-the-caller-sees).

A priced response also carries its cost on the usage object it already returns, as `usage.cost_usd` (a six-decimal USD string) and `usage.pricing_source`. The [rerank](#rerank) and embeddings endpoints carry the same two fields on their `usage`. On a stream the fields ride the terminal usage event: for Chat Completions, the last chunk that reports usage and either has no choices (OpenAI's `stream_options.include_usage` chunk) or finishes a choice (providers such as Cohere and Gemini report usage on the chunk carrying `finish_reason`); `message_delta` for Messages; and `response.completed` for Responses. The two fields always appear together, and an unpriced or unreported request carries neither.

The amount is the one the gateway wrote to its own usage record, including any gateway-run tool charges, and `pricing_source` names the rate that priced the model: `organization` (an organization's override), `deployment` (a rate stored on this gateway), or `defaults` (the bundled or accepted models.dev snapshot).

Each request's cost is settled to the micro-dollar, half-up, so a request that costs less than half a micro-dollar, such as a few tokens of a cheap embedding model, is recorded, debited and reported as `"0.000000"`. The usage row and the inline field always agree, and `pricing_source` still says the request was priced.

### Who runs a built-in tool

The `Otari-Code-Execution`, `Otari-Web-Search` and `Otari-Web-Fetch` request headers say who runs a tool declared in a provider's own words (`code_execution_<date>`, `code_interpreter`, `web_search_<date>`, `web_fetch_<date>`). Each takes `auto`, `otari` or `provider`, decided for each attempt of a fallback chain on its own. A workspace may pin the same choice for every request billed to it, and then a header that disagrees is refused with a 403. [Who runs a tool](/reference/gateway/tools.md#who-runs-a-tool) defines the values and their limits.

### Gateway tool calls on Chat Completions

Chat Completions has no shape for a tool the server runs, so a chat completion reports the calls Otari ran itself (web searches and fetches, code execution and MCP tool calls) in `otari_tool_calls`, at the top level of the response, beside `choices`. A caller can read there what a tool really returned rather than rely on the model's account of it. Each entry is one call, and a field that does not apply to its type is left out:

| Field                           | Value                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                            | The call's ID: `otari_ws_…` for a search, `otari_wf_…` for a fetch, `otari_ci_…` for code execution, `otari_mcp_…` for an MCP call.                                                                                                                                                                                                                                                           |
| `type`                          | `web_search`, `web_fetch`, `code_execution` or `mcp`.                                                                                                                                                                                                                                                                                                                                         |
| `status`                        | `in_progress`, `completed` or `failed`. A run that exits non-zero, and an MCP call that returned a tool error, are `failed`.                                                                                                                                                                                                                                                                  |
| `query`                         | A search's query.                                                                                                                                                                                                                                                                                                                                                                             |
| `url`                           | The URL the model asked a fetch for, without its query string and fragment. A redirect does not change it.                                                                                                                                                                                                                                                                                    |
| `sources`                       | A completed search's results, each with its `url`, `title` and `published_date` (an ISO date, or date and time, for a search on a search instance; the provider's own date string on the legacy search settings; or `null`); empty when it found none. Never page text.                                                                                                                       |
| `code`                          | The code a code execution ran.                                                                                                                                                                                                                                                                                                                                                                |
| `stdout`, `stderr`, `exit_code` | What an ended code execution printed and its exit status, absent when the run never reached the sandbox.                                                                                                                                                                                                                                                                                      |
| `file_ids`                      | The [Files API](/reference/gateway/files.md) IDs of the files a code execution produced.                                                                                                                                                                                                                                                                                                      |
| `server`, `name`                | The MCP server and the tool the model called on it.                                                                                                                                                                                                                                                                                                                                           |
| `output`                        | The text an ended MCP call gave the model. A failed call's is Otari's own `[tool error] …` text, never the server's exception.                                                                                                                                                                                                                                                                |
| `truncated`                     | `true` when `stdout`, `stderr` or `output` was cut to 64 KiB. The model read it whole.                                                                                                                                                                                                                                                                                                        |
| `error`                         | Why a failed call failed, when Otari can say: `provider`, `status`, `tag` and `retryable`, the same four fields the [Search](#search) endpoint's `Otari-Error-*` headers carry. A call refused past its declaration's `max_uses`, or past the request's `web_search_max_calls`, has `"tag": "call_limit"` and a `null` provider. Absent when Otari can say no more than that the call failed. |

A response that is not streamed carries the ended entry of every call, across all of the tool loop's rounds:

```json
{
  "id": "chatcmpl-…",
  "object": "chat.completion",
  "choices": [{"index": 0, "message": {"role": "assistant", "content": "Python 3.14 is…"}, "finish_reason": "stop"}],
  "otari_tool_calls": [
    {
      "id": "otari_ws_5f0c…",
      "type": "web_search",
      "status": "completed",
      "query": "latest python release",
      "sources": [{"url": "https://www.python.org/downloads/", "title": "Download Python", "published_date": null}]
    }
  ]
}
```

A stream sends a chunk when each call starts and another when it ends, before the answer's text; a call refused past `max_uses` never started, so it is sent once, ended. Each chunk carries whole entries at its top level and one choice whose `delta` has no content or tool calls, so a client that reads only `choices` passes over it. Match entries by `id`: the ended one replaces the started one.

```
data: {"id":"chatcmpl-…","choices":[{"delta":{},"index":0,"finish_reason":null}],…,"otari_tool_calls":[{"id":"otari_ws_5f0c…","type":"web_search","status":"in_progress","query":"latest python release"}]}

data: {"id":"chatcmpl-…","choices":[{"delta":{},"index":0,"finish_reason":null}],…,"otari_tool_calls":[{"id":"otari_ws_5f0c…","type":"web_search","status":"completed","query":"latest python release","sources":[…]}]}
```

The entries are not in the `delta`, where the OpenAI Python SDK's stream helper (`client.chat.completions.stream()`) would fail on them. That helper ignores them, so read the raw chunks to see them. A stream that has sent an entry has sent response bytes, so a provider failure after it ends the stream with an error event rather than falling over to another candidate (see [Failure behavior](/reference/gateway/routing.md#failure-behavior)).

A client may keep the entries on the assistant message it sends back: Otari removes `otari_tool_calls` from every assistant message before calling the provider.

### Provider-specific fields

A field a provider adds to a chat completion's message beyond the OpenAI schema (Exa's `citations`, for instance) is kept where the provider put it and copied under `message.provider_specific_fields` (`delta.provider_specific_fields` on a stream), where clients written against LiteLLM look for it.

### Request tags

A request can tag its spend through `metadata`, OpenAI's field for this, on all three surfaces:

```json
{"model": "openai:gpt-4o", "messages": [...], "metadata": {"purpose": "chat", "country": "DE"}}
```

The gateway records the tags on every usage row the request writes: served, failed, streamed or not, refused by the gateway (a disallowed model, missing pricing, an exhausted budget), and the separate row billing an image description made for a text-only model. LiteLLM's nested form, `"metadata": {"spend_logs_metadata": {...}}`, is read as well, so a client moving off a LiteLLM proxy keeps its attribution unchanged; a nested key wins over a flat one of the same name. The limits are OpenAI's: up to 16 string pairs, keys up to 64 characters and values up to 512. A null value is ignored, and anything else is refused with a 422. Tags are stored without redaction, so never put a secret in one.

A client that can add a header but not a body field, such as Claude Code (see [Use with Claude Code](https://docs.otari.ai/guides/agents-and-frameworks/use-with-claude-code#tag-claude-code-traffic)), sends the same tags in an `Otari-Tags` header, on the same three surfaces:

```
Otari-Tags: agent=bug-fix,run=1234
```

The value is comma-separated `key=value` pairs. Whitespace around a pair is ignored, everything after a pair's first `=` is its value, and keys and values are percent-decoded, so `note=a%2Cb` records `a,b`. A non-ASCII character must be percent-encoded as UTF-8 (`caf%C3%A9`), and a header sent more than once is read as one comma-joined list. Header tags are the base and the body's tags override them on the same key; the bounds apply to the merged set. A raw non-ASCII character, a pair without `=`, a `%` that does not start a two-digit hex escape, a key given twice, a value that does not decode to UTF-8, or tags past the bounds are refused with a 422 whose `detail` locates the error at `["header", "Otari-Tags"]`, as a malformed `metadata` is refused. The header is never forwarded to the provider.

What reaches the provider depends on the surface. Chat Completions and Messages never forward the caller's `metadata`. On Messages, `metadata.user_id` names the billed user and is not a tag; the provider gets an [end-user identifier](https://docs.otari.ai/self-hosting/configuration#end-user-identifiers) derived from it in its place. Responses forwards `metadata`, minus `spend_logs_metadata`, because the Responses API stores it.

The usage endpoints (`GET /api/v1/usage`, `/count`, `/summary`, `/series`, and their `/api/v1/organizations/me/usage` counterparts) filter by tag with a repeatable `tag=key:value`. Values for the same key match any of them, and different keys must all match. `/summary` also takes `group_by_tag=<key>` and returns spend by that tag's values as `by_tag`, with untagged rows under a null key:

```
GET /api/v1/usage/summary?group_by_tag=purpose&dimensions=none
```

### The `user` field

OpenAI's `user` field is a free-form end-user identifier, and OpenAI examples often fill it with one. In Otari it names the user a request is billed to, so it is not free-form. A request made with an API key may name only that key's own user: any other value is refused with `403 'user' field does not match the authenticated API key's user`. A key's `reject_user_mismatch` (see [Mismatched `user` fields](https://docs.otari.ai/guides/organizations-and-access/api-keys#mismatched-user-fields)), or the deployment-wide setting of the same name, can accept other values instead, in which case the value is ignored and spend stays with the key's user. A [service key](https://docs.otari.ai/guides/organizations-and-access/end-users) may name its own end users, and a master-key request must name the user to bill. Chat Completions and Responses never send `user` to the provider.

To tag a request with a free-form end-user ID, put it in `metadata` (see [Request tags](#request-tags)) rather than in `user`:

```json
{"model": "openai:gpt-4o", "messages": [...], "metadata": {"end_user": "customer-42"}}
```

### Cost of a failed or interrupted request

* A stream that fails mid-response ends in an error event, and one the client disconnects from ends with nothing, so neither delivers `usage.cost_usd`.
* The gateway still records and bills the tokens the provider reported before the stream ended (an Anthropic stream reports its input tokens in `message_start`). The request is not treated as free.
* A stream the client abandoned before the provider reported any usage, and that ran no gateway tools, has nothing to bill and writes no usage row.

`GET /api/v1/usage/requests/{request_id}` returns what a request was billed, looked up by its `Otari-Request-ID`:

```json
{
  "request_id": "5f0c…",
  "status": "error",
  "cost_usd": "0.012400",
  "prompt_tokens": 4100,
  "completion_tokens": 0,
  "total_tokens": 4100,
  "row_count": 1
}
```

| Aspect     | Behavior                                                                                                                                                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Totals     | The sum of every usage row the request wrote. A request routed through a policy writes one per attempt, all sharing the id as their `request_group_id`, so the total covers the attempts it fell over from as well as the one that served. |
| `cost_usd` | The [inline cost](#request-id-and-inline-cost) format; `null` when nothing was priced.                                                                                                                                                     |
| Access     | An API key sees only its own requests; the master key sees any.                                                                                                                                                                            |
| `404`      | Until the request has settled (usage rows are written in the background), for an unknown id, for another key's id, and for a request that wrote no row.                                                                                    |

### Retrying safely

What a retry costs depends on how the first attempt ended:

| First attempt                                                                                               | Billed                                                                                                                                                       | A retry                                                                                     |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| Refused by the provider or the gateway (a 429, a 529, any other error)                                      | No: its budget hold is refunded                                                                                                                              | Can be sent as it is.                                                                       |
| A stream that failed or that the client disconnected from                                                   | The tokens the provider reported before it ended, and nothing more (see [Cost of a failed or interrupted request](#cost-of-a-failed-or-interrupted-request)) | Is a new request, billed on its own.                                                        |
| A non-streaming request that succeeded while its response was lost (a dropped connection, a client timeout) | Yes                                                                                                                                                          | Calls the provider again and is billed again, unless both carry the same `Idempotency-Key`. |

#### `Idempotency-Key`

A non-streaming Chat, Messages, or Responses request may carry an `Idempotency-Key` header: any unique string of 1 to 255 printable ASCII characters, usually a UUID. A retry with the same key and the same request gets the original response, with its original `Otari-Request-ID`, serving headers and `usage.cost_usd`, plus `Otari-Idempotent-Replayed: true` (and no `Otari-Response-Duration-Ms` or `Server-Timing`, which described the original), without calling the provider or billing again.

| Aspect                      | Behavior                                                                                                                                                                                                                                                                                         |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Scope                       | A key belongs to the API key that sent it (or, for the master key, to the billed user), so two callers never see each other's responses.                                                                                                                                                         |
| Same request                | The same body, and the same `Otari-Code-Execution`, `Otari-Web-Search`, `Otari-Web-Fetch`, `Otari-Router`, `Otari-Conversation-Id`, `Otari-Tags` and `anthropic-beta` headers, since those change what the request does. Key order and whitespace in the JSON body do not count as a difference. |
| Different request, same key | `422`.                                                                                                                                                                                                                                                                                           |
| Original still running      | `409` with `Retry-After`, at once, as the IETF `Idempotency-Key` draft and Stripe's API answer.                                                                                                                                                                                                  |
| Original failed             | The next retry runs in its place. Only a successful response is kept.                                                                                                                                                                                                                            |
| Long-running original       | The request holding a key renews its claim while it runs, so a retry never runs it a second time. If the worker running it dies, its key frees up within `idempotency_lease_sec` (a minute by default).                                                                                          |
| Retention                   | A response, generated content included, is kept for `idempotency_retention_sec` (a day by default; `0` ignores the header) and then deleted. Expired responses are still deleted after the header is turned off.                                                                                 |
| Storage                     | Encrypted with `OTARI_SECRET_KEY`; a deployment without that key ignores the header. Responses larger than 8 MiB are not kept.                                                                                                                                                                   |
| Checks on a replay          | The request is still authenticated and checked against the key's model access, and a user who has since been blocked is refused rather than given the stored response.                                                                                                                           |
| Not applied                 | Streaming requests ignore the header.                                                                                                                                                                                                                                                            |

A keyed retry runs again, and is billed again, whenever the original's response was not stored or can no longer be read:

* the response was larger than 8 MiB;
* its retention passed, or the header was turned off;
* `OTARI_SECRET_KEY` was rotated away, so no configured key can decrypt it;
* the gateway stopped after the provider answered and before the response was stored;
* the database stayed unreachable for about `idempotency_lease_sec` while the original ran, so its claim lapsed and a retry took it over.

#### Procedure: retry a non-streaming request

1. Generate a UUID for the request and send it as `Idempotency-Key`.
2. On a lost response, resend the identical request with the same key.
3. On a `409`, wait for `Retry-After` and retry with the same key, backing off exponentially.
4. For a new or changed request, generate a new key; reusing one is a `422`.

## Rerank

`POST /api/v1/rerank` orders a list of documents by relevance to a query. The response keeps the provider's own billing report in `meta`, unchanged, and says in `usage` what the gateway metered and what it charged:

```json
{
  "id": "…",
  "model": "rerank-v3.5",
  "results": [{"index": 0, "relevance_score": 0.86}],
  "meta": {"billed_units": {"search_units": 1.0}, "tokens": null},
  "usage": {"total_tokens": null, "search_units": 1, "cost_usd": "0.002000", "pricing_source": "deployment"}
}
```

* `total_tokens` is the token count the provider reported, or null when it reported none.
* `search_units` is the number of search units the request was billed for, and appears only when the model is priced per request: the provider's count rounded up, or one when it reported none. A model priced per token is billed on `total_tokens` instead, and its usage has no `search_units`.
* `cost_usd` and `pricing_source` are the inline cost described under [Request ID and inline cost](#request-id-and-inline-cost), and are absent when the request was not priced.

How a rerank model is priced, per search unit or per input token, is described in [Per-request pricing](https://docs.otari.ai/self-hosting/configuration#per-request-pricing-audio-moderations-rerank-and-completion-models).

## Search

`POST /api/v1/search` and `POST /api/v1/search/{search_tool_name}` run a configured search tool directly. This is separate from `otari_web_search`, which lets a model request searches during a completion. Both are described in [Built-in tools](/reference/gateway/tools.md).

The request and the response follow LiteLLM's `/v1/search`, so a client written for it works against Otari with its base URL changed. `include_raw` and `provider_options` are Otari's own.

**Which tool.** The path names the tool. On `POST /api/v1/search`, the body's `search_tool_name` does, else the deployment's `web_search_default_tool`, else the only configured tool. A call that names no tool runs on the workspace's [organization search key](/reference/gateway/tools.md#organization-search-keys) when it has a usable one; a named call runs on that tool with its own key. Either way the tool decides the access check, the `rate_limits` rules, the timeout and the usage row's model and provider. The access check reads the key's `allowed_tools`, else its user's, for the tool's name or `*`. With neither set, it follows the key's model access, or its user's when the key has none: unrestricted model access runs every search tool, and restricted model access runs only a tool named by its deprecated `<provider>:<tool>` entry in `allowed_models` ([Model and search tool access](https://docs.otari.ai/guides/organizations-and-access/api-keys#model-and-search-tool-access)).

**Request.**

| Field                  | Type                | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`                | string, required    | What to search for.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `search_tool_name`     | string              | The tool, when the path does not name one.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `max_results`          | integer, at least 1 | How many results. 10 when unset. No upper bound of Otari's own: lowered to the workspace's ceiling when its web-access policy sets one, and to the provider's own maximum (100 for Exa).                                                                                                                                                                                                                                                       |
| `search_domain_filter` | up to 20 domains    | Only results from these domains and their subdomains; a leading `-` excludes a domain instead. Exa filters natively; for other providers the results are filtered after the search.                                                                                                                                                                                                                                                            |
| `country`              | two-letter ISO code | Localizes results where the provider can (Exa's `userLocation`); ignored where it cannot.                                                                                                                                                                                                                                                                                                                                                      |
| `max_tokens_per_page`  | integer, at least 1 | The page budget, at four characters a token: it cuts every result's `snippet`, and bounds the page text Exa returns, at most 10,000 characters. Unset, a budget of 1,024 tokens cuts every provider's snippet but Exa's, whose page text that budget already bounds.                                                                                                                                                                           |
| `include_raw`          | boolean             | Also return `raw`, the provider's whole response. Default `false`.                                                                                                                                                                                                                                                                                                                                                                             |
| `provider_options`     | object              | Native options of the provider serving the call, under its own names, as `GET /api/v1/search-tools/providers` lists them. They win over the tool's own options; the fields above win over them. An option the provider does not take, or takes only from the deployment, is a 400 naming it. A call that names no tool may be served by an organization key on another provider, so name the tool when the options are meant for one provider. |
| `user`                 | string              | Who the search is billed to; see below.                                                                                                                                                                                                                                                                                                                                                                                                        |

Until any-search has their adapters, `include_raw` and `provider_options` are a 400 on a `searxng` tool and on a call a Tavily or Brave organization key serves; `false` and `{}` pass.

**Response.**

```json
{
  "object": "search",
  "search_tool": "exa-search",
  "results": [
    {
      "title": "Python 3.14.0 (final) is here!",
      "url": "https://blog.python.org/2025/10/python-3140-final-is-here.html",
      "snippet": "Python 3.14.0 is the newest major release ...",
      "date": "2025-10-01T00:00:00.000Z",
      "last_updated": null
    }
  ]
}
```

`title` and `snippet` are always strings, empty when the provider gave none. `snippet` is the page text when the provider returned it, else its excerpt. `date` is the page's date as the provider gave it, else `null`, and `last_updated` is `null`. `raw` appears only when the request set `include_raw`.

A service key's `user` field names one of its end users here as it does on chat completions: the search is billed to that end user, under the key's end-user budget, and counted by `rate_limits`.

**Price.** A search is billed at the provider's reported cost when it reports one, else at the tool's own per-call rate: its `price_per_call`, or a price row of kind `search` keyed by the tool's name (`POST /api/v1/pricing` takes a `kind`, as do the pricing reads and deletes, as a `?kind=` query, and `/api/v1/organizations/me/pricing`, where only a deployment operator sets a `search` or `fetch` override). A row written under the old `<provider>:<tool>` key is read after it, until a later release. See [Pricing a gateway-run tool](/reference/gateway/tools.md#pricing-a-gateway-run-tool) for the order and the organization key's rate.

**Errors.** Where a refusal carries a stable code, it is in the `Otari-Error-Code` header and the body's `code`; see [Error codes](#error-codes).

| Situation                                                                                                                                                                                                                    | Status | `Otari-Error-Code`            |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------- |
| No credentials, or a key that is not valid                                                                                                                                                                                   | 401    |                               |
| No tool is configured, the tool named is not, or several are and the call names none with no default                                                                                                                         | 400    |                               |
| The tool cannot run as configured, such as a `searxng` tool with no address                                                                                                                                                  | 400    |                               |
| A `provider_options` key the provider does not take from a request                                                                                                                                                           | 400    |                               |
| `include_raw` or `provider_options` on a call that does not run on any-search yet                                                                                                                                            | 400    |                               |
| The master key without a `user`                                                                                                                                                                                              | 400    |                               |
| The body fails schema validation, such as an `include_raw` that is not a boolean                                                                                                                                             | 422    | `invalid_request`             |
| The workspace turned web search off                                                                                                                                                                                          | 403    |                               |
| The key's `allowed_tools`, or the deprecated `<provider>:<tool>` entry in `allowed_models`, does not grant the tool                                                                                                          | 403    |                               |
| A `user` naming another user, where the key rejects a mismatch                                                                                                                                                               | 403    |                               |
| A service key's `Otari-End-User-Budget` is not one it may use                                                                                                                                                                | 403    | `end_user_budget_not_allowed` |
| A budget refuses the search                                                                                                                                                                                                  | 403    | `budget_exceeded`             |
| `require_pricing` is on and the search has no price (a row of kind `search` keyed by the tool's name, or its `price_per_call`; `otari:web_search` for a search the workspace's organization key serves); the detail names it | 402    | `pricing_required`            |
| A rate limit is full                                                                                                                                                                                                         | 429    | `rate_limited`                |
| The workspace's organization search key cannot be used as it is                                                                                                                                                              | 502    |                               |
| The provider failed the search, or could not be reached                                                                                                                                                                      | 502    |                               |
| The workspace's stored web-access policy cannot be read                                                                                                                                                                      | 503    |                               |

Every provider failure is a 502 whose `detail` names no upstream detail, whatever the provider answered, unlike the completion routes, which classify a provider's failure. The 502 carries what is safe to show of the failure as headers instead, never a body, a URL or the query:

| Header                  | Value                                                                                                                                                                                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Otari-Error-Provider`  | The provider that failed, such as `exa`.                                                                                                                                                                                                                           |
| `Otari-Error-Status`    | The provider's own HTTP error status; absent when it gave none, as for a timeout, an error inside a successful answer, or an answer that could not be read.                                                                                                        |
| `Otari-Error-Tag`       | What failed, in the provider's terms or the library's: `timeout`, `network`, `invalid_header` (a key holding a character that is not ASCII), `http_error`, `invalid_response`, `unexpected_response`, or the provider's own code, such as Exa's `INVALID_API_KEY`. |
| `Otari-Error-Retryable` | `true` for a timeout, a 429 or a 5xx from the provider, when a retry may help; otherwise `false`.                                                                                                                                                                  |

On a search the workspace's organization key serves, they name the key's provider. A 502 for a failure that is not the provider's, such as an organization key this gateway cannot call, carries none of them. The completion routes send the same headers when a provider fails a completion; see [Provider failures](#provider-failures).

Search-tool management lives under `/api/v1/search-tools`. The generated OpenAPI document describes the supported providers, filters, and management schemas.

## Code execution

`POST /api/v1/code/execute` runs code in the gateway's sandbox and returns its output, with no model involved. It is admitted, priced and budgeted as an `otari_code_execution` call; see [Running code without a model](/reference/gateway/tools.md#running-code-without-a-model).

**Request.**

| Field             | Type             | Description                                                                                                                                                                                                                    |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `code`            | string           | Python source to run. Required, and sent to the sandbox unchanged.                                                                                                                                                             |
| `container`       | string           | `auto` to hold the sandbox after the run, or an `otari_cntr_…` id from an earlier `Otari-Container-Id` to run in that sandbox again, with its files and interpreter state. Omitted, the sandbox is released when the run ends. |
| `timeout_seconds` | integer          | How long the code may run. Capped at the workspace's `exec_timeout_s`, or 60 seconds when it sets none.                                                                                                                        |
| `file_ids`        | array of strings | Uploads from `POST /api/v1/files` to put in the working directory before the code runs, each under its own filename. At most 32.                                                                                               |
| `user`            | string           | The billed user, as on the completion routes ([the `user` field](#the-user-field)). Required with the master key.                                                                                                              |

**Response.**

```json
{
  "object": "code_execution.result",
  "stdout": "2\n",
  "stderr": "",
  "return_code": 0,
  "files": [{"file_id": "file-…", "filename": "chart.png"}]
}
```

Code that fails is still a `200`, and is billed: its `return_code` is nonzero, or its `stderr` carries the error. `files` lists each file the code wrote, stored in the Files API, so `GET /api/v1/files/{file_id}/content` downloads it; it is empty where the Files API is off. A held sandbox is named in `Otari-Container-Id` and `Otari-Container-Expires-At`.

**Errors.**

| Status | When                                                                                                                                                                            |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | No sandbox is configured, `container` names a sandbox that has expired or does not exist, or a `file_ids` entry is not an upload the caller can read (or the Files API is off). |
| `402`  | `require_pricing` is on and `otari:code_execution` has no price (`pricing_required`), or the organization's funds are exhausted.                                                |
| `403`  | The workspace's code execution policy is off, or the billed user's budget is spent.                                                                                             |
| `409`  | The named container is running another request.                                                                                                                                 |
| `502`  | The sandbox could not be reached.                                                                                                                                               |
| `503`  | The sandbox is temporarily out of capacity, with `Retry-After` when it gave one.                                                                                                |

## Decisions

`POST /api/v1/decisions` answers typed questions about a piece of content with probabilities rather than generated text: `noul` (the probability of yes), `choice` (one of the named options) and `score` (a level on an ordered scale). The body is TypeSafe's System One shape, which OpenRouter's alpha Decisions API and llama-server also take:

```json
{
  "model": "typesafe:jev-latest",
  "state": "I've been trying to connect Stripe for 3 days and I'm losing sales.",
  "questions": {
    "urgency": {"type": "noul", "instructions": "Does this message express urgency?"},
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {"billing": null, "technical": "Integrations and outages"}
    }
  }
}
```

`model` is `<provider>:<model>`, where the provider is an entry under [`decision_providers`](https://docs.otari.ai/self-hosting/configuration#decision-providers). The answer comes back as the provider returned it, keyed like the questions, with `probabilities`, `confidence` and `usage`. `images` takes data URLs for a vision decision model served by llama-server; other providers refuse it.

`POST /api/v1/systemone` serves the same request at the path TypeSafe's SDK and llama-server use, so a client written for either works against Otari with its base URL set to `https://<otari>/api`.

A decision is billed like a completion: the `<provider>:<model>` rate prices the reported tokens, the provider's own reported cost is used when no rate is set (only OpenRouter reports one), and it is subject to budgets, key allow-lists and `require_pricing`. A provider's refusal of the request (400, 422, or llama-server's 501 for a model that cannot answer it) returns 400, its rate limit returns 429, its refusal of the gateway's credential (401 or 403) returns 424 `provider_credential_rejected`, and any other failure returns 502.

## Routing policies

Routing-policy management lives under `/api/v1/routing/policies`. A subagent model recommendation is asked at `POST /api/v1/routing/recommend`, and the operator manages the subagent routers that answer it under `/api/v1/routing/recommenders` (see [Routing recommendations](#routing-recommendations)). See [Routing policies](/reference/gateway/routing.md) for configuration and behavior, and OpenAPI for the request schemas.

## Routing recommendations

`POST /api/v1/routing/recommend` recommends the model a coding agent's subagent should start on, asked by the harness at spawn time. One decision call is made and nothing else is dispatched: the harness runs the subagent on the answer with its own provider credentials. How the recommendation is reached is in [Agent model recommendations](/reference/gateway/routing.md#agent-model-recommendations).

Any active API key may ask; the decision is billed to the key's own user. A master-key request names the billed user in `user`. The call is metered, budgeted and recorded in usage like a [decision](#decisions).

```bash
curl "$OTARI_URL/api/v1/routing/recommend" \
  -H "Authorization: Bearer $OTARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "harness": "claude-code",
    "session_id": "bf05abe2-5ff2-4eb0-8459-ab578d5c9468",
    "tool_use_id": "toolu_01Ab3dEfGh",
    "agent_type": "Explore",
    "description": "Find the budget reservation code",
    "prompt": "Find where budgets are reserved before dispatch.",
    "parent_model": "claude-opus-5",
    "requested_model": null
  }'
```

**Request.** Unknown fields are refused with a `422`.

| Field             | Type                            | Meaning                                                                                                                                                                                                                                                             |
| ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `harness`         | `claude-code` or `pi`, required | The harness asking. It decides the candidates (below).                                                                                                                                                                                                              |
| `candidates`      | object                          | The models the subagent may be sent to, as Otari model ids (`<provider>:<model>`, up to 256 characters) mapped to what each is for (up to 1,024 characters, or `null` when the name says enough). Two to 255 entries. Required for `pi`, refused for `claude-code`. |
| `session_id`      | string, 1 to 256, required      | The harness's own id for the session the spawn happens in.                                                                                                                                                                                                          |
| `tool_use_id`     | string, 1 to 256, required      | The harness's own id for the tool call that spawns the subagent.                                                                                                                                                                                                    |
| `agent_type`      | string, 1 to 128, required      | The subagent type: a built-in such as `Explore` or `Plan`, or a custom agent's name.                                                                                                                                                                                |
| `description`     | string, up to 1,024             | The caller's short description of the task. Default empty.                                                                                                                                                                                                          |
| `prompt`          | string, 1 to 65,536, required   | The task the subagent is given. Its first 32,000 characters reach the decision model. Never stored or logged.                                                                                                                                                       |
| `parent_model`    | string, 1 to 256, required      | The model the parent conversation runs on, as the harness names it.                                                                                                                                                                                                 |
| `requested_model` | string, up to 256               | The model the caller asked for, if any. Received as a fact and kept out of the question, so the orchestrator's guess cannot pull the answer toward itself.                                                                                                          |
| `user`            | string                          | The user billed for a master-key request; not sent upstream.                                                                                                                                                                                                        |

**Candidates.** A `claude-code` request chooses among the deployment's `agent_recommender_candidates`, Claude Code's `haiku`, `sonnet` and `opus` aliases by default, because Claude Code sends the subagent's requests to Anthropic itself and resolves the alias on its side; it may not send `candidates`. A `pi` request must send its own. Either mismatch is a `422`.

**Response.**

```json
{
  "model": "haiku",
  "reason": "haiku with 81%",
  "probabilities": { "haiku": 0.81, "sonnet": 0.16, "opus": 0.03 }
}
```

| Field           | Meaning                                                                              |
| --------------- | ------------------------------------------------------------------------------------ |
| `model`         | One of the candidates: a model alias or id the harness can start the subagent on.    |
| `reason`        | One short sentence the harness may show, or `null`.                                  |
| `probabilities` | The decision model's probability for each candidate, or `null` when it reports none. |

**Errors.** No [subagent router](/reference/gateway/routing.md#subagent-routers) stored, or every stored one switched off, is a `400`. The decision model's refusal of the request (400, 422, or 501) is a `400`, its rate limit a `429`, its refusal of the gateway's credential (401 or 403) a `424` with `provider_credential_rejected`, and any other failure, or an answer that names no candidate, a `502`.

## Provider error details

Otari may return a short, sanitized provider diagnostic when the upstream provider rejects something the caller can fix, such as a model name or request parameter. Credentials, URLs, account identifiers, and reflected payloads are removed.

The diagnostic is the provider's own sentence, taken from the error body it sent, and not the provider SDK's rendering of that error. An OpenAI 400 for a model that only accepts its default temperature answers:

```json
{"detail": "Unsupported value: 'temperature' does not support 0.5 with this model. Only the default (1) value is supported."}
```

rather than a detail that begins `Error code: 400 - {'error': {...}}`. The same holds for Anthropic, Cohere and the other providers whose SDKs keep the parsed body: only the body's message can reach the caller, never a status prefix, the body's other fields, or the response headers. When that body has no message, or its message is redacted away, the caller gets the fixed detail for the status instead (for a 400, `The provider rejected the request as invalid (check the model name and parameters)`). The provider's `param` and `code` fields are not copied into the response. When the request named a model alias, a provider message that names the alias's target names the alias instead.

The completion routes and the pass-through routes (embeddings, images, audio, moderations, rerank) classify a provider failure the same way:

| Provider outcome                                                                                                                                  | Otari answers         | `Otari-Error-Code` and headers                                         | `detail`                                                                                                                                                                                                                                                                                           | Client retry                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| Rejects the request (its own 400 or 422)                                                                                                          | 400                   |                                                                        | The provider's sentence                                                                                                                                                                                                                                                                            | No                                          |
| OpenAI's 403 with error type `invalid_request_error` (logprobs requested from a model that does not return them, an embedding model sent to chat) | 400                   |                                                                        | OpenAI's sentence                                                                                                                                                                                                                                                                                  | No                                          |
| Unknown model                                                                                                                                     | 404                   |                                                                        |                                                                                                                                                                                                                                                                                                    | No                                          |
| The provider has no such operation for the model (rerank on a chat model), or its SDK has no parameter for a field the caller sent                | 400                   |                                                                        | Names the operation or the field                                                                                                                                                                                                                                                                   | No                                          |
| Rate limit                                                                                                                                        | 429                   | `upstream_rate_limited`; the provider's `Retry-After` when it sent one |                                                                                                                                                                                                                                                                                                    | After waiting                               |
| Refuses the gateway's own credential (its 401, or a 403 that is not a rejection of the request)                                                   | 424                   | `provider_credential_rejected`                                         |                                                                                                                                                                                                                                                                                                    | No                                          |
| Timeout                                                                                                                                           | 504                   |                                                                        |                                                                                                                                                                                                                                                                                                    | Yes                                         |
| A non-streamed Chat Completions response with no choices                                                                                          | 502                   | `provider_error`                                                       | `The provider returned a completion with no choices`                                                                                                                                                                                                                                               |                                             |
| Any other failure                                                                                                                                 | 502 (500 on Messages) |                                                                        | What happened upstream and nothing more: `The provider failed the request with HTTP 500` (the provider's own status), `The provider closed the connection before answering`, or `The gateway could not connect to the provider`. A failure Otari cannot place keeps the fixed `LLM provider error` | Yes, unless `x-should-retry: false` (below) |

An OpenAI-compatible client does not retry a 400 or a 404 on its own. On Chat Completions, Messages and Responses, a timeout, a dropped connection or a provider 5xx also carries the [provider failure headers](#provider-failures).

A chat completion with no choices is a provider failure, not an answer: it is recorded on the usage row as an error, and a routing policy moves on to its next candidate. Inside a gateway tool loop this applies to the model's first response; a later one is returned as the answer, since the rounds before it were already billed. Messages and Responses are not checked, since an empty `content` or `output` can be a legitimate answer there.

A 5xx that resending the same request cannot fix also carries `x-should-retry: false`, the header the OpenAI and Anthropic SDKs read before their own retry rule, so such a client stops after one attempt instead of retrying with backoff. Two failures qualify: a provider account out of credit or over its billing limit, and a fault in the code making the provider call, such as a provider adapter that cannot convert the request, which raises before anything is sent and has no upstream status. A timeout, a dropped connection, a provider's own 5xx or 429, and any failure Otari cannot place carry no such header, so a client retries those. A streamed request that fails before its first chunk carries it too, since that is answered with an ordinary HTTP error (see [Error codes](#error-codes)). A routing policy still moves on to its next candidate on either failure: the header describes only the answer the caller receives, and a policy that tried several candidates and saw every one fail answers without it.

Gateway-side failures use fixed public messages. Diagnose them with protected logs and safe metadata such as request ID, provider, model, and status. Do not log provider keys, prompts, responses, or raw upstream bodies.

## Error codes

A refusal a caller is expected to act on carries a stable code, both as an `Otari-Error-Code` header and as `code` in the body beside the human-readable `detail`: `{"detail": "...", "code": "budget_exceeded"}`. Map refusals by the code: it keeps its meaning across releases, while the `detail` text may be reworded.

The Messages routes (`/api/v1/messages` and `/api/v1/messages/count_tokens`) answer every error in Anthropic's envelope instead, the body Anthropic's SDKs read: `{"type": "error", "error": {"type": "rate_limit_error", "message": "..."}, "code": "rate_limited"}`. A body that fails validation there is a 400 `invalid_request_error`, as Anthropic answers it, whose message names each failing field.

| `Otari-Error-Code`             | Status                                | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Also sent                                                                                                                                                                                                                                                                                        |
| ------------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `budget_exceeded`              | 403                                   | A budget refused the request                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `Otari-Budget-Scope`: `user` for the billed user's own budget, otherwise the ceiling's scope: `organization`, `workspace`, `workspace_member`, `org_member` or `api_token`; `Retry-After`: seconds until the refusing budget's window ends, when it resets and the request would fit a fresh one |
| `user_blocked`                 | 403                                   | The billed user is blocked                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |                                                                                                                                                                                                                                                                                                  |
| `user_not_found`               | 404                                   | The billed user does not exist                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |                                                                                                                                                                                                                                                                                                  |
| `rate_limited`                 | 429                                   | A gateway rate limit is full                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `Otari-Rate-Limit-Rule` for a `rate_limits` rule; `Retry-After` when waiting helps                                                                                                                                                                                                               |
| `upstream_rate_limited`        | 429                                   | The provider rate limited the gateway                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `Retry-After` when the provider sent one                                                                                                                                                                                                                                                         |
| `invalid_model`                | 400                                   | The model selector names no configured provider, alias, policy or catalog model id; checked before the key's allow-list, so a restricted key gets it too, and before `require_pricing`, so it is never answered as `pricing_required`. A `provider:model` selector whose provider resolves is not judged here, since only the provider knows its models                                                                                                                                                                                                                                                                                                                                                          |                                                                                                                                                                                                                                                                                                  |
| `model_not_allowed`            | 403                                   | The key may not use the model                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |                                                                                                                                                                                                                                                                                                  |
| `model_not_serving`            | 403                                   | The organization offers the model on a provider key, with its serving switch off                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |                                                                                                                                                                                                                                                                                                  |
| `model_not_found`              | 404                                   | A provider key of the organization serves the provider, and none offers the model                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |                                                                                                                                                                                                                                                                                                  |
| `country_not_supported`        | 403                                   | `provider_supported_countries` closes the model's provider to the country the request comes from, or the country is unknown. See [Supported countries](https://docs.otari.ai/self-hosting/configuration#supported-countries)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |                                                                                                                                                                                                                                                                                                  |
| `context_length_exceeded`      | 400                                   | The prompt is too long for the model                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |                                                                                                                                                                                                                                                                                                  |
| `unsupported_content`          | 400                                   | The provider refused content the request carries because the model does not accept it, such as an image sent to a text-only model. Sent only when Otari recognizes the provider's wording for that refusal; a refusal worded otherwise stays a `400` carrying the provider's message and no code. `detail` names the model where it can. A routing policy moves on to its next candidate first, and answers with this code when every candidate gave a refusal Otari recognizes                                                                                                                                                                                                                                  |                                                                                                                                                                                                                                                                                                  |
| `wrong_model_type`             | 400                                   | The model is a kind the endpoint cannot serve. A chat completion, a message or a response is refused for an embedding model, which the provider's model listing or models.dev may name, or for a rerank model, a transcription model or one that writes no text, which only the listing names; for a routing policy or a catalog ID, only when every candidate is such a kind. `/embeddings` is refused only for a model its provider's listing names as another kind; a listed `other` model, and one only models.dev describes, still reach the provider. `detail` names the kind, except for a model the listing says writes no text, which it calls not a chat model. GET /models carries the kind as `type` |                                                                                                                                                                                                                                                                                                  |
| `all_candidates_rejected`      | 400                                   | Every model a routing policy or a catalog ID tried rejected the request as invalid                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |                                                                                                                                                                                                                                                                                                  |
| `insufficient_funds`           | 402                                   | The model is served on a credential the deployment pays for, and the organization has no funds paid in to cover it. Nothing was sent upstream                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |                                                                                                                                                                                                                                                                                                  |
| `pricing_required`             | 402                                   | `require_pricing` is on and the model resolves but has no price, a direct search has no price (see [Search](#search)), or a gateway-run tool the request declares has no row to price its calls ([Tools](/reference/gateway/tools.md#pricing-a-gateway-run-tool)). An unpriced model whose provider has no credential answers `provider_not_configured` instead                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                                                                                                                                                                  |
| `provider_not_configured`      | 424                                   | Nothing declares the model's provider, or its declaration names no key the provider needs, so nothing was sent upstream. Not a 5xx, so a client does not retry it: an operator adds the provider as a [hosted provider](https://docs.otari.ai/self-hosting/providers/hosted-providers) (switching on the model), or the organization adds its own provider key. A provider's environment variable alone serves nothing                                                                                                                                                                                                                                                                                           |                                                                                                                                                                                                                                                                                                  |
| `provider_credential_rejected` | 424                                   | The provider refused the credential the gateway holds for it (an upstream 401, or a 403 other than one rejecting the request, which is a 400). It is the gateway's key, not the caller's, so it is never a 401 or 403; and not a 5xx, because no retry makes the key valid. Replace or fix the provider credential. The usage row keeps the provider's own status. A routing policy moves on to its next candidate first, and answers with this code when every candidate either had no credential or had it rejected, and at least one was rejected                                                                                                                                                             |                                                                                                                                                                                                                                                                                                  |
| `end_user_budget_not_allowed`  | 403                                   | A service key named an end-user budget that is not on its `end_user_budget_ids`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                                                                                                                                                                  |
| `end_user_limit_reached`       | 403                                   | A service key already has as many end users as its `max_end_users` allows, so a request that would create a new end user or revive a deleted one is refused. End users it already has are unaffected                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |                                                                                                                                                                                                                                                                                                  |
| `terms_not_accepted`           | 400                                   | The deployment publishes terms (`terms_url`), and creating or first claiming the account needs them accepted: send `terms_accepted` with the signup, the invitation's first password, the password reset, or the OAuth callback                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                                                                                                                                                                  |
| `invalid_request`              | 422, or 400 on the Messages routes    | The request body or a parameter failed schema validation. `detail` lists the failing fields, each with `loc`, `msg` and `type`; on the Messages routes, `error.message` names them instead                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |                                                                                                                                                                                                                                                                                                  |
| `provider_error`               | 502, or 200 in a stream's error event | As a 502, the provider answered a non-streamed chat completion with no choices. In a stream's error event, the provider failed after the stream started, for a reason no other code names                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                                                                                                                                                                                                                                                                                                  |
| `provider_stream_interrupted`  | 200, in a stream's error event only   | The provider's stream ended before it finished: it closed without reporting a `finish_reason` (Chat Completions) or without the event that ends a response (Responses, from a provider with its own Responses API), or the connection carrying it was cut. The output received so far is truncated                                                                                                                                                                                                                                                                                                                                                                                                               |                                                                                                                                                                                                                                                                                                  |

A `budget_exceeded` refusal's `detail` tells a spent budget (`... has exceeded budget limit`) from a request whose estimated cost is larger than the budget it has left (`... has $X of budget remaining, less than this request's estimated cost of $Y`); see [Budgets](https://github.com/mozilla-ai/otari/tree/gitbook-docs/reference/budgets.md#what-a-budget-limits). Its `Retry-After` is when the budget resets, which may be days away for a monthly budget; see [Refusals and `Retry-After`](https://docs.otari.ai/guides/organizations-and-access/budgets#refusals-and-retry-after).

### Provider failures

When the provider itself fails a completion (Chat Completions, Messages or Responses), by timing out, dropping the connection or answering with a 5xx, the `5xx` Otari answers with carries what is safe to show of it, the same headers a [direct search](#search)'s `502` carries, and never a body or a URL:

| Header                  | Value                                                                                                                             |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Otari-Error-Provider`  | The provider that failed, as config names it, such as `exa`.                                                                      |
| `Otari-Error-Status`    | The provider's own HTTP status; absent for a timeout or a lost connection.                                                        |
| `Otari-Error-Tag`       | `timeout`, `network`, `http_error`, or the provider's own code when its error body names one, such as Exa's `SERVICE_OVERLOADED`. |
| `Otari-Error-Retryable` | `true`: each of these may succeed on a retry.                                                                                     |

When a routing policy tried several candidates, they describe the last one tried. A `4xx` from the provider carries none of them: a billing, quota or credential failure describes the gateway's own provider account, and a provider's `429` already carries `upstream_rate_limited`.

A streamed request whose provider fails before producing any output is answered with an ordinary HTTP error, with the same status, `detail` and code the request would get unstreamed: a `400` for a parameter the provider does not accept, a `502` for a provider outage. Otari holds the response until the provider's first chunk to make that possible. The gateway holds it for at most `streaming_first_chunk_hold_ms` (15 seconds by default; `0` turns the hold off and commits the `200` as soon as the stream opens). For a stream running the gateway's tools, opening their backends (connecting to MCP servers, leasing a sandbox) counts toward that window too. A stream running the gateway's tools (MCP servers, sandbox, web search) locks onto its candidate once the first round of that loop ends instead, which can happen before anything is sent: a failure past that point cannot fall over either, even though no output has reached the client yet. A provider slower than that to start still streams, and a failure after that point is reported as below.

A failure after a stream has started arrives as an error event, which carries the code as `error.code` on Chat Completions and Responses: `{"error": {"message": "...", "type": "server_error", "code": "upstream_rate_limited"}}`. The status line has already gone out as `200` by then, so the event is the only place the failure is named. It carries the code from the table above when one applies (`upstream_rate_limited`, `context_length_exceeded`), `provider_stream_interrupted` when the provider's stream ended before it finished, otherwise `provider_error` when the provider failed the stream. On Chat Completions and Responses, a failure on the provider's own side also carries the [provider failure](#provider-failures)'s fields as `error.provider_failure`: `{"provider": "exa", "status": 503, "tag": "SERVICE_OVERLOADED", "retryable": true}`, with `status` `null` when the provider gave none. On Chat Completions a stream is finished once a choice reports its `finish_reason`; one that closes without any, even after sending content, ends in this error event rather than in a bare `data: [DONE]`. On Responses from a provider with its own Responses API, a stream is finished once the provider sends `response.completed`, `response.incomplete` or `response.failed`, and one that closes before them ends the same way. A Responses stream Otari [translates from a chat completion](#responses-on-providers-without-a-responses-api) always ends in `response.completed`, or in `response.incomplete` when the provider stopped for length or a content filter, so one the provider cut short is not caught and closes as a success. The error event's message is the `detail` the same failure would carry as an HTTP error where that names a cause (a rejected parameter, the provider's own explanation of a rejected request, redacted as described above, or what happened upstream, such as the provider's error status), and a fixed `An error occurred during streaming` otherwise. An event with no `code` is a failure inside the gateway. Either way, the gateway records the request's usage row with status `error`. On Messages, a stream the provider ends without `message_stop`, including one that sent no events at all, also ends in an `api_error` event rather than closing as if it had succeeded, and the request is settled as failed. A Messages error event's `error.type` says whether a retry makes sense: `overloaded_error` and `rate_limit_error` when the provider failed the stream that way, `timeout_error` when it stopped responding (a read timeout, such as Bedrock's, or an upstream `504`), and `api_error` otherwise.

## Caller-orchestrated MCP

`GET /api/v1/mcp/servers/{mcp_server_id}/tools` lists the tools a stored MCP server exposes to the authenticated workspace, and `POST /api/v1/mcp/execute` runs one exact caller-authorized call and returns the server's native MCP result. Never retry `POST /api/v1/mcp/execute` automatically, including from a reverse proxy or service mesh: an `outcome_unknown` response means the tool may already have run. The request shapes, the error and execution-state contract, and the limits are in [Caller-orchestrated MCP](/reference/gateway/mcp.md#caller-orchestrated-mcp).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.otari.ai/reference/api-reference.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
