# Telluvian documentation > Every page of https://telluvian.ai/docs as Markdown, in reading order. The index is https://telluvian.ai/llms.txt. --- # Introduction > An OpenAI-compatible API that picks the right model for every request, and can score every token it returns. Source: https://telluvian.ai/docs Telluvian is an OpenAI-compatible API with two products on one endpoint. **Routing** picks which model answers each request. **Verification** can score every token it returns for hallucination, when you turn it on. Use either, or both. The API is OpenAI-compatible. If you can call OpenAI, you can call this. Change the base URL and the key. ```bash https://api.telluvian.ai/v1 ``` ## Routing Model choice is usually made once and left alone, which means a frontier model answers your cheap requests and a small model answers your hard ones. **Gallery** is our router, and you call it like a model. Send `telluvian/gallery-1` and each request goes to the cheapest model that we expect clears the quality bar you set. The response reports which model ran. See [Gallery](https://telluvian.ai/docs/gallery.md). **Model Select** is the engine behind it. It reads each prompt and predicts how well every candidate model will handle it. You can also call it directly with `/v1/modelSelect` to get the decision and make the model call yourself. See [Model Select](https://telluvian.ai/docs/model-select.md). You control the quality bar and the allowed models. `xLatency` is currently accepted but ignored. Billing follows the chosen model's own rates, plus a $0.05 per 1M input-token Model Select fee. ## Verification When enabled, verification returns per-token scores indicating potentially unsupported text. These are not calibrated probabilities; readings can be missing. Not one number for the whole response: one score per generated token, so you can point at the specific claim that looks wrong. Externally generated text is replayed through our probe model on our GPUs; locally served models can be scored from their own forward pass. The probe uses those hidden states to score tokens. Scores arrive per chunk when streaming, or as arrays on the completed response. Verification is off by default; enable it per request with `include_scores: true`. See [Reading the scores](https://telluvian.ai/docs/scores.md). ## Next steps - [Quickstart](https://telluvian.ai/docs/quickstart.md): Your first routed request. - [Gallery](https://telluvian.ai/docs/gallery.md): A router you call like a model. - [Model Select](https://telluvian.ai/docs/model-select.md): The engine that picks the model. - [Pricing](https://telluvian.ai/docs/pricing.md): What each product costs. - [Reading the scores](https://telluvian.ai/docs/scores.md): What the numbers mean. - [include_scores](https://telluvian.ai/docs/include-scores.md): Turning verification on and off. - [Compatibility](https://telluvian.ai/docs/compatibility.md): What's supported, translated, or silently dropped. - [curl recipes](https://telluvian.ai/docs/curl.md): Every call shape, including streaming. - [SDKs](https://telluvian.ai/docs/sdks.md): The OpenAI Python and TypeScript SDKs. - [Errors](https://telluvian.ai/docs/errors.md): Status codes and rate limits. --- # 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" } ``` | 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. | 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. --- # Reading the scores > What the probe returns, and how to interpret it. Source: https://telluvian.ai/docs/scores Verification is optional and off by default. Send [`include_scores: true`](https://telluvian.ai/docs/include-scores.md) to request a `scores` object alongside the usual OpenAI fields, mapping each probe name to its output. > **Choose which probes run** > > Discover available IDs and descriptions with authenticated `GET /v1/probes`. > Send `include_scores: true` and `probes: ["hallucination"]` to request only > hallucination detection. Omitting `probes` while opting in requests all loaded > response probes; omitting `include_scores` runs none. This also applies to > `telluvian/gallery-1`. See the [selection table](https://telluvian.ai/docs/include-scores.md#choosing-probes). > Keys in `scores` identify the returned probes. A missing key or `null` reading > means no score was returned, not that the text passed verification. ## Non-streaming `scores` holds one array per probe, the same length as the `tokens` array: ```json { "choices": [{ "message": { "content": "The Eiffel Tower opened in 1889." } }], "tokens": ["The", " Eiffel", " Tower", " opened", " in", " 1889", "."], "scores": { "hallucination": [0.01, 0.02, 0.02, 0.05, 0.03, 0.61, 0.01] } } ``` `tokens` comes back alongside it: the decoded text of each generated token, in order, such that joining them reproduces `content` exactly. This is what makes the scores usable. Without it you would hold N scores against one opaque string and have to re-tokenise with the identical tokeniser to line them up. ```python for token, score in zip(response.tokens, response.scores["hallucination"]): if score is not None and score > 0.5: print(f"flagged: {token!r}") # flagged: ' 1889' ``` > **Scoring text you did not generate here** > > The same `tokens`/`scores` pairing is available for text that already > exists, such as another model's output or an archived transcript, via the > `-analyze` model > suffix. The token boundaries there are always the probe model's, which needs > care if you are holding a token list from another provider: see [Scoring > existing text](https://telluvian.ai/docs/analyze.md). > **An individual score may be null** > > The score array is **always** the same length as `tokens`, so > `scores[probe][i]` always describes `tokens[i]`. An individual entry can > still be `null` when the probe's reading for that token was unavailable: > most often the last token of a response cut short by `max_tokens`. > > A `null` means "no score for this token", not "a score of zero". Check for it > before comparing against a threshold, as above. ## Streaming With `include_scores: true`, content chunks can carry one number per probe. Role, finish, and unavailable-score chunks may omit `scores`: ```python stream = client.chat.completions.create( model="google/gemma-4-31b-it", extra_body={"include_scores": True, "probes": ["hallucination"]}, messages=[{"role": "user", "content": "Tell me about Ada Lovelace."}], stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content or "" score = (getattr(chunk, "scores", None) or {}).get("hallucination") print(delta, score) ``` ## Turning verification off Verification is off by default. Omit `include_scores`, or explicitly pass `include_scores: false`, for a plain completion: ```json { "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}], "include_scores": false } ``` Both `scores` and `tokens` come back `null`, and the verification fee is not charged. In the Python SDK this must go through `extra_body`; see [`include_scores`](https://telluvian.ai/docs/include-scores.md) for the details and [Pricing](https://telluvian.ai/docs/pricing.md) for what it saves. ## Interpreting a score For the `hallucination` probe, a higher score means the probe considers the token more likely to be unsupported. Other probes measure different properties; do not interpret every probe's output as a hallucination score. > **The score is not a probability of error** > > The scale is not calibrated to "chance this is wrong", and the useful cutoff > is not 0.5. Pick a threshold from your own tolerance for false positives, and > treat a flag as a prompt to verify a claim rather than a verdict. Email > [hello@telluvian.ai](mailto:hello@telluvian.ai) for the current operating > point and its measured precision and recall. ### How the score is produced Externally generated completions are replayed through our probe model on our GPUs. Locally served models can instead be scored from their own forward pass. Each token is forced during the replay, so the probe reads hidden states for text it did not itself generate. That is a reconstruction rather than a direct reading, and it adds latency proportional to how much the model wrote. Routed models use this replay mechanism, including models [picked for you by Gallery](https://telluvian.ai/docs/gallery.md). Scores still need validation on your workload; the replay does not expose the original provider model's hidden states. --- # include_scores > The switch that controls whether the probe runs, what it changes in the response, and what it costs. Source: https://telluvian.ai/docs/include-scores `include_scores` decides whether [verification](https://telluvian.ai/docs/scores.md) runs for a request. It is a boolean and **defaults to `false`**. Use `probes` to specify exactly which response probes to run when you opt in. ```json { "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}], "include_scores": true } ``` Leave it out, or send `false`, and the request is an ordinary unscored completion. ## What it changes | | `true` (opt-in) | `false` (default) | | --- | --- | --- | | `choices[0].message.content` | the model's answer | the model's answer | | `usage`, `finish_reason` | unchanged | unchanged | | `scores`, the probe's output | present | `null` | | `tokens`, the array those scores align to | present | `null` | | `scores` on stream chunks | present | absent | | Verification fee | $1.00 / 1M completion tokens | not charged | | Latency | probe pass included | no probe pass | **You always get the answer.** Only the two probe extensions go away. `include_scores: false` is a request for an unscored completion, not for a shorter one. The generation settings are unchanged, although separate sampled requests can produce different answers. `tokens` is the one to read carefully. It is not the completion: it is the decoded text of each generated token, in order, such that joining it reproduces `content` exactly. It exists so a score has something to point at, so it is `null` when there are no scores. The text itself is in `content`, as always. ## Sending it ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}], "include_scores": true }' ``` ```python # 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="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello"}], extra_body={"include_scores": True}, ) ``` ```typescript // Goes inline: the JS SDK forwards unknown top-level keys as-is. const response = await client.chat.completions.create({ model: 'google/gemma-4-31b-it', messages: [{ role: 'user', content: 'Hello' }], include_scores: true, } as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean }); ``` > **A dropped opt-in means no verification** > > If `include_scores: true` does not reach the API, verification stays off. > In Python use `extra_body`; check the response for scores rather than assuming > a framework forwarded the field. A missing token score is not a score of zero. ## When to turn it on Turn it on for anything a user reads as a factual claim. Leave it off where the score would not change what you do: - **Cheap intermediate steps** in a chain: dispatch, classification, reformatting, where nothing factual is being asserted. - **Re-running a prompt you have already verified**, e.g. regenerating output from a template with the same facts. ## Choosing probes Discover the probes loaded by the backend: ```bash curl -sS https://api.telluvian.ai/v1/probes \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" ``` Use IDs from the response's `data` array in `probes`. This works with both a named generation model and `telluvian/gallery-1`, on Chat Completions and the Responses API: ```json { "model": "telluvian/gallery-1", "messages": [{"role": "user", "content": "When was the Eiffel Tower built?"}], "include_scores": true, "probes": ["hallucination"] } ``` With probing enabled, omitting `probes` runs all loaded probes. A non-empty list selects only those probes; unknown names return `400`. An empty list disables probing and its surcharge. Supplying names alone does not enable probing: `include_scores: true` is still required. | Request fields | Response probes run | | --- | --- | | `include_scores` omitted or `false` | None, even if `probes` names a probe | | `include_scores: true`, `probes: ["hallucination"]` | Only `hallucination`, if available | | `include_scores: true`, `probes` omitted or `null` | All currently loaded response probes | | `probes: []` | None; no verification surcharge | | `include_scores: true`, an unknown probe ID | Request rejected with `400` | **Use an explicit list when you want a fixed set of probes.** The set selected by omitting `probes` can change as probes are loaded. `GET /v1/probes` lists available probes, not probes automatically enabled for your requests. Read each entry's `id` and `description`; do not assume that a name shown in an example is available on every deployment. Gallery's routing probe selects the generation model. It is separate from these optional response probes: using `telluvian/gallery-1` does not enable hallucination detection or add response probe scores. Each selected probe's ID is also its key in `scores`. In the Python SDK, pass both controls through `extra_body`, for example `extra_body={"include_scores": True, "probes": ["hallucination"]}`. The older `probe_path` selector is deprecated and only accepts an already loaded probe; it cannot load arbitrary files or be combined with `probes`. ## Per-request, not per-key There is no account-level setting. Each request decides for itself, so a single API key can serve both scored and unscored traffic. One client can verify user-facing answers while another skips the probe for internal bookkeeping. ## Verifying what you got The honest check is the response itself, not the request you think you sent: ```python response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello"}], extra_body={"include_scores": True}, ) assert response.scores is not None, "the opt-in was dropped: no verification ran" ``` Your usage records also carry the per-request cost, so a scored request is visible in [the dashboard](https://telluvian.ai/dashboard) as a higher `cost_micros` than an unscored one with the same token counts. --- # Scoring existing text > The -analyze suffix scores text you already have, and how to align its tokens with your own. Source: https://telluvian.ai/docs/analyze Normally the probe scores tokens as the model generates them. The `-analyze` suffix scores text that **already exists**: output from another model, an archived transcript, a draft a human wrote. Nothing is generated. > **Looking for “bring your own frontier-model tokens”?** > > You don't need it, and that is not a workaround. It is the design. **This > endpoint never calls your model provider, so there is no key to bring.** You > generate on your own account, with your own key, and send us only the > finished text to score. Telluvian does not have to be your gateway. Jump to > [the recipe](#you-dont-need-to-generate-through-us). Append `-analyze` to a catalogued model id, set `include_scores: true`, and put the text to score in an `assistant` message: ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it-analyze", "messages": [ { "role": "user", "content": "When did the Eiffel Tower open?" }, { "role": "assistant", "content": "The Eiffel Tower opened in 1889." } ], "include_scores": true }' ``` The response is an ordinary completion object. `content` is the text you sent back verbatim, and `tokens` / `scores` line up with it exactly as they do on [a generated response](https://telluvian.ai/docs/scores.md): ```json { "model": "google/gemma-4-31b-it", "choices": [{ "message": { "content": "The Eiffel Tower opened in 1889." } }], "usage": { "prompt_tokens": 47, "completion_tokens": 11, "total_tokens": 58 }, "tokens": ["The", " Eiffel", " Tower", " opened", " in", " ", "1", "8", "8", "9", "."], "scores": { "hallucination": [0.0, 0.0, 0.0, 0.0, 0.001, ...] } } ``` ## How the messages are read The rule is positional, not structural: - **The last `assistant` message is the text scored.** Nothing else is. - **The last `user` message before it** becomes the context the text is scored against. It is a framing prompt, not something the model answers. Everything earlier in `messages` is ignored. To score a multi-turn transcript, send one request per assistant turn rather than the whole conversation. > **No assistant message is a 422** > > A request whose `messages` contain no `assistant` role fails with > **422, No assistant message found to analyze**. This is the one analyze-mode > error that is not shared with the normal chat path, and it is easy to hit by > sending the conversation exactly as you would for a completion. The user turn is genuinely worth setting, and its effect is larger than it looks. The probe judges the text against the context it is given, so the same assistant text scores differently under different user turns. Scoring one identical false claim under two phrasings can produce different scores. The following values illustrate the effect; they are not an operating benchmark: | User turn | Score on `indexed` | | --- | --- | | `Fix the off-by-one in this loop and explain the change.` | 0.626 | | `Explain the fix in this diff.` | 0.466 | Same text, same probe, and a 0.5 threshold flags the first and misses the second. Send the **real** prompt that produced the text rather than a summary of it, and keep it stable across a batch you intend to compare. A prompt that varies run to run moves your scores independently of the content. If you omit the user turn entirely, a generic one is substituted. ## The tokenizer is always the probe's This is the part that surprises people, and it is the reason this page exists. The `-analyze` suffix changes **what is scored**, never **what does the scoring**. The probe reads hidden states, and hidden states only exist inside the model that runs the forward pass, so analyze always prefills your text through the local Gemma model, whatever id you named. ```json // request: "model": "anthropic/claude-opus-5-analyze" // response: { "model": "anthropic/claude-opus-5", "tokens": ["The", " Eiffel", " Tower", " opened", " in", " ", "1", "8", "8", "9", "."] } ``` Those are Gemma's tokens, not Anthropic's, even though the response echoes the Anthropic model id. The suffix on a replayed model affects billing and labelling. It does **not** send the scoring pass anywhere else. This is a statement about the *tokenizer*, not about which id you should send. You should still name the model that generated the text. See [Name the model that wrote the text](#you-dont-need-to-generate-through-us) below. The point here is only that whatever you name, the token boundaries coming back are the probe model's, so that is what you must align against. > **Never assume the token boundaries are your model's** > > If you send Claude's or GPT's output to `-analyze` and then zip the returned > `scores` against **your** provider's token list, the two will silently > disagree: same text, different splits, and no error anywhere. `1889` above > is one token to most tokenizers and four here. Always align on the returned > `tokens` array, never on a token list from somewhere else. > **model_not_found on an -analyze id** > > A 404 like: > ```json {"error": {"message": "The model 'anthropic/claude-sonnet-4-5-analyze' does not exist ...", "code": "model_not_found"}} ``` > > is almost never about the suffix. The base id has to be a model that actually > exists in the catalogue. The suffix is stripped and the remainder looked up, > so `anthropic/claude-sonnet-4-5-analyze` and bare `anthropic/claude-sonnet-4-5` > fail identically. Drop the suffix and retry to confirm, then check > `GET /v1/models` for the exact id. Version numbers are the usual culprit: > use the exact currently listed base ID instead of assuming an older spelling > is supported. ## Syncing scores with your own tokens `tokens` is the alignment key. It is guaranteed that: ``` "".join(tokens) == choices[0].message.content ``` That equality is what lets you map scores onto any other segmentation: your own tokenizer, words, sentences, or UI spans, without re-tokenising anything. Walk the tokens, keep a running character offset, and you have a character range for every score: ```python def spans(tokens, scores): """Yield (start, end, score) character ranges over the original text.""" pos = 0 for token, score in zip(tokens, scores): yield pos, pos + len(token), score pos += len(token) ``` From character ranges, everything else is a regrouping. To fold the probe's tokens up into your own, the usual case when you generated the text elsewhere and want scores against that provider's tokens, assign each of your tokens the scores whose ranges overlap it: ```python def align(their_tokens, probe_tokens, probe_scores, reduce=max): """Re-express probe scores over a different tokenization of the same text.""" probe_spans = list(spans(probe_tokens, probe_scores)) out, pos = [], 0 for token in their_tokens: start, end = pos, pos + len(token) overlapping = [ s for (a, b, s) in probe_spans if a < end and b > start and s is not None ] out.append(reduce(overlapping) if overlapping else None) pos = end return out ``` > **Pick the reduction deliberately** > > Where several probe tokens fall inside one of yours, `max` is the right > default for flagging: it is the "was any part of this suspect?" question, and > it keeps a single high-scoring fragment (`9` in a hallucinated year) from > being averaged away by the neutral fragments around it. Use `mean` only when > you are ranking spans rather than flagging them. The same overlap logic covers sentence and paragraph alignment. Replace `their_tokens` with the spans of your own segmentation. Because the join equality above holds by construction, this needs no tokenizer on your side at all. > **Whitespace lives in the tokens** > > Token text includes its leading whitespace (`" Eiffel"`, and a bare `" "` > before `1889`). The offsets are therefore exact against the original string, > but if you `strip()` tokens before measuring lengths, every offset after the > first space will be wrong. Measure first, strip only for display. ## Scores can be null Exactly as on the generated path, an individual entry in a score array can be `null`. The array is always the same length as `tokens`, so index `i` always describes `tokens[i]`, but a particular token's probe reading may be unavailable. A `null` means "no score for this token", not a score of zero. The `align` helper above drops them before reducing, and returns `None` for one of your tokens only when every overlapping probe token was null. ## You don't need to generate through us The common reason to reach for analyze is wanting hallucination scores on output your own stack produced, such as a coding agent's diffs and reasoning or a pipeline you already run against your own provider account, without making this API the gateway that generation flows through. That is what analyze is for. There is no "bring your own frontier-model token" option, and you do not need one: **you never send us a provider key, because we never call your provider.** You generate wherever you already do, then send the finished text here to be scored. Two independent calls, two separate bills, and no credential of yours ever leaves your own stack. ### curl Step 1. Generate against **your** provider, with **your** key. Telluvian is not involved and never sees `$OPENAI_API_KEY`: ```bash ANSWER=$(curl -s https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.6-terra", "messages": [{"role": "user", "content": "Explain the fix in this diff."}] }' | jq -r '.choices[0].message.content') ``` Step 2. Send that finished text to Telluvian for scoring. The only key here is your Telluvian key: ```bash # The model id names what GENERATED the text: the same model as step 1, # with -analyze appended. Not the probe model that does the scoring. jq -n --arg prompt "Explain the fix in this diff." --arg answer "$ANSWER" '{ model: "openai/gpt-5.6-terra-analyze", include_scores: true, messages: [ {role: "user", content: $prompt}, {role: "assistant", content: $answer} ] }' | curl -s https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d @- | jq '[.tokens, .scores.hallucination] | transpose | map(select(.[1] != null and .[1] > 0.5))' ``` That last `jq` prints just the flagged tokens and their scores: ```json [["indexed", 0.626]] ``` > **Build the JSON with jq, not string interpolation** > > `jq -n --arg` is doing real work here: model output routinely contains quotes, > backslashes and newlines, and for coding agents that is near-guaranteed, all of > which break a hand-interpolated `-d "{\"content\": \"$ANSWER\"}"`. Passing > the text through `--arg` escapes it correctly, and `-d @-` avoids a > too-long-argument error on a large diff. ### Python ```python # 1. Generate wherever you already generate. Your account, your key, # your provider. This API is not involved and never sees the key. completion = your_existing_client.chat.completions.create(...) answer = completion.choices[0].message.content # 2. Score the finished text. The model id names whatever generated it # in step 1, with -analyze appended. scored = telluvian.chat.completions.create( model="openai/gpt-5.6-terra-analyze", extra_body={"include_scores": True}, messages=[ {"role": "user", "content": original_prompt}, {"role": "assistant", "content": answer}, ], ) for token, score in zip(scored.tokens, scored.scores["hallucination"]): if score is not None and score > 0.5: print(f"flagged: {token!r}") ``` You are billed for the analyze request only: the scored text as completion tokens, plus any metered prompt tokens and the verification fee. Whatever the original generation cost, you paid your own provider for it directly, and it does not appear on your Telluvian bill. > **Name the model that wrote the text** > > The base model in an analyze request identifies **what generated the text you > are sending**. Scoring GPT-5.6 output means `openai/gpt-5.6-terra-analyze`; > scoring Claude output means `anthropic/claude-sonnet-5-analyze`. Only use > `google/gemma-4-31b-it-analyze` when Gemma actually wrote it. > > Nothing is sent to that provider. Analyze never calls out, it only prefills > your text locally. But the id is how the request records which model's output > was scored, so naming a different one both misattributes the result and bills > you against the wrong rate card. > **Today the scoring pass is identical whatever you name** > > Being straight about the current implementation: every analyze request is > prefilled through the same local Gemma container. The same text scored under > `openai/gpt-5.6-terra-analyze`, `anthropic/claude-sonnet-5-analyze` and > `google/gemma-4-31b-it-analyze` returns **the same numbers**. Naming the true > generating model is attribution and billing today, not fidelity. > > No per-provider adapter behavior is promised by this API. Validate scores > on your own workload rather than assuming that naming a provider changes > the scoring model. ### Scoring agent output For coding-agent output specifically, the useful signal is where the score moves *within* an explanation, not the average over it. Given a bad diff justified by a false claim, the probe localises the claim: ``` 0.001 'I' 0.001 ' changed' 0.180 'range' 0.229 '(' ... 0.023 ' safe' 0.014 ' because' 0.057 ' Python' 0.123 ' lists' 0.110 '1' 0.626 'indexed' <-- "Python lists are 1-indexed" 0.023 ' so' ``` These numbers are illustrative, not guaranteed outputs. Compare corrected and incorrect statements on your own workload before choosing a threshold. > **Code fragments score higher than prose at rest** > > Note the baseline in that example: `range`, `(`, `n`, `+`, `1` all sit around > 0.1 to 0.25 with nothing wrong at all. Identifier and punctuation tokens simply > carry more probe noise than ordinary prose, so a single global threshold > tuned on English will over-flag every diff you send it. > > Calibrate on your own traffic before trusting a cutoff: score a batch of > known-good agent messages, look at where their peaks actually land, and set > the threshold above that. Comparing a message against that baseline is far > more reliable than comparing it against an absolute number. > **Scope: claims, not compilation** > > The probe reads the model's internal state for signs the text is unsupported. > That catches confident false *statements*, such as wrong invariants, > invented API behaviour and misremembered semantics, which is a large share > of what makes a bad diff convincing. It is not a substitute for > type-checking or tests, and it will not tell you a diff fails to compile. > Treat a spike as "this claim deserves a second look", and keep the tests. ## Streaming Analyze mode streams. Chunks arrive in the same shape as [a streamed completion](https://telluvian.ai/docs/scores.md#streaming): one token per chunk, its score on `chunk.scores`. The deltas replay the text you sent rather than producing anything new, so streaming here buys you progressive scores on a long document, not faster time-to-first-word. ## Billing Analyze is billed as an ordinary request against the **base** model. The registry strips `-analyze` before pricing, so `google/gemma-4-31b-it-analyze` bills at `google/gemma-4-31b-it`'s rate. The scored text counts as completion tokens (it is what the probe did work on), and the verification fee applies on those, as set out on the [pricing page](https://telluvian.ai/docs/pricing.md). > **Analyze requires explicit opt-in** > > Omitted or false `include_scores` is rejected with `400` before the scoring > request reaches the GPU. Set `include_scores: true` to request analysis. > --- # 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. --- # curl recipes > Copy-paste requests for every shape of call, including streaming. Source: https://telluvian.ai/docs/curl Every example assumes your key is in the environment: ```bash export TELLUVIAN_API_KEY="sk_live_..." ``` ## Basic request ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [ { "role": "user", "content": "When was the Eiffel Tower built?" } ], "include_scores": true }' ``` This example explicitly enables verification. The response adds two extension fields: ```json { "id": "chatcmpl-...", "object": "chat.completion", "model": "google/gemma-4-31b-it", "choices": [{ "index": 0, "message": {"role": "assistant", "content": "The Eiffel Tower opened in 1889."}, "finish_reason": "stop" }], "usage": {"prompt_tokens": 9, "completion_tokens": 7, "total_tokens": 16}, "tokens": ["The", " Eiffel", " Tower", " opened", " in", " 1889", "."], "scores": {"hallucination": [0.01, 0.02, 0.02, 0.05, 0.03, 0.61, 0.01]} } ``` `tokens[i]` lines up with `scores.hallucination[i]`, and joining `tokens` gives back `content` exactly. ## Streaming Set `"stream": true`. Use `curl -N` to disable buffering, or you will see the whole response arrive at once and streaming will look broken. ```bash curl -N https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [ { "role": "user", "content": "Tell me about Ada Lovelace." } ], "stream": true, "include_scores": true }' ``` Server-sent events, one JSON object per `data:` line: ``` data: {"choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} data: {"choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}],"scores":{"hallucination":0.01}} data: {"choices":[{"index":0,"delta":{"content":" Eiffel"},"finish_reason":null}],"scores":{"hallucination":0.02}} data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":9,"completion_tokens":7,"total_tokens":16}} data: [DONE] ``` Three chunk shapes, and code that assumes every chunk has content will crash on two of them: 1. **The first chunk** carries `delta.role` and no content. 2. **Content chunks** carry `delta.content` and one `scores` value for that token when available. 3. **The final chunk** carries `finish_reason`, `usage`, and no content. The stream ends with the literal line `data: [DONE]`, which is not JSON. Parse for it before attempting `JSON.parse`. > **Usage is always on the last chunk** > > You do not need `stream_options`. Token counts arrive on the final chunk of > every stream, and they are what your request is billed from. ### Streaming, scores only Pull just the per-token scores out of a stream with `jq`. This reads the probe name out of each chunk rather than hardcoding it, so it works whichever probe the model is serving: ```bash curl -sN https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [ { "role": "user", "content": "Tell me about Ada Lovelace." } ], "stream": true, "include_scores": true }' \ | grep '^data: {' \ | sed 's/^data: //' \ | jq -r '(.scores // {}) as $s | ($s | to_entries | map(.value | tostring) | join("\t")) as $vals | select($vals != "") | "\($vals)\t\(.choices[0].delta.content // "")"' ``` ``` 0.01 The 0.02 Eiffel 0.02 Tower 0.61 1889 ``` > **No output means no scores, not a broken command** > > Chunks without a score omit `scores` entirely, so a filter that selects on it > prints **nothing at all** rather than reporting an error, which looks exactly > like a mistyped pipeline. If you get no score column, drop the `jq` and look > at the raw `data:` lines to see whether the scores are there. Scores are keyed by probe name. `hallucination` is the usual one, but a model can expose a different probe or several at once, so it is worth checking what you are getting rather than assuming: ```bash # Which probes does this model report? curl -sN https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [ { "role": "user", "content": "hi" } ], "stream": true, "include_scores": true }' \ | grep '^data: {' | sed 's/^data: //' \ | jq -r 'select(.scores) | .scores | keys | join(", ")' | head -1 ``` ### Why am I not getting scores? If no chunk has a `scores` field, check these in order: 1. **`include_scores: true` was not sent.** Omitting the field or setting it to `false` disables probing. Check client wrappers and proxies too. 2. **The probe name is different from the one you are filtering on.** Run the `keys` command above; if it prints a name, the scores are arriving under that key. 3. **You are calling a model you did not mean to.** Confirm the exact `model` value your request sent; `GET /v1/models` lists every available model. If the `keys` command prints nothing for a model listed as supporting probes, that is on our side rather than yours: [get in touch](mailto:hello@telluvian.ai) with the model name and roughly when you called it. > **Verification must be explicitly enabled** > > Omitted `include_scores` defaults to `false`. Add `"include_scores": true` > when you want scores, and allow enough output tokens for a reasoning model > to produce visible text. Reasoning-only responses have no visible tokens to score. A single `null` score is different, and expected: occasionally one token's reading is unavailable, most often the last token of a response cut short by `max_tokens`. Alignment is preserved and only that token is affected. See [Scores](https://telluvian.ai/docs/scores.md). ## Without verification ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}], "include_scores": false }' ``` `scores` and `tokens` come back `null`, and the verification fee is not charged. See [`include_scores`](https://telluvian.ai/docs/include-scores.md). ## Multi-turn conversation Send the whole history; the API is stateless. ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [ {"role": "system", "content": "You are a concise assistant."}, {"role": "user", "content": "When was the Eiffel Tower built?"}, {"role": "assistant", "content": "It opened in 1889."}, {"role": "user", "content": "Who designed it?"} ] }' ``` ## Sampling controls The usual OpenAI parameters are supported: `max_tokens`, `temperature`, `top_p`, `stop`, `seed`, `presence_penalty`, `frequency_penalty`. ```bash curl https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "List three facts about Mars."}], "max_tokens": 200, "temperature": 0.2, "stop": ["\n\n"] }' ``` ## Listing models ```bash curl https://api.telluvian.ai/v1/models \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" ``` Returns each model with its current price. This endpoint requires a key, like OpenAI's. ```bash # Just the ids and prices curl -s https://api.telluvian.ai/v1/models \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ | jq -r '.data[] | "\(.id)\t\(.pricing.prompt)\t\(.pricing.completion)"' ``` ## Checking your rate limit Rate-limit state is in the response headers: ```bash curl -i https://api.telluvian.ai/v1/chat/completions \ -H "Authorization: Bearer $TELLUVIAN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"google/gemma-4-31b-it","messages":[{"role":"user","content":"hi"}]}' \ | grep -i 'x-ratelimit\|retry-after' ``` See [Errors and rate limits](https://telluvian.ai/docs/errors.md). --- # SDKs > Using the official OpenAI Python and TypeScript SDKs against Telluvian. Source: https://telluvian.ai/docs/sdks The API implements both the OpenAI **Chat Completions** and **Responses** interfaces, so the **official OpenAI SDKs work unmodified**. Point `base_url` at Telluvian and use your Telluvian key. There is no Telluvian SDK to install and nothing to wrap. This page covers `client.chat.completions`. For `client.responses` and server-side conversation state, see [Responses API](https://telluvian.ai/docs/responses.md). Probe fields are extensions to the standard SDK types. The examples below explicitly enable probing wherever they read scores. ## Install ```bash pip install openai ``` ```bash npm install openai ``` ## Client setup ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.telluvian.ai/v1", api_key=os.environ["TELLUVIAN_API_KEY"], ) ``` ```typescript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://api.telluvian.ai/v1', apiKey: process.env.TELLUVIAN_API_KEY, }); ``` > **Note the /v1** > > The base URL includes `/v1`. The SDKs append paths like `/chat/completions` > to whatever you give them, so omitting it produces a 404 that looks like a > routing bug. ## Reading scores off a response `scores` and `tokens` are extensions to the OpenAI schema. The SDKs keep unrecognised response fields, so they are readable as ordinary attributes. ```python response = client.chat.completions.create( model="google/gemma-4-31b-it", extra_body={"include_scores": True}, messages=[{"role": "user", "content": "When was the Eiffel Tower built?"}], ) print(response.choices[0].message.content) # The Eiffel Tower opened in 1889. # Extension fields. The SDK keeps them in `model_extra`, and attribute # access works. Your type checker will not know about them. print(response.tokens) # ['The', ' Eiffel', ' Tower', ' opened', ' in', ' 1889', '.'] print(response.scores) # {'hallucination': [0.01, 0.02, 0.02, 0.05, 0.03, 0.61, 0.01]} ``` ```typescript const response = await client.chat.completions.create({ model: 'google/gemma-4-31b-it', messages: [{ role: 'user', content: 'When was the Eiffel Tower built?' }], include_scores: true, } as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean }); console.log(response.choices[0].message.content); // The Eiffel Tower opened in 1889. // Extension fields. Cast, because they are not in the OpenAI types. const { tokens, scores } = response as typeof response & { tokens: string[]; scores: Record; }; console.log(tokens); console.log(scores.hallucination); ``` ## Finding the flagged span `tokens[i]` and `scores["hallucination"][i]` are aligned and the same length, which is what makes the scores usable. You can point at the exact claim rather than at the whole answer. ```python THRESHOLD = 0.5 # pick your own; see Reading the scores flagged = [ tok for tok, score in zip(response.tokens, response.scores["hallucination"]) if score is not None and score > THRESHOLD ] print("".join(flagged)) # ' 1889' ``` ```typescript const THRESHOLD = 0.5; const flagged = tokens.filter((_, i) => scores.hallucination[i] != null && scores.hallucination[i]! > THRESHOLD); console.log(flagged.join('')); // ' 1889' ``` > **Reassembling the text** > > `"".join(tokens)` equals `choices[0].message.content` exactly. You never need > to re-tokenise to line the scores up with the text. ## Streaming With probing enabled, content chunks carry available scores for that token. A missing reading, role chunk, or finish chunk may have no score. ```python stream = client.chat.completions.create( model="google/gemma-4-31b-it", extra_body={"include_scores": True}, messages=[{"role": "user", "content": "Tell me about Ada Lovelace."}], stream=True, ) usage = None for chunk in stream: # The final chunk carries usage and no content; role-only chunks carry # neither. Guard both rather than assuming every chunk has a delta. if chunk.usage is not None: usage = chunk.usage delta = chunk.choices[0].delta.content if not delta: continue scores = getattr(chunk, "scores", None) score = scores.get("hallucination") if scores else None print(delta, score) print(usage) # CompletionUsage(prompt_tokens=9, completion_tokens=7, ...) ``` ```typescript const stream = await client.chat.completions.create({ model: 'google/gemma-4-31b-it', messages: [{ role: 'user', content: 'Tell me about Ada Lovelace.' }], stream: true, include_scores: true, } as OpenAI.ChatCompletionCreateParamsStreaming & { include_scores: boolean }); let usage = null; for await (const chunk of stream) { if (chunk.usage) usage = chunk.usage; const delta = chunk.choices[0]?.delta?.content; if (!delta) continue; const { scores } = chunk as typeof chunk & { scores?: Record; }; console.log(delta, scores?.hallucination); } console.log(usage); ``` > **Usage arrives without asking** > > Token counts are attached to the final chunk of every stream. You do not need > `stream_options: {include_usage: true}`. It is always there, and it is what > your request is billed from. ## Turning verification off Probing is off by default. You can also disable it explicitly; Python passes the extension through `extra_body`, while TypeScript sends it inline. ```python # Python's SDK validates arguments against the OpenAI schema, so a probe # extension must go through extra_body or it is rejected before sending. response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello"}], extra_body={"include_scores": False}, ) print(response.scores) # None ``` ```typescript // The JS SDK passes unknown top-level keys straight through, so the field // goes inline. The cast is only to satisfy the compiler. const response = await client.chat.completions.create({ model: 'google/gemma-4-31b-it', messages: [{ role: 'user', content: 'Hello' }], include_scores: false, } as OpenAI.ChatCompletionCreateParamsNonStreaming & { include_scores: boolean }); ``` See [`include_scores`](https://telluvian.ai/docs/include-scores.md) for what this changes, and [Pricing](https://telluvian.ai/docs/pricing.md) for what it saves. ## Listing models ```python for model in client.models.list().data: extra = model.model_extra or {} print(model.id, extra.get("pricing"), extra.get("probe")) ``` ```typescript const { data } = await client.models.list(); for (const model of data) { console.log(model.id, (model as never as { pricing: unknown }).pricing); } ``` ## Other OpenAI-compatible tooling Anything that speaks Chat Completions and lets you set a base URL should work, LangChain, LlamaIndex, Vercel AI SDK, and so on. The caveat is always the same: tools that re-serialise responses into their own types will **drop `scores` and `tokens`**, because those are not part of the OpenAI schema. If a framework is losing your scores, call the endpoint directly for the scored path. --- # Responses API > The stateful /v1/responses endpoint, with server-side conversation history. Source: https://telluvian.ai/docs/responses Telluvian implements OpenAI's **Responses API** as well as Chat Completions. The official SDKs work against it unchanged: ```python from openai import OpenAI client = OpenAI( api_key="sk_live_...", base_url="https://api.telluvian.ai/v1", ) response = client.responses.create( model="google/gemma-4-31b-it", input="Who was the first person on Mars?", ) print(response.output_text) ``` ## What makes it different Chat Completions is **stateless**: you re-send the whole conversation on every call. The Responses API is **stateful**. You send only the new input plus the id of the previous response, and we rebuild the history server-side. ```python first = client.responses.create( model="google/gemma-4-31b-it", input="My favourite colour is crimson.", ) second = client.responses.create( model="google/gemma-4-31b-it", previous_response_id=first.id, input="What is my favourite colour?", # the colour is not re-sent ) print(second.output_text) # "Crimson." ``` > **Which should you use?** > > Use **Chat Completions** if you already keep the conversation in your own > application state. It is one fewer thing for us to hold, and it is what most > existing code and frameworks expect. > > Use **Responses** if you would otherwise be storing the transcript purely to > replay it to us, or if you are following OpenAI examples written against it. ## Probe scores Scoring is off by default. Set `include_scores: true` to enable it. The per-token output rides on the content part, next to the text it describes: ```python response = client.responses.create( model="google/gemma-4-31b-it", extra_body={"include_scores": True}, input="Who was the first person on Mars?", ) part = response.output[0].content[0] for token, score in zip(part.tokens, part.scores["hallucination"]): if score is not None and score > 0.5: print(f"flagged: {token!r}") ``` The same `null`-entry rule applies as in [Reading the scores](https://telluvian.ai/docs/scores.md): the array is always as long as `tokens`, but an individual entry may be `null`. Omit `include_scores`, or pass `include_scores: false` through `extra_body`, for an unscored request without the verification fee. ## Streaming The stream is a sequence of **typed events**, not the uniform chunks of Chat Completions: ```python stream = client.responses.create( model="google/gemma-4-31b-it", input="Tell me about Ada Lovelace.", stream=True, ) for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) elif event.type == "response.completed": print(f"\n{event.response.usage.output_tokens} tokens") ``` Events emitted, in order: | Event | Carries | | --- | --- | | `response.created` | The response object, `status: "in_progress"` | | `response.in_progress` | Same | | `response.output_text.delta` | `delta` (text), and available `scores` when probing is enabled | | `response.output_text.done` | The complete `text` | | `response.completed` | The final response, with `usage` | ## Managing stored responses Responses are stored by default so they can be continued. ```python fetched = client.responses.retrieve(response.id) # GET client.responses.delete(response.id) # DELETE ``` Pass `store=False` to make the response unavailable for retrieval or continuation with `previous_response_id`. This is not a no-storage setting: request auditing, billing, and configured transcript logging still apply. ```python client.responses.create( model="google/gemma-4-31b-it", input="Nothing to keep.", store=False, ) ``` ## Field mapping If you are porting from Chat Completions: | Chat Completions | Responses | | --- | --- | | `messages` | `input` (a string, or a list of messages) | | a leading `system` message | `instructions` | | `max_tokens` | `max_output_tokens` | | `choices[0].message.content` | `output_text` | | `usage.prompt_tokens` | `usage.input_tokens` | | `usage.completion_tokens` | `usage.output_tokens` | | `finish_reason: "length"` | `status: "incomplete"` | > **instructions apply to one call only** > > `instructions` is **not** inherited by a response that continues this one. If > a later call in the chain needs the same instruction, pass it again. This > matches OpenAI's behaviour, and it means a system prompt can never become > silently sticky across a conversation. ## Pricing Identical to Chat Completions: the same per-token rates, the same verification fee when explicitly enabled. Routing adds $0.05 per 1M input tokens when `telluvian/gallery-1` picks the model. See [Pricing](https://telluvian.ai/docs/pricing.md). Continuing a chain re-sends the stored history to the model, so **input tokens grow with the conversation**, exactly as they would if you had re-sent it yourself. --- # 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. --- # All models > Every model the API serves, with its live per-token rates. Source: https://telluvian.ai/docs/models Every model available through the API, with the rates your requests are billed against. The table is fetched from the gateway and cached by the website; `GET /v1/models` is the current catalogue when checking a recent change. For the models most people start with, and how the two fees we set work, see [Pricing](https://telluvian.ai/docs/pricing.md). > **Catalogue pricing** > > Generation is billed at the gateway's published model rates, including > applicable cache and long-context rates. Verification is opt-in at $1.00 > per 1M completion tokens. Routing adds $0.05 per 1M input tokens when we > pick the model. See [Pricing](https://telluvian.ai/docs/pricing.md). `GET /v1/models` also lists `telluvian/gallery-1` first. It is not a model but a request to choose one, so its `pricing` and `context_length` are `null`: both belong to whichever model gets picked. Having no rate of its own, it is absent from the table below. See [Gallery](https://telluvian.ai/docs/gallery.md). Model lookup accepts unambiguous case variants, such as `Google/gemma-4-31B-it`; the canonical catalogue ID is `google/gemma-4-31b-it`. They share one model entry and one rate card. ## Rates ### openai | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `openai/gpt-6-luna-pro` | $0.10 | $0.50 | | `openai/gpt-6-luna` | $0.10 | $0.50 | | `openai/gpt-6-sol-pro` | $2.00 | $10.00 | | `openai/gpt-6-sol` | $2.00 | $10.00 | | `openai/gpt-6-astra` | $10.00 | $50.00 | | `openai/gpt-6-astra-pro` | $10.00 | $50.00 | | `openai/gpt-5.6-luna-pro` | $0.20 | $1.20 | | `openai/gpt-5.6-luna` | $0.20 | $1.20 | | `openai/gpt-5.6-terra-pro` | $2.00 | $12.00 | | `openai/gpt-5.6-terra` | $2.00 | $12.00 | | `openai/gpt-5.6-sol-pro` | $2.00 | $10.00 | | `openai/gpt-5.6-sol` | $2.00 | $10.00 | | `openai/gpt-chat-latest` | $5.00 | $30.00 | | `openai/gpt-5.5-pro` | $30.00 | $180.00 | | `openai/gpt-5.5` | $5.00 | $30.00 | | `openai/gpt-5.4-nano` | $0.20 | $1.25 | | `openai/gpt-5.4-mini` | $0.75 | $4.50 | | `openai/gpt-5.4-pro` | $30.00 | $180.00 | | `openai/gpt-5.4` | $2.50 | $15.00 | | `openai/gpt-5.3-codex` | $1.75 | $14.00 | | `openai/gpt-5.2-codex` | $1.75 | $14.00 | | `openai/gpt-5.2-chat` | $1.75 | $14.00 | | `openai/gpt-5.2-pro` | $21.00 | $168.00 | | `openai/gpt-5.2` | $1.75 | $14.00 | | `openai/gpt-5.1-codex-max` | $1.25 | $10.00 | | `openai/gpt-5.1` | $1.25 | $10.00 | | `openai/gpt-5.1-codex` | $1.25 | $10.00 | | `openai/gpt-5.1-codex-mini` | $0.25 | $2.00 | | `openai/gpt-5-pro` | $15.00 | $120.00 | | `openai/gpt-5` | $1.25 | $10.00 | | `openai/gpt-5-mini` | $0.25 | $2.00 | | `openai/gpt-5-nano` | $0.05 | $0.40 | | `openai/o3-pro` | $20.00 | $80.00 | | `openai/o4-mini-high` | $1.10 | $4.40 | | `openai/o3` | $2.00 | $8.00 | | `openai/o4-mini` | $1.10 | $4.40 | | `openai/gpt-4.1` | $2.00 | $8.00 | | `openai/gpt-4.1-mini` | $0.40 | $1.60 | | `openai/gpt-4.1-nano` | $0.10 | $0.40 | | `openai/o1-pro` | $150.00 | $600.00 | | `openai/o3-mini-high` | $1.10 | $4.40 | | `openai/o3-mini` | $1.10 | $4.40 | | `openai/o1` | $15.00 | $60.00 | | `openai/gpt-4o-2024-11-20` | $2.50 | $10.00 | | `openai/gpt-4o-2024-08-06` | $2.50 | $10.00 | | `openai/gpt-4o-mini` | $0.15 | $0.60 | | `openai/gpt-4o-mini-2024-07-18` | $0.15 | $0.60 | | `openai/gpt-4o` | $2.50 | $10.00 | | `openai/gpt-4o-2024-05-13` | $5.00 | $15.00 | | `openai/gpt-4-turbo` | $10.00 | $30.00 | | `openai/gpt-3.5-turbo-0613` | $1.00 | $2.00 | | `openai/gpt-3.5-turbo-instruct` | $1.50 | $2.00 | | `openai/gpt-3.5-turbo-16k` | $3.00 | $4.00 | | `openai/gpt-3.5-turbo` | $0.50 | $1.50 | | `openai/gpt-4` | $30.00 | $60.00 | ### qwen | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `qwen/qwen3.8-max-prime` | $4.00 | $12.00 | | `qwen/qwen3.8-omni-flash` | $0.15 | $0.47 | | `qwen/qwen3.8-max-0902` | $2.00 | $6.00 | | `qwen/qwen3.8-flash` | $0.15 | $0.47 | | `qwen/qwen3.8-27b` | $0.42 | $3.00 | | `qwen/qwen3.8-2.4t-a95b` | $2.00 | $6.00 | | `qwen/qwen3.7-flash` | $0.03 | $0.13 | | `qwen/qwen3.7-plus` | $0.32 | $1.28 | | `qwen/qwen3.7-max` | $1.48 | $4.42 | | `qwen/qwen3.5-plus-20260420` | $0.30 | $1.80 | | `qwen/qwen3.6-flash` | $0.19 | $1.13 | | `qwen/qwen3.6-35b-a3b` | $0.15 | $1.00 | | `qwen/qwen3.6-max-preview` | $1.03 | $6.16 | | `qwen/qwen3.6-27b` | $0.32 | $2.70 | | `qwen/qwen3.6-plus` | $0.33 | $1.95 | | `qwen/qwen3.5-9b` | $0.10 | $0.15 | | `qwen/qwen3.5-35b-a3b` | $0.31 | $1.25 | | `qwen/qwen3.5-27b` | $0.20 | $1.56 | | `qwen/qwen3.5-122b-a10b` | $0.26 | $2.08 | | `qwen/qwen3.5-flash-02-23` | $0.07 | $0.26 | | `qwen/qwen3.5-plus-02-15` | $0.26 | $1.56 | | `qwen/qwen3.5-397b-a17b` | $0.55 | $3.50 | | `qwen/qwen3-max-thinking` | $0.78 | $3.90 | | `qwen/qwen3-coder-next` | $0.12 | $0.80 | | `qwen/qwen3-vl-32b-instruct` | $0.10 | $0.42 | | `qwen/qwen3-vl-8b-thinking` | $0.18 | $2.10 | | `qwen/qwen3-vl-8b-instruct` | $0.12 | $0.45 | | `qwen/qwen3-vl-30b-a3b-thinking` | $0.20 | $2.40 | | `qwen/qwen3-vl-30b-a3b-instruct` | $0.15 | $0.60 | | `qwen/qwen3-vl-235b-a22b-thinking` | $0.40 | $4.00 | | `qwen/qwen3-vl-235b-a22b-instruct` | $0.21 | $1.90 | | `qwen/qwen3-max` | $0.78 | $3.90 | | `qwen/qwen3-coder-plus` | $0.65 | $3.25 | | `qwen/qwen3-coder-flash` | $0.20 | $0.97 | | `qwen/qwen3-next-80b-a3b-thinking` | $0.15 | $1.20 | | `qwen/qwen3-next-80b-a3b-instruct` | $0.10 | $1.10 | | `qwen/qwen-plus-2025-07-28` | $0.26 | $0.78 | | `qwen/qwen3-30b-a3b-thinking-2507` | $0.20 | $2.40 | | `qwen/qwen3-coder-30b-a3b-instruct` | $0.07 | $0.28 | | `qwen/qwen3-30b-a3b-instruct-2507` | $0.10 | $0.30 | | `qwen/qwen3-235b-a22b-thinking-2507` | $0.23 | $2.30 | | `qwen/qwen3-coder` | $0.30 | $1.00 | | `qwen/qwen3-235b-a22b-2507` | $0.09 | $0.35 | | `qwen/qwen3-30b-a3b` | $0.12 | $0.50 | | `qwen/qwen3-8b` | $0.12 | $0.45 | | `qwen/qwen3-14b` | $0.12 | $0.24 | | `qwen/qwen3-32b` | $0.08 | $0.28 | | `qwen/qwen3-235b-a22b` | $0.45 | $1.82 | | `qwen/qwen2.5-vl-72b-instruct` | $0.80 | $1.00 | | `qwen/qwen-plus` | $0.26 | $0.78 | | `qwen/qwen-2.5-coder-32b-instruct` | $0.66 | $1.00 | | `qwen/qwen-2.5-7b-instruct` | $0.10 | $0.20 | | `qwen/qwen-2.5-72b-instruct` | $0.36 | $0.40 | ### google | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `google/gemini-3.8-flash` | $0.75 | $3.75 | | `google/gemini-3.7-flash` | $0.75 | $3.75 | | `google/gemini-3.6-flash` | $0.75 | $3.75 | | `google/gemini-3.5-flash-lite` | $0.30 | $2.50 | | `google/gemini-3.5-flash` | $1.50 | $9.00 | | `google/gemini-3.1-flash-lite` | $0.25 | $1.50 | | `google/gemma-4-26b-a4b-it` | $0.07 | $0.22 | | `google/gemma-4-31b-it` | $0.09 | $0.34 | | `google/gemini-3.1-flash-lite-preview` | $0.25 | $1.50 | | `google/gemini-3.1-pro-preview-customtools` | $2.00 | $12.00 | | `google/gemini-3.1-pro-preview` | $2.00 | $12.00 | | `google/gemini-3-flash-preview` | $0.50 | $3.00 | | `google/gemini-2.5-flash-lite` | $0.10 | $0.40 | | `google/gemini-2.5-flash` | $0.30 | $2.50 | | `google/gemini-2.5-pro` | $1.25 | $10.00 | | `google/gemini-2.5-pro-preview` | $1.25 | $10.00 | | `google/gemma-3-4b-it` | $0.05 | $0.10 | | `google/gemma-3-12b-it` | $0.05 | $0.15 | | `google/gemma-3-27b-it` | $0.08 | $0.45 | | `google/gemma-2-27b-it` | $0.65 | $0.65 | ### z-ai | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `z-ai/glm-5.3-prime` | $2.80 | $8.80 | | `z-ai/glm-5.3-flashx` | $0.37 | $1.25 | | `z-ai/glm-5.3-flash` | $0.04 | $0.14 | | `z-ai/glm-5.3` | $1.40 | $4.40 | | `z-ai/glm-5.2` | $0.65 | $2.04 | | `z-ai/glm-5.1` | $0.96 | $3.03 | | `z-ai/glm-5v-turbo` | $1.20 | $4.00 | | `z-ai/glm-5-turbo` | $1.20 | $4.00 | | `z-ai/glm-5` | $0.60 | $1.92 | | `z-ai/glm-4.7-flash` | $0.06 | $0.40 | | `z-ai/glm-4.7` | $0.60 | $2.20 | | `z-ai/glm-4.6v` | $0.30 | $0.90 | | `z-ai/glm-4.6` | $0.43 | $1.75 | | `z-ai/glm-4.5v` | $0.60 | $1.80 | | `z-ai/glm-4.5` | $0.60 | $2.20 | | `z-ai/glm-4.5-air` | $0.13 | $0.85 | ### deepseek | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `deepseek/deepseek-v4.1-flash` | $0.15 | $0.60 | | `deepseek/deepseek-v4-flash-vision-exp` | $0.22 | $0.66 | | `deepseek/deepseek-v4-pro-0813` | $0.35 | $1.04 | | `deepseek/deepseek-v4-flash-0731` | $0.03 | $0.32 | | `deepseek/deepseek-v4-pro` | $0.72 | $1.45 | | `deepseek/deepseek-v4-flash` | $0.05 | $0.10 | | `deepseek/deepseek-v3.2` | $0.27 | $0.40 | | `deepseek/deepseek-v3.2-exp` | $0.27 | $0.41 | | `deepseek/deepseek-v3.1-terminus` | $0.27 | $1.00 | | `deepseek/deepseek-chat-v3.1` | $0.25 | $0.95 | | `deepseek/deepseek-r1-0528` | $0.50 | $2.15 | | `deepseek/deepseek-chat-v3-0324` | $0.25 | $1.00 | | `deepseek/deepseek-r1-distill-llama-70b` | $0.80 | $0.80 | | `deepseek/deepseek-r1` | $0.70 | $2.50 | | `deepseek/deepseek-chat` | $0.32 | $0.89 | ### anthropic | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `anthropic/claude-opus-5-5` | $4.00 | $20.00 | | `anthropic/claude-fable-5-1` | $10.00 | $50.00 | | `anthropic/claude-opus-5` | $5.00 | $25.00 | | `anthropic/claude-sonnet-5` | $2.00 | $10.00 | | `anthropic/claude-fable-5` | $10.00 | $50.00 | | `anthropic/claude-opus-4-8` | $5.00 | $25.00 | | `anthropic/claude-opus-4-7` | $5.00 | $25.00 | | `anthropic/claude-sonnet-4-6` | $3.00 | $15.00 | | `anthropic/claude-opus-4-6` | $5.00 | $25.00 | | `anthropic/claude-opus-4-5-20251101` | $5.00 | $25.00 | | `anthropic/claude-haiku-4-5-20251001` | $1.00 | $5.00 | | `anthropic/claude-sonnet-4-5-20250929` | $3.00 | $15.00 | ### x-ai | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `x-ai/grok-4.7` | $1.60 | $4.80 | | `x-ai/grok-4.6` | $2.00 | $6.00 | | `x-ai/grok-4.5` | $2.00 | $6.00 | | `x-ai/grok-build-0.1` | $1.00 | $2.00 | | `x-ai/grok-4.3` | $1.25 | $2.50 | | `x-ai/grok-4.20-multi-agent` | $1.25 | $2.50 | | `x-ai/grok-4.20` | $1.25 | $2.50 | ### moonshotai | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `moonshotai/kimi-k3` | $3.00 | $15.00 | | `moonshotai/kimi-k2.7-code` | $0.66 | $3.30 | | `moonshotai/kimi-k2.6` | $0.95 | $4.00 | | `moonshotai/kimi-k2.5` | $0.45 | $2.25 | | `moonshotai/kimi-k2-thinking` | $0.60 | $2.50 | | `moonshotai/kimi-k2-0905` | $0.60 | $2.50 | | `moonshotai/kimi-k2` | $0.57 | $2.30 | ### meta | Model | Input / 1M | Output / 1M | | --- | ---: | ---: | | `meta/muse-spark-1.3-contributor` | $0.10 | $0.20 | | `meta/muse-spark-1.3` | $1.25 | $4.25 | | `meta/muse-spark-1.2-contributor` | $0.10 | $0.20 | | `meta/muse-glimmer-30b` | $0.30 | $1.20 | | `meta/muse-spark-1.2` | $1.25 | $4.25 | | `meta/muse-spark-1.1` | $1.25 | $4.25 | --- # 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).