# Telluvian routing: guide for coding agents

Gallery (`telluvian/gallery-1`) is Telluvian's router: you call it like a model, and Model Select, the engine behind it, picks the cheapest model expected to meet a quality bar, per request. It is an OpenAI-compatible API: use the official OpenAI SDK and change only the base URL, the key and the model.

## Setup

- Base URL: `https://api.telluvian.ai/v1` (the `/v1` is required).
- Auth: `Authorization: Bearer $TELLUVIAN_API_KEY`. Read the key from the environment; never hard-code it.
- Model: `"telluvian/gallery-1"`. The response's `model` field (and every stream chunk's) names the model that actually answered. Log it.
- Endpoints: `POST /v1/chat/completions` and `/v1/responses`. Streaming and multi-turn work as normal.

## Model Select controls

All optional, read only when `model` is `telluvian/gallery-1`, and never forwarded to the model.

| Field | Value | Default |
| --- | --- | --- |
| `xPerf` | Quality bar: a number from `0.1` (unquoted) or a model id meaning "at least as good as this model" | `0.9` |
| `xPerfEffort` | `minimal`, `low`, `medium`, `high`, `xhigh`, `max`; only valid when `xPerf` is a model id | that model's best |
| `modelZoo` | Comma-separated globs of allowed models, e.g. `"anthropic/*,openai/gpt-5*"` | all Model Select candidates |
| `sessionId` | A GUID you generate once per conversation and resend every turn | none |
| `xLatency` | `instant`, `chat` or `batch`; accepted but currently ignored | `chat` |

- Python SDK: pass these through `extra_body={...}`, or the SDK rejects them.
- TypeScript SDK: pass them inline and cast the params type.

## Errors

- `400`: a bad Model Select control. The message names the field (a quoted number in `xPerf`, an unknown model, `xPerfEffort` with a numeric `xPerf`, a `modelZoo` matching no Model Select candidate, a `sessionId` that is not a GUID).
- `401` bad key, `402` out of credit, `429` rate limited (honour `Retry-After`).
- `503`: Model Select unavailable. Retry with backoff, or name a model directly.
- A bar no model can meet is not an error: the best available model answers.

## Cost

The chosen model's catalogue rate plus a Model Select fee of $0.05 per 1M input tokens. Naming a model yourself skips the fee. Read current rates from `GET /v1/models`; do not hard-code prices.

## Limits

No tool calling (tool fields are silently dropped) and text-only input (`content` must be a string). Hallucination scoring is off by default; see the verification guide.

---

# Reference

---

# Quickstart

> Create a key, add credit, and make your first routed request.

Source: https://telluvian.ai/docs/quickstart

## 1. Create an API key

Mint one in [the dashboard](https://telluvian.ai/dashboard/keys). The key is shown **once** and
cannot be recovered afterwards, so store it somewhere safe.

## 2. Add credit

Billing is prepaid and denominated in dollars. A $25 payment adds $25 of API
credit. [Add credit](https://telluvian.ai/dashboard/billing); requests are refused with a `402`
until your balance is positive.

## 3. Send a request

Send `telluvian/gallery-1` and we pick the model. Probing is off by default;
`include_scores: false` makes that choice explicit.

```bash
curl https://api.telluvian.ai/v1/chat/completions \
  -H "Authorization: Bearer $TELLUVIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "telluvian/gallery-1",
    "messages": [{"role": "user", "content": "When was the Eiffel Tower built?"}],
    "include_scores": false
  }'
```

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.telluvian.ai/v1",
    api_key=os.environ["TELLUVIAN_API_KEY"],
)

response = client.chat.completions.create(
    model="telluvian/gallery-1",
    messages=[{"role": "user", "content": "When was the Eiffel Tower built?"}],
    extra_body={"include_scores": False},
)

print(response.choices[0].message.content)
print(response.model)   # the model we picked
```

```typescript
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.telluvian.ai/v1',
  apiKey: process.env.TELLUVIAN_API_KEY,
});

const response = await client.chat.completions.create({
  model: 'telluvian/gallery-1',
  messages: [{ role: 'user', content: 'When was the Eiffel Tower built?' }],
  include_scores: false,
} as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean });

console.log(response.choices[0].message.content);
console.log(response.model);   // the model we picked
```

You can also name a model yourself. Any id from `GET /v1/models` works in place
of `telluvian/gallery-1`, and the rest of the request is identical.

## 4. Add verification

Set `include_scores: true` to request per-token hallucination scores. A token
whose probe reading is unavailable has a `null` score.

```bash
curl https://api.telluvian.ai/v1/chat/completions \
  -H "Authorization: Bearer $TELLUVIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "model": "telluvian/gallery-1",
  "messages": [
    {
      "role": "user",
      "content": "When was the Eiffel Tower built?"
    }
  ],
  "include_scores": true
}'
```

```python
response = client.chat.completions.create(
    model="telluvian/gallery-1",
    extra_body={"include_scores": True},
    messages=[{"role": "user", "content": "When was the Eiffel Tower built?"}],
)

print(response.scores)   # {"hallucination": [0.03, 0.01, 0.44, ...]}
print(response.tokens)   # the tokens those scores line up with
```

Verification adds $1.00 per 1M completion tokens. See [Reading the
scores](https://telluvian.ai/docs/scores.md) for what the numbers mean.

> **Which model?**
>
> `GET /v1/models` lists everything you can call, with its current price. If you
> would rather not choose, [Gallery](https://telluvian.ai/docs/gallery.md) picks for you, and [Model
> Select](https://telluvian.ai/docs/model-select.md#the-controls) has the controls that shape the choice: a quality bar and an allowlist. The latency control is currently ignored.

---

# Gallery

> A router you call like a model. Send one model id and each request goes to the right model.

Source: https://telluvian.ai/docs/gallery

renamed, sed this file plus the cross-links in index, quickstart, models,

Picking a model is a per-request decision that almost nobody makes per request.
It gets made once at integration time and left alone. The result is a frontier
model answering "what's 2+2", and a small model answering the question that
needed the frontier one.

Gallery is our router, and you call it like a model. Set `model` to
`telluvian/gallery-1` and each request goes to the cheapest model that still
clears the quality bar you asked for. The response reports which model actually
ran.

```json
{
  "model": "telluvian/gallery-1",
  "messages": [{"role": "user", "content": "Reformat this address as JSON: ..."}],
  "include_scores": false
}
```

Everything else about the request is unchanged. Streaming, multi-turn messages
and the [Responses API](https://telluvian.ai/docs/responses.md) work exactly as they do when you name a
model yourself.

> **Gallery runs on Model Select**
>
> The choice itself is made by [Model Select](https://telluvian.ai/docs/model-select.md), which reads
> your prompt and predicts how well each candidate model will handle it. Gallery
> asks Model Select for a model, then sends your request there. If you would
> rather make the model call yourself, call Model Select directly.

> **Verification is opt-in**
>
> Gallery does not enable hallucination scoring. Set `include_scores: true` to
> add [verification](https://telluvian.ai/docs/scores.md), billed at $1.00 per 1M completion tokens.

## Sending a request

```bash
curl https://api.telluvian.ai/v1/chat/completions \
  -H "Authorization: Bearer $TELLUVIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "telluvian/gallery-1",
    "messages": [{"role": "user", "content": "Summarise this changelog in one line: ..."}],
    "xPerf": "openai/gpt-5.6-terra",
    "include_scores": false
  }'
```

```python
# The Model Select controls must go through extra_body. The Python SDK
# validates arguments against the OpenAI schema and rejects unknown keyword
# arguments before sending.
response = client.chat.completions.create(
    model="telluvian/gallery-1",
    messages=[{"role": "user", "content": "Summarise this changelog in one line: ..."}],
    extra_body={"xPerf": "openai/gpt-5.6-terra", "include_scores": False},
)

print(response.model)   # the model that actually ran
```

```typescript
// Goes inline. The JS SDK forwards unknown top-level keys as-is.
const response = await client.chat.completions.create({
  model: 'telluvian/gallery-1',
  messages: [{ role: 'user', content: 'Summarise this changelog in one line: ...' }],
  xPerf: 'openai/gpt-5.6-terra',
  include_scores: false,
} as OpenAI.ChatCompletionCreateParamsNonStreaming & {
  xPerf: string;
  include_scores: boolean;
});

console.log(response.model);   // the model that actually ran
```

To add verification, set `include_scores: true` and read `scores` off the response.
See [Reading the scores](https://telluvian.ai/docs/scores.md).

## Reading what ran

`telluvian/gallery-1` is not a model. It is resolved to a real one before your
request is sent anywhere, so the `model` field on the response, and on every
chunk of a stream, names the model that generated the text:

```json
{
  "id": "chatcmpl-...",
  "model": "openai/gpt-5.4-mini",
  "sessionId": "3f2b7c58-9d41-4e0a-9a7c-6f0b1c2d3e4f",
  "choices": [ ... ]
}
```

Log `response.model` to track what generated each answer. Non-streamed
responses also carry the `sessionId` Model Select used. See
[keeping a conversation together](https://telluvian.ai/docs/model-select.md#sessionid-keeping-a-conversation-together).

> **The choice is made before the model is called**
>
> Nothing is decided mid-generation, so `stream: true` behaves normally: the
> same SSE chunks in the same order. The one difference you can measure is time
> to first byte, because choosing costs a round trip before your request leaves
> for the model.

## Steering the choice

Gallery accepts the same five controls as Model Select, on the request body
alongside `model`. They are read only when `model` is `telluvian/gallery-1`,
passed to Model Select unchanged, and never forwarded to the model.

| Control | What it sets | Default |
| --- | --- | --- |
| `xPerf` | The quality bar, as a number or a model id | `0.9` |
| `xPerfEffort` | Which effort of a named `xPerf` model you mean | the named model's best |
| `xLatency` | Reserved: `instant`, `chat` or `batch` | `chat` |
| `modelZoo` | Which models may be chosen | all Model Select candidates |
| `sessionId` | Keeps a conversation on one model | a fresh one per request |

[Model Select](https://telluvian.ai/docs/model-select.md#the-controls) documents each one in full. A
bad value is a `400` naming the field, never a quiet fallback. See
[Errors](https://telluvian.ai/docs/errors.md#model-select).

## What it costs

A Gallery request costs the selected model's catalogue rates, plus the [Model
Select fee](https://telluvian.ai/docs/model-select.md#what-it-costs) of **$0.05 per 1M input tokens**
for making the choice. Naming a model yourself skips Model Select and its fee.

[Verification](https://telluvian.ai/docs/scores.md) is optional at $1.00 per 1M completion tokens and
applies only when `include_scores: true`.

See [Pricing](https://telluvian.ai/docs/pricing.md) for the full picture, or [all
models](https://telluvian.ai/docs/models.md) for the rate of whichever model you were served.

> **Your invoice names real models, never this id**
>
> `telluvian/gallery-1` has no token rate of its own and never appears as a line
> item. Model spend is recorded against the model that generated the tokens, so
> usage reporting keeps working without knowing that Gallery is in play. The
> Model Select fee is billed as part of that model's charge.

## When Gallery is unavailable

If Model Select cannot make the choice, the request fails with a `503` and
nothing is charged. It is never served by quietly substituting a model. A
Gallery request that silently became something else would be indistinguishable
from a working one, and you would find out from the bill.

One narrow substitution does happen. If the model chosen is not one the
catalogue can serve, the default model runs instead. The response names it like
any other, so it is visible in the same place you were already looking.

---

# Model Select

> The engine behind Gallery. Send a prompt, get back the model that should answer it.

Source: https://telluvian.ai/docs/model-select

Model Select decides which model should answer a request. It reads the prompt,
predicts how well each candidate model will handle it, and returns the cheapest
one expected to clear the quality bar you set, along with the reasoning effort
to run it at.

There are two ways to use it:

- **Through [Gallery](https://telluvian.ai/docs/gallery.md).** Set `model` to `telluvian/gallery-1` and
  Gallery asks Model Select for a choice, sends your request to that model, and
  returns the answer. This is the usual route.
- **Directly**, with `POST /v1/modelSelect`. You get the decision back and make
  the model call yourself. Use this when you call providers with your own keys,
  want to inspect or log the choice before acting on it, or want the decision
  without the generation.

Both take the same controls, make the same choice and cost the same.

## The controls

Five optional controls shape the choice. On a direct call they sit beside
`messages`. On a Gallery request they sit beside `model`.

| Control | Type | Default |
| --- | --- | --- |
| `xPerf` | number or model id | `0.9` |
| `xPerfEffort` | string | the named model's best |
| `xLatency` | `instant`, `chat` or `batch` | `chat` |
| `modelZoo` | comma-separated patterns | all active Model Select candidates |
| `sessionId` | GUID | a fresh one per request |

Each is validated on its own. A bad value is a `400` naming the field, never a
quiet fallback. See [Errors](https://telluvian.ai/docs/errors.md#model-select).

### xPerf, how good the answer has to be

The quality bar establishes a baseline: choose the cheapest candidate at or
above that level. Model Select can then choose a cheaper candidate below that
level if it predicts comparable success on this particular request. This is not
a guarantee of answer quality. Selection also considers estimated cache cost.

Reach for a model id. It reads as "at least as capable as this model":

```json
{"xPerf": "anthropic/claude-sonnet-5"}
```

You are not asking for that model. You are asking for that standard, and usually
getting something cheaper that meets it on this prompt. The provider prefix is optional, so
`claude-sonnet-5` means the same thing.

It also takes a number from `0.1` upwards, measured against a fixed reference at
`1.0`. That reference never moves, so a number you set today keeps its meaning,
and a model that beats it scores above `1.0`. The [model zoo](https://telluvian.ai/docs/model-zoo.md)
lists where each model falls.

> **Levels set the scale; the prompt decides the model**
>
> A model's published level is its general ability, one number, and it is what
> gives `xPerf` a fixed meaning. It is not the whole decision. How well a model
> will do on *your* prompt is estimated per request, which is how a model rated
> below your bar can still be the right choice for an easy one. If that
> per-prompt estimate is unavailable, selection falls back to the published
> levels alone: the cheapest model rated at or above the bar.

The default of `0.9` is deliberately high: left alone, Model Select saves money
without lowering your ceiling much. Drop it for work where a cheap model is
genuinely fine:

```json
{"xPerf": 0.5}
```

> **A bar nothing can meet gets you the best available, not an error**
>
> Ask for more than anything on offer can deliver and you get the strongest
> model that qualifies on your other constraints. The request succeeds, so a bar
> set too high shows up as a bill rather than a `400`. Check the chosen model if
> you are not sure your bar is doing anything.

> **Send the number unquoted**
>
> `"xPerf": "0.9"` is a string, and strings are read as model ids. Shell quoting
> and `String(x)` in JavaScript are the usual culprits. This is rejected rather
> than guessed at, with a `400` suggesting the unquoted form.

### xPerfEffort, which setting of that model you mean

Reasoning models are not one point on the scale. The same model at `low` and at
`max` are far apart in both quality and price. When `xPerf` names such a model,
`xPerfEffort` says which of its settings is the bar you mean:

```json
{"xPerf": "openai/gpt-5.6-terra", "xPerfEffort": "low"}
```

Accepted values are `minimal`, `low`, `medium`, `high`, `xhigh` and `max`. Omit
it and the named model is read at its best, which is what naming a model usually
implies.

This sets the reference quality level, not a direct generation parameter.
It only qualifies a named model. Sending it with a number is a `400` rather than
being ignored, and so is asking for a setting that model does not offer. The
error lists the ones it does.

### xLatency, reserved

`instant`, `chat` (the default), and `batch` are accepted and recorded, but
currently ignored during selection. They use the same candidate set and do not
override `xPerf`, force a default model, promise latency, or enable provider
batch pricing.

### modelZoo, which models are allowed at all

A comma-separated list of glob patterns. Only models matching at least one
pattern can be chosen. This is how you keep a request inside a provider you have
a data agreement with, or out of one you do not:

```json
{"modelZoo": "anthropic/*,openai/gpt-5*"}
```

Patterns match with or without the provider prefix and ignore case, so `*mini*`
and `openai/*` are both valid.

> **A pattern that matches nothing is an error**
>
> If no active Model Select candidate matches, the request fails with a `400`.
> It does not fall back to the full list. A typo in an allowlist should not
> quietly widen it.

### sessionId, keeping a conversation together

A GUID you resend on every turn of the same conversation:

```json
{"sessionId": "3f2b7c58-9d41-4e0a-9a7c-6f0b1c2d3e4f"}
```

Multi-turn conversations are cheaper when consecutive turns land on the same
model, because the history is already warm there. Passing a `sessionId` lets
that weigh on the choice, so a conversation tends to stay put unless there is a
real reason to move.

It is optional. Without one, every request is chosen on its own merits, which is
right for one-shot work. A session is forgotten after an hour of inactivity.

> **Where the id comes from**
>
> Send none and Model Select issues one. A direct call always returns it, as
> does a non-streamed Gallery response. A Gallery stream does not, so for
> streaming, mint a GUID when a conversation starts, store it with the
> conversation, and send the same one each turn. A new id every turn is the same
> as sending none.

## Calling it directly

`POST /v1/modelSelect` takes the conversation and the controls, and returns a
decision. Nothing is generated.

```bash
curl https://api.telluvian.ai/v1/modelSelect \
  -H "Authorization: Bearer $TELLUVIAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "Summarise this changelog in one line: ..."}],
    "xPerf": "openai/gpt-5.6-terra"
  }'
```

```python
import os, httpx

decision = httpx.post(
    "https://api.telluvian.ai/v1/modelSelect",
    headers={"Authorization": f"Bearer {os.environ['TELLUVIAN_API_KEY']}"},
    json={
        "messages": [{"role": "user", "content": "Summarise this changelog in one line: ..."}],
        "xPerf": "openai/gpt-5.6-terra",
    },
).json()

print(decision["model"], decision["reasoning"])
```

This is not an OpenAI endpoint, so the OpenAI SDKs have no method for it. Call
it with any HTTP client.

### The request

| Field | Rule |
| --- | --- |
| `messages` | Required. A string, treated as one user message, or an array of 1 to 2,000 messages whose last one has role `user`. |
| `xPerf`, `xPerfEffort`, `xLatency`, `modelZoo`, `sessionId` | Optional. See [the controls](#the-controls). |

Messages take roles `system`, `user` and `assistant`, with `content` as a
string or an array of `text` and `document` parts. Any other field is rejected
with a `400` rather than ignored, so a typo such as `xPrf` fails loudly instead
of selecting at the default. The body can be at most 1 MB.

> **Not yet supported: tools and images**
>
> A `tools` field is a `400`, and a message containing an image part is a
> `422`. Document parts restrict the choice to models that accept documents.

### The response

```json
{
  "model": "claude-sonnet-5",
  "reasoning": { "effort": "low" },
  "sessionId": "3f2b7c58-9d41-4e0a-9a7c-6f0b1c2d3e4f",
  "cost": 0.0000015
}
```

| Field | Meaning |
| --- | --- |
| `model` | The chosen model's id, without a provider prefix. |
| `reasoning` | `{"effort": ...}` to send to the model, or `null` when the model takes no reasoning effort. |
| `sessionId` | The one you sent, or a newly issued GUID. Resend it next turn. |
| `cost` | What this call was billed, in USD. See [what it costs](#what-it-costs). |

The id names the model, not who hosts it. Send it as `model` to Telluvian's
`/v1/chat/completions` as-is, or map it to your provider's name for that model.
When `reasoning` is not `null`, pass its `effort` as the provider's reasoning
effort parameter. When it is `null`, send none.

### Errors

Errors from a direct call use their own envelope, with a stable `code` and the
offending `field`:

```json
{ "error": { "code": "out_of_range", "message": "xPerf must be at least 0.1.", "field": "xPerf" } }
```

| Status | `code` | When |
| --- | --- | --- |
| `400` | `invalid_messages` | `messages` missing, empty or malformed, or the last message is not `user`. |
| `400` | `out_of_range` | Numeric `xPerf` below `0.1`, a quoted number such as `"0.9"`, or an unknown `xLatency`. |
| `400` | `unknown_model` | `xPerf` names a model we do not know. |
| `400` | `invalid_effort` | `xPerfEffort` without a model-id `xPerf`, or an effort that model does not offer. |
| `400` | `empty_model_zoo` | `modelZoo` matches no active candidate. |
| `400` | `invalid_session_id` | `sessionId` is not a GUID. |
| `400` | `unknown_field` | A field not listed above. |
| `413` | | The body is over 1 MB. |
| `422` | `messages_too_large` | No candidate's context window fits the messages. |
| `422` | `unsupported_input` | No candidate accepts the messages' images or documents. |
| `503` | | Model Select is unavailable. Retry with backoff. |

Authentication, credit and rate-limit errors are the same as on every other
endpoint. See [Errors](https://telluvian.ai/docs/errors.md).

## What it costs

Model Select costs **$0.05 per 1M input tokens**, charged on the whole prompt,
cached tokens included. The fee is the same whichever way you use it:

- **Direct:** the fee is the whole charge, recorded on your usage as
  `modelSelect`. No model runs.
- **Through Gallery:** the fee is added to the selected model's generation
  charge.

A failed choice, whether a `400` or a `503`, is not charged. Naming a model
yourself skips Model Select and its fee. See [Pricing](https://telluvian.ai/docs/pricing.md).

---

# Model zoo

> Published xPerf levels and rates for the models Model Select chooses between.

Source: https://telluvian.ai/docs/model-zoo

The zoo is the set of models [Model Select](https://telluvian.ai/docs/model-select.md) chooses between. This page
lists published **xPerf levels**. Active Model Select candidates can also use inferred
levels, so absence from this table does not necessarily make a model invalid
as a quality bar:

```json
{"xPerf": "anthropic/claude-sonnet-5"}
```

That reads as "at least as capable as this model", and it is the same as sending
the number in its xPerf column. Naming the model is usually better, because the
number moves when a model is re-measured and the name does not.

## Levels

A level measures a model against a fixed reference point. The reference is
`1.0` and it never moves, so a level means the same thing in a year as it does
today, and a bar you set now does not quietly tighten as better models arrive.

`1.0` is not a ceiling. The reference was set at the frontier of the day, which
does not impose a maximum: a model that beats the reference scores **above**
`1.0`.

> **A level is a general ranking, not a verdict on your prompt**
>
> A level measures a model's ability across a broad, hard benchmark. Your
> request is one prompt, and on an easy one a lower-ranked model often answers
> as well as the strongest models do. For that request it meets an `xPerf` above
> its own level, and routing will choose it. On a hard prompt the same model
> falls short and is passed over. See [how `xPerf` is
> applied](https://telluvian.ai/docs/model-select.md#xperf-how-good-the-answer-has-to-be).

The floor is `0.1`, which is also the smallest number `xPerf` accepts. Anything
below that is not a valid requested quality bar.

| Model | xPerf | Input / 1M | Output / 1M |
| --- | ---: | ---: | ---: |
| `anthropic/claude-fable-5-1` | 1.00 | $10.00 | $50.00 |
| `openai/gpt-6-astra` | 1.00 | $10.00 | $50.00 |
| `anthropic/claude-opus-5` | 0.96 | $5.00 | $25.00 |
| `meta/muse-spark-1.3` | 0.91 | $1.25 | $4.25 |
| `openai/gpt-5.6-sol` | 0.89 | $2.00 | $10.00 |
| `qwen/qwen3.8-max-0902` | 0.85 | $2.00 | $6.00 |
| `moonshotai/kimi-k3` | 0.83 | $3.00 | $15.00 |
| `x-ai/grok-4.6` | 0.83 | $2.00 | $6.00 |
| `openai/gpt-5.6-terra` | 0.79 | $2.00 | $12.00 |
| `z-ai/glm-5.3-flash` | 0.79 | $0.04 | $0.14 |
| `google/gemini-3.8-flash` | 0.77 | $0.75 | $3.75 |
| `anthropic/claude-sonnet-5` | 0.72 | $2.00 | $10.00 |
| `openai/gpt-5.6-luna` | 0.72 | $0.20 | $1.20 |
| `qwen/qwen3.8-27b` | 0.64 | $0.42 | $3.00 |
| `anthropic/claude-haiku-4-5-20251001` | 0.34 | $1.00 | $5.00 |
| `google/gemma-4-31b-it` | 0.28 | $0.09 | $0.34 |

16 models. Rates are live; levels were published 2026-10-06.

> **Each level is the model at its best**
>
> A reasoning model is not one point on this scale. The number above is its
> highest effort, which is what an `xPerf` naming it resolves to when you send
> no [`xPerfEffort`](https://telluvian.ai/docs/model-select.md#xperfeffort-which-setting-of-that-model-you-mean).
> The same model at `low` sits well below its row here. `claude-opus-5` runs
> from `0.75` at `low` to `0.96` at `max`.

## Models without a published level

The serving catalogue and the Model Select candidate set are different. A model
can be available for a direct completion without being a candidate of the active
Model Select checkpoint.

For `xPerf`, Model Select uses a published level when available; otherwise it
can use an inferred level for an active candidate. A model for which neither
is available returns `400`. `modelZoo` filters active Model Select candidates, not
the entire serving catalogue:

```json
{"modelZoo": "anthropic/*,openai/gpt-5*"}
```

A pattern matching no active candidate is rejected.

## Reading the table

Rates come from the gateway catalogue, as on [Pricing](https://telluvian.ai/docs/pricing.md). The
website caches catalogue data, so a recent update may not appear immediately.

The levels are a snapshot, and the date under the table says when it was taken.
They change only when a model is re-measured, which is rare, but it does mean a
level here can lag the one the API is using. Prices can also lag during cache revalidation.

---

# Compatibility

> Exactly which OpenAI request fields are supported, translated, ignored, or dropped.

Source: https://telluvian.ai/docs/compatibility

The API speaks two OpenAI interfaces and nothing else: **Chat Completions** and
the **[Responses API](https://telluvian.ai/docs/responses.md)**. There is no embeddings,
audio, image generation, moderation, or realtime endpoint. If your integration
only needs chat, the base-URL swap in the [quickstart](https://telluvian.ai/docs/quickstart.md) is the
whole migration.

"OpenAI-compatible" still leaves a lot unstated, and a field that is silently
ignored rather than rejected is the kind of thing that only shows up in
production. This page is the precise contract for **Chat Completions**
(`POST /v1/chat/completions`), sourced directly from the request schema the
server validates against, not from prose that can drift out of sync with it.
Every unrecognized field is accepted and ignored **except** where noted under
[Rejected, not dropped](#rejected-not-dropped).

## Supported as-is

These map straight onto the model's sampling parameters, unchanged:

`model`, `messages`, `stream`, `max_tokens`, `max_completion_tokens`
(OpenAI's o-series alias for `max_tokens`), `temperature`, `top_p`, `n`, `stop`,
`presence_penalty`, `frequency_penalty`, `seed`.

> **messages roles**
>
> Only `system`, `user`, and `assistant` are accepted. There is no `tool` role
> and no `developer` role (the o-series alias for `system`). See
> [No tool calling](#no-tool-calling) below.

## Translated

The shape differs from OpenAI's because the underlying serving engine's
parameters differ. The request is accepted in OpenAI's shape and converted:

| Field | OpenAI shape | What happens |
| --- | --- | --- |
| `logprobs` / `top_logprobs` | `logprobs: bool` + `top_logprobs: int` | Combined into a single top-k count |
| `logit_bias` | string keys (stringified token ids) | Converted to integer token ids |
| `response_format` | `{"type": "json_object"}` or `{"type": "json_schema", ...}` | `json_object` maps to guided decoding. `json_schema` is **best-effort**, and complex schemas may not be enforced exactly |

## Accepted, no effect

Present in the request schema so SDKs don't error, but nothing on the serving
path reads them:

`user`, `store`, `metadata`, `stream_options`.

> **store and stream_options do nothing here**
>
> These matter on OpenAI's own API and on Telluvian's [Responses
> endpoint](https://telluvian.ai/docs/responses.md#managing-stored-responses). But on **Chat
> Completions specifically**, they are accepted and ignored. Usage always
> arrives on the final stream chunk regardless of `stream_options`; see
> [Usage is always on the last chunk](https://telluvian.ai/docs/curl.md#streaming).

## Not in the schema, silently dropped

Anything not listed above or on the [`include_scores`
page](https://telluvian.ai/docs/include-scores.md) is **not rejected**. The request still succeeds;
the unrecognized field is dropped before the request reaches the model.
Concretely, that includes:

### No tool calling

`tools`, `tool_choice`, `functions`, `function_call`, and `parallel_tool_calls`
are all silently dropped. There is no tool/function-calling support, and no
`tool` message role to carry a result back. Sending a tool-calling request
gets you back a plain text completion as if `tools` had never been set.

### Anything else not listed above

`logit_bias` beyond the translation noted, reasoning-effort style fields,
`service_tier`, and any other field not in the tables above. Same rule:
accepted request, field ignored.

## Rejected, not dropped

One shape is a hard error rather than a silent drop:

### No vision / multimodal input

Every model is text-in, text-out. `content` must be a **plain string**. A
content array, `[{"type": "image_url", ...}]`, is **rejected with a `422`**,
not ignored like the fields above.

> **The 422 is not OpenAI-shaped**
>
> This error is raised by the model server's schema validation and passes
> through unchanged, so its body is `{"detail": [...]}` rather than the
> `{"error": {...}}` object [every other status](https://telluvian.ai/docs/errors.md) returns. Parse
> defensively if you handle it.

> **Check the response, not just the request you sent**
>
> A silently dropped field is the single easiest way to lose hours: your code
> looks correct, the request succeeds, and the behavior you expected just
> never happens. If a feature you're relying on doesn't seem to work, confirm
> it's actually in the tables above before debugging anything else. The same
> advice as [verifying `include_scores`](https://telluvian.ai/docs/include-scores.md#verifying-what-you-got).

## Telluvian extensions

Four things exist only on this API, layered on top of the OpenAI shape:

- **`include_scores`** enables verification when explicitly set to `true`; it defaults to `false`. See the
  [dedicated page](https://telluvian.ai/docs/include-scores.md).
- **`tokens` / `scores`** on the response carry the probe's output. See [Reading
  the scores](https://telluvian.ai/docs/scores.md).
- **The `-analyze` model suffix** scores text you already have instead of
  generating any. It is a suffix on `model`, so it passes through an unmodified
  OpenAI SDK untouched. See [Scoring existing text](https://telluvian.ai/docs/analyze.md).
- **The Model Select controls**: `xPerf`, `xPerfEffort`, `xLatency`, `modelZoo` and
  `sessionId`. Unlike every other extra field on this page these are read rather
  than ignored, but **only** when `model` is `telluvian/gallery-1`. They are consumed
  by the choice and stripped before the model is called. `xLatency` is accepted
  and recorded but currently ignored. Sent with any other
  model they are ordinary unrecognized fields, and ignored. See
  [Model Select](https://telluvian.ai/docs/model-select.md#the-controls).

## The Responses API

[`/v1/responses`](https://telluvian.ai/docs/responses.md) is a separate, stateful interface built on
the same underlying model. The same "no tool calling, no multimodal input"
limits apply there too. The constraint is the model and probe pipeline, not
which endpoint you call.

---

# Pricing

> What routing costs, what verification costs, and how the prepaid balance works.

Source: https://telluvian.ai/docs/pricing

| What you pay for | Rate |
| --- | --- |
| **Model tokens** | Gateway catalogue rates, based on provider pricing. |
| **Model Select** | $0.05 per 1M input tokens, only when we pick the model. |
| **Verification** | $1.00 per 1M completion tokens. Optional. |

Model generation uses the gateway catalogue rates. Verification is charged
only when explicitly enabled; the Model Select fee only when Gallery or a direct `/v1/modelSelect`
call picks the model.

## Your balance

Billing is **prepaid**. You add credit up front and each request debits your
balance. There is no subscription and no invoice at the end of the month.

- **Your balance is in dollars.** A $25 payment adds $25 of API credit. There
  is no separate credit currency and no exchange rate to work out.
- Top-ups run between **$5.00** and **$500.00** per transaction, paid by card
  through Stripe. We never see or store your card details.
- Cost is settled *after* each response, from the token counts the model
  reports.
- Credit does not expire and is not transferable. Unused credit is refundable on
  request. See the [Terms](/terms).

When your balance reaches zero, requests are refused with a `402`. Nothing is
charged automatically.

## Model tokens

The gateway bills its published model rates, based on provider pricing.
Provider updates and discounts may not be reflected until the catalogue is
refreshed; use the gateway rate card to calculate your Telluvian charge.

Dollars per million tokens, for the models most people start with. Read live
from the gateway catalogue, with website caching. Rates can lag a recent update.
See [all models](https://telluvian.ai/docs/models.md) for the full catalogue.

| Model | Input / 1M | Output / 1M |
| --- | ---: | ---: |
| `google/gemma-4-31b-it` | $0.09 | $0.34 |
| `anthropic/claude-opus-5` | $5.00 | $25.00 |
| `anthropic/claude-sonnet-5` | $2.00 | $10.00 |
| `openai/gpt-5.6-terra` | $2.00 | $12.00 |
| `openai/gpt-5.5` | $5.00 | $30.00 |
| `openai/gpt-5.4` | $2.50 | $15.00 |
| `openai/gpt-5.4-mini` | $0.75 | $4.50 |
| `openai/gpt-4o-mini` | $0.15 | $0.60 |
| `deepseek/deepseek-chat-v3.1` | $0.25 | $0.95 |
| `x-ai/grok-4.6` | $2.00 | $6.00 |

All models (191): https://telluvian.ai/docs/models.md

## Gallery and Model Select

[Model Select](https://telluvian.ai/docs/model-select.md) costs **$0.05 per 1M input tokens**, on both
[Gallery](https://telluvian.ai/docs/gallery.md) requests and direct `/v1/modelSelect` calls. It applies
to the whole prompt, cached tokens included, because Model Select reads every
token whatever the provider later serves from its cache. It is never scaled by a
long-context tier.

Gallery completions also incur the selected model's generation charges. Direct
Model Select calls generate no completion, so the fee is the whole charge.
Naming a model yourself skips Model Select and is not charged the fee.

## Verification

Verification costs **$1.00 per 1M completion tokens**, on top of the model's
pass-through rate.

> **Completion tokens only**
>
> The probe scores each token as it is generated, so the work scales with how
> much the model writes, not with how long your prompt was. A 100k-token prompt
> with a short answer pays the fee only on that short answer.

Enable it with [`include_scores: true`](https://telluvian.ai/docs/include-scores.md). Omitted or false
means no verification surcharge on a normal completion. Analyze requests also
require explicit opt-in.

### Calculating a charge

Generation depends on the selected model, input and visible output tokens,
reasoning tokens, cache reads/writes, and any long-context tier. Use the current
catalogue rates rather than a fixed price table copied into application code.

Verification adds $0.30 for 300,000 completion tokens when enabled,
regardless of prompt length. It adds nothing when disabled. Routing adds $0.01
for a 200,000-token prompt when we pick the model, and nothing when you name
one yourself.

Usage may be estimated if a stream ends before the provider reports its final
counts; stored usage records distinguish estimates from reported counts.

## Reasoning tokens

Some models spend part of their budget on internal reasoning that never appears
in the output. OpenAI's `gpt-5` family most notably, which can consume a whole
budget and return no visible text at all.

These are reported separately as `usage.reasoning_tokens` and billed at the
model's **standard output rate**, passed through with no markup, exactly like
every other token.

> **No verification fee on reasoning tokens**
>
> The probe scores tokens by reading the hidden states of the pass that produced
> them. Reasoning tokens never reach our probe model, so no probe pass runs on
> them and the $1.00 fee does not apply.

They are **not** included in `completion_tokens` or `total_tokens`, so a
response's token counts will not sum to what you were billed for on a reasoning
model. Read `reasoning_tokens` explicitly if you are reconciling spend. It is
`0` for every non-reasoning model.

> **How money is counted**
>
> Balances are integer micro-dollars ($1 = 1,000,000 micros) and every charge is
> computed in integers, rounded up once at the end. No floating point touches the
> billing path, so a price can never drift by a fraction of a cent.

---

# Errors and rate limits

> Status codes, error shapes, and throttling.

Source: https://telluvian.ai/docs/errors

Errors are OpenAI-shaped: a JSON body with an `error` object carrying
`message`, `type`, and `code`. The one exception is `422`, covered below.

```json
{
  "error": {
    "message": "Insufficient credits. Top up your balance in the dashboard.",
    "type": "invalid_request_error",
    "code": "insufficient_credits"
  }
}
```

## Status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Malformed JSON, invalid Model Select controls, or analyze without probe opt-in. | Check the body; analyze requires `include_scores: true`. |
| `401` | Missing, unknown, or revoked API key. | Check the `Authorization` header, or mint a new key. |
| `402` | Balance exhausted. | [Add credit](https://telluvian.ai/dashboard/billing). |
| `403` | Account suspended. | [Contact us](mailto:hello@telluvian.ai). |
| `404` | Unknown model, or unknown `previous_response_id`. | Call `GET /v1/models`, or check the id. |
| `422` | Request body failed schema validation: a non-string `content`, or an [`-analyze`](https://telluvian.ai/docs/analyze.md) call with no `assistant` message. | Check the shape against [Compatibility](https://telluvian.ai/docs/compatibility.md). |
| `429` | Rate limited. | Back off for `Retry-After` seconds. |
| `503` | [Model Select](https://telluvian.ai/docs/model-select.md) is unavailable. | Retry with backoff, or name a model yourself. |

> **422 has a different body shape**
>
> A `422` comes from the model server's own validation and is relayed
> unchanged, so it carries `{"detail": ...}` rather than the `error` object
> above. It is a string for the analyze case, and a list of field errors for a
> schema violation. Nothing is billed for a rejected request.

A `404` for an unrecognised model carries `code: "model_not_found"` and names
the offending field in `param`:

```json
{
  "error": {
    "message": "The model 'gpt-9' does not exist or you do not have access to it. Call GET /v1/models for the available models, or see https://telluvian.ai/docs.",
    "type": "invalid_request_error",
    "param": "model",
    "code": "model_not_found"
  }
}
```

### Model Select

A [Model Select control](https://telluvian.ai/docs/model-select.md#the-controls) that cannot be
honoured is rejected before anything is generated, and nothing is billed. The
message names the field. A direct `/v1/modelSelect` call returns its own error
envelope with a stable `code`; see [Model Select errors](https://telluvian.ai/docs/model-select.md#errors). The common causes:

| Status | Cause |
| --- | --- |
| `400` | `xPerf` sent as a quoted number, or naming a model we do not know. |
| `400` | `xPerfEffort` sent alongside a numeric `xPerf`, or an effort the named model does not offer. |
| `400` | `xLatency` outside `instant`, `chat` and `batch`. |
| `400` | `modelZoo` matching no active Model Select candidate. |
| `400` | `sessionId` that is not a GUID. |
| `503` | Model Select is unavailable. Nothing is substituted. See [when Gallery is unavailable](https://telluvian.ai/docs/gallery.md#when-gallery-is-unavailable). |

A quality bar that nothing can meet is **not** an error: the request succeeds
on the best model available. Read `response.model` to see what ran.

## Rate limits

Chat Completions and Responses normally carry these headers when rate-limit
checks complete successfully:

- `X-RateLimit-Limit`, requests allowed in the current window
- `X-RateLimit-Remaining`, how many are left

A `429` additionally carries `Retry-After`, in seconds.

Limits are per API key, **600 requests per 60-second window by default**. That
is generous, because spend is already gated by your balance. It is an abuse
ceiling rather than a quota. A rejected request still counts against the
window, so hammering the endpoint keeps it pinned rather than earning a fresh
allowance. If you need a higher limit, [get in touch](mailto:hello@telluvian.ai).

> **The limiter fails open**
>
> Counting happens centrally, not per server instance. The gateway runs on
> several at once, and an in-process counter on each would both admit multiples
> of the intended limit and reset every time an instance scales. If that count
> itself is unreachable (a database blip), **the request is allowed through**
> rather than rejected: a limiter that hard-fails every request during a
> partial outage would turn it into a total one. Your balance is the backstop
> either way. A burst let through during a blip still has to be paid for.

> **Balances can dip slightly negative**
>
> The balance check happens before your request runs, but the cost is settled
> after it finishes. A request admitted with a small positive balance can take
> it just below zero, bounded by your `max_tokens`. The next request is then
> refused with a `402`.

## Streaming failures

A stream can fail after it has already started, after your client has
received a `200` and some content chunks. There is no structured SSE error
event for this: the connection is simply closed without a trailing
`data: [DONE]`.

> **A stream without [DONE] is a failure, not a slow finish**
>
> Treat any stream that ends without the literal `data: [DONE]` line as
> failed, not as "still coming." Retry the request rather than waiting.

Billing settles from whatever token counts were observed before the failure (or an estimate when final usage is unavailable),
so a request that fails partway through can still incur a (smaller) charge for
the tokens the model had already generated, the same way a partial response
you kept and used would be billed. This is the same settlement path either
way; see [Pricing](https://telluvian.ai/docs/pricing.md).
