Decisions
Typed answers from decision models.
/v1/decisionsAsk a decision model typed questions about a state (a message, a ticket, a record) and get one typed answer per question. A decision model does not write text. Each answer is a choice, a score, a yes/no, an order, a set of tags or extracted fields, with the probability or confidence behind it. Use it for routing, classification, moderation and other decision points in your code.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"typesafe/jev-1.13",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'No SDK has a decisions method. Send plain HTTP.
import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "typesafe/jev-1.13",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {"team": {"type": "choice", "instructions": "Which team?",
"criteria": {"billing": "Payments", "technical": "Bugs"}}},
},
)
answer = r.json()["answers"]["team"]
print(answer["choice"], answer["confidence"])const r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "typesafe/jev-1.13",
state: { ticket: "Help! My payouts have been failing for 3 days." },
questions: { urgent: { type: "noul", instructions: "Is `ticket` urgent?" } },
}),
});
const { answers, usage } = await r.json();
console.log(answers.urgent.noul, usage.cost);Models
The catalog has twelve decision models:
| Model | Catalog id | Made by | Providers |
|---|---|---|---|
| Jev 1.13 | typesafe/jev-1.13 | TypeSafe | typesafe |
| Bespoke Nimble v4 | bespokelabs/nimble-v4 | Bespoke Labs | bespokelabs |
| Bespoke Nimble v4 Max | bespokelabs/nimble-v4-max | Bespoke Labs | bespokelabs |
| Clef | cloudflare/clef | Cloudflare | workers-ai |
| Clef-flash | cloudflare/clef-flash | Cloudflare | workers-ai |
| Decider 2B | routerplus/decider-2b | RouterPlus | routerplus |
| Kev 4B | routerplus/kev-4b | RouterPlus | routerplus |
| Perplexity Decider v1 27B | perplexity/pplx-decider-v1-27b | Perplexity | perplexity-decisions |
| GPT-6 Luna Decisions | openai/gpt-6-luna-decisions | OpenAI | openai-decisions |
| Sage 1.2 | levanto/sage-1.2 | Levanto | levanto |
| ScaleDown Classify-1 | scaledown/classify-1 | ScaleDown | scaledown |
| GLiNER-2.5-Decide | fastino/gliner-2.5-decide | Fastino | fastino |
The gateway also accepts bespokelabs/nimble-latest, which no list shows. Bespoke picks the model that answers it. See Bespoke Nimble.
Every call has the same envelope: model and state in, answers and usage out. The questions use the model's own format:
- Eleven models share one format, System One (TypeSafe's): Jev, the two Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage and ScaleDown Classify-1. They take a
questionsmap. A body written for one of them runs on the others with onlymodelchanged, within each model's limits. ScaleDown Classify-1 bills each question as its own call. - GLiNER takes a
schemaobject:classifications,entities,structures,relations. - A question in another model's format is a 400.
- The twelve are different models, so the gateway never fails over from one to another.
The gateway calls these endpoints:
| Model | Order | Provider | Endpoint the gateway calls | Model id sent upstream |
|---|---|---|---|---|
| Jev | 1 | TypeSafe (direct API) | POST https://api.typesafe.ai/v1/systemone | jev-1.13.0 |
| Nimble v4, Nimble v4 Max | 1 | Bespoke Labs (direct API) | POST https://api.bespokelabs.ai/v1/nimble/systemone | nimble-v4, nimble-v4-max (and nimble-latest for bespokelabs/nimble-latest) |
| Clef | 1 | Cloudflare Workers AI (direct API) | POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/cloudflare/clef | clef |
| Clef-flash | 1 | Cloudflare Workers AI (direct API) | POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/cloudflare/clef-flash | clef-flash |
| Decider 2B | 1 | RouterPlus (our own service, direct API) | POST /v1/decisions on RouterPlus's endpoint | decider-2b |
| Kev 4B | 1 | RouterPlus (our own service, direct API) | POST /v1/decisions on RouterPlus's endpoint | kev-4b |
| Perplexity Decider | 1 | Perplexity (direct API) | POST https://api.perplexity.ai/v1/decisions | pplx-decider-v1-27b |
| GPT-6 Luna Decisions | 1 | OpenAI (direct API) | POST https://api.openai.com/v1/decisions | gpt-6-luna |
| ScaleDown Classify-1 | 1 | ScaleDown (direct API) | POST https://api.scaledown.xyz/v1/scaledown | classify-1 |
| Sage | 1 | Levanto (direct API) | POST https://sage.levanto.ai/v1/systemone | sage-latest |
| GLiNER | 1 | Fastino (direct API) | POST https://api.fastino.ai/v1/chat/completions | fastino/GLiNER-2.5-Decide |
Each decision model has one provider, so a request has no second provider to try. The Nimble models share one Bespoke deployment: one pool and one health circuit serve all of them.
- Decider 2B and Kev 4B share one RouterPlus deployment: one pool and one health circuit serve both.
- On Workers AI,
{account_id}is our Cloudflare account, and the path names the model. - OpenAI's API does not speak System One. The gateway translates your body into OpenAI's format, and OpenAI's answers back into Jev's shape (see GPT-6 Luna Decisions).
- The
x-tm-providerheader names the provider that answered.
To compare the models without code, use Decision in the playground. It translates one set of questions for each model.
Authentication
| Header | Format |
|---|---|
Authorization | Bearer tm_vk_... |
x-api-key | tm_vk_... (also accepted, same key) |
A missing or invalid key returns 401 with error_type: "auth".
Request
The body is JSON, at most 1 MB (413 request_too_large above that). The gateway checks every rule on this page before it reserves money or calls a provider. A refused request has no ledger row, no x-tm-attempts header and no charge.
| Parameter | Type | Behavior |
|---|---|---|
model | string, required | A decision model id. A dated id of a listed decision model routes as that model: typesafe/jev-1.13-20260917 as typesafe/jev-1.13. Providers' own names are not ids (see the next table). A chat or image model is a 400 that names its route. An unknown id is a 404. |
state | required | What the model evaluates. Its forms depend on the model: see System One questions and GLiNER schema. |
questions | object, the System One models, required | A map of question id to question. You choose the ids, and each answer comes back under its id. At least one question. On GLiNER, questions is a 400 that says the model takes a GLiNER schema. |
schema | object, GLiNER only, required | GLiNER's own schema. See GLiNER schema. On Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage or ScaleDown Classify-1, schema is a 400 that says the model takes questions. |
threshold, include_confidence, include_spans | GLiNER only | See GLiNER options. |
provider | object | Routing controls: only, order, ignore, require_parameters and the rest. The gateway reads them and never forwards them. See Routing policies. The provider ids are in the next table. |
Model names that are not ids, and provider ids:
| Model | Send as model | Not an id here | provider id |
|---|---|---|---|
| Jev | typesafe/jev-1.13 | typesafe | |
| Nimble | bespokelabs/nimble-v4, bespokelabs/nimble-v4-max | Bespoke's names nimble-v4, nimble-v4-max, nimble-latest. bespokelabs/nimble-latest is accepted but not listed: the gateway sends it to Bespoke as nimble-latest, and Bespoke picks the model (see Bespoke Nimble). | bespokelabs |
| Clef, Clef-flash | cloudflare/clef, cloudflare/clef-flash | clef, clef-flash, @cf/cloudflare/clef, @cf/cloudflare/clef-flash | workers-ai (not cloudflare, which matches no route) |
| Decider 2B, Kev 4B | routerplus/decider-2b, routerplus/kev-4b | RouterPlus's own short names (decider-2b, kev-4b) are not ids here | routerplus |
| Perplexity Decider | perplexity/pplx-decider-v1-27b | pplx-decider-v1-27b | perplexity-decisions (not perplexity, which matches no route) |
| GPT-6 Luna Decisions | openai/gpt-6-luna-decisions | OpenAI's own name gpt-6-luna is our id for OpenAI's chat model GPT-6 Luna, so here it is a 400 that names the chat routes. openai/gpt-6-luna and gpt-6-luna-decisions without openai/ are a 404. | openai-decisions for GPT-6 Luna Decisions (not openai, which is our OpenAI chat route) |
| Sage | levanto/sage-1.2 | sage-latest, levanto-sage-v1.3 | levanto |
| GLiNER | fastino/gliner-2.5-decide | fastino |
System One questions
Nine models take the same question format, TypeSafe's System One: Jev, the two Nimble models, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider and GPT-6 Luna Decisions. This section holds for each of the nine. Some models add limits of their own: see Bespoke Nimble, Clef and Clef-flash, Decider 2B and Kev 4B, Perplexity Decider v1 27B, GPT-6 Luna Decisions and Sage 1.2.
The examples use Jev. To run them on another System One model, change model.
State. A non-empty string, or any JSON object or array (a chat log, a record, application state). A question can point at a field by name: "Is `ticket.body` urgent?".
Questions. Every question has a type, instructions and, for most types, criteria. instructions is a non-empty string, or a JSON object or array: put the question in one field and the data it refers to in others.
type | criteria | The answer |
|---|---|---|
choice | Required. An object of option name to description, 1 to 255 options. A description can be a string, JSON, or null when the name says enough. | choice (the most likely option), probabilities (every option), confidence |
score | Required. An ordered array of level descriptions, low to high, 2 to 10 levels. | score (probability-weighted, can fall between levels), legend, probabilities, confidence |
noul | Optional. {"true": "...", "false": "..."}: what yes and no mean. | noul: the probability that the answer is yes, 0 to 1 |
- A question takes only
type,instructionsandcriteria. Any other field is a 400 that names it. - A question with
kind(the format of Levanto's own/decideAPI) is a 400 that says the model takes System One questions. - Ask many questions in one call. The model reads the state once and answers every question against it, so one call with ten questions costs less and returns sooner than ten calls.
- Exceptions: Perplexity Decider and Sage read and bill the state once per question. GPT-6 Luna Decisions bills the state once per call, but each question adds about 150 input tokens.
confidenceis not the chance that the answer is right. Some models compute it their own way (see each model's Numbers row). Tune a threshold on the model you use, or use the probabilities.
{
"model": "typesafe/jev-1.13",
"state": { "ticket": "Help! My payouts have been failing for 3 days." },
"questions": {
"team": {
"type": "choice",
"instructions": "Which team should handle `ticket`?",
"criteria": { "billing": "Payments, invoices, refunds", "technical": "Bugs, outages, integrations", "sales": null }
},
"frustration": {
"type": "score",
"instructions": "How frustrated is the customer?",
"criteria": ["Calm", "Annoyed", "Angry"]
},
"urgent": {
"type": "noul",
"instructions": "Is this urgent?",
"criteria": { "true": "Explicitly time-sensitive", "false": "No urgency expressed" }
}
}
}The sections below list the limits and behavior of each System One model. The gateway checks the rules above. It does not check each model's limits. The Playground's Decision mode does check them, before a run. If a body breaks a model's limits, the provider refuses it, and the gateway relays the refusal as an upstream_error with the provider's message, at no cost. Limits and failures covers the rules for every provider: pool limits, relayed 429s, the health circuit and refusals of our account.
Bespoke Nimble
The gateway calls Bespoke Labs' own API.
| Bespoke Nimble v4 and Nimble v4 Max | |
|---|---|
| Catalog ids | bespokelabs/nimble-v4 and bespokelabs/nimble-v4-max. Bespoke no longer serves Nimble v3: bespokelabs/nimble-v3 is a 404. |
| Models | Nimble v4 is the faster, lower-cost model. Nimble v4 Max is Bespoke's most accurate model. It is larger and slower, and it costs more. The two return different probabilities for the same request, so check your thresholds when you change models. |
bespokelabs/nimble-latest | Accepted, but no list shows it. Bespoke picks the model: Nimble v4 today, and a newer model when Bespoke releases one. It costs $0.04 per million input tokens, Bespoke's price for nimble-latest. The answer's model is bespokelabs/nimble-latest. To stay on one model, send that model's id. |
| Route | Bespoke Labs' API only, as nimble-v4, nimble-v4-max or nimble-latest. One deployment serves all of them. |
| Questions | 1 to 64 questions per call. A choice takes 2 to 255 options, so a one-option choice (which Jev takes) is refused. A score takes 2 to 10 levels through the gateway (Nimble itself takes up to 255). Bespoke refuses a body above these limits with a 422. |
| Structured content | instructions and a choice option's description may be a JSON object or array, as on Jev. Bespoke's published schema types them as strings, but a probe with both answered 200 (2026-10-13 on Nimble v4 and v4 Max). |
| Context | 262,144 tokens for each question's prompt on Nimble v4, Nimble v4 Max and bespokelabs/nimble-latest, the state included. Nimble refuses a longer prompt and never cuts one. Bespoke counts the state and the questions once per call. |
| Busy | Bespoke answers 503 with Retry-After while Nimble starts, 529 when it is busy (Bespoke says to retry after about one second) and 502 when the model fails. Each is a retryable upstream_error that counts against the health circuit, and one circuit covers all the Nimble models. Retry after a second or two, with backoff. The gateway does not relay Bespoke's Retry-After on a 503. |
| Numbers | Full precision. Jev's numbers have 2 decimals, and Nimble's are not rounded. A very small probability can come back in exponent form, for example 3.7e-06. confidence is 1 when one option has all the probability and 0 when every option is equally likely. |
| Our limit | The pool takes 7 requests in flight at once, and each organization at most 5 of them. Bespoke allows our account 8, and our test environment uses one. The pool also takes 600 requests a minute (450 per organization) and 15,000,000 tokens a minute (11,250,000 per organization). The Nimble models share these limits. |
| Bespoke's limit | At most 8 requests at once for our whole account, over all the Nimble models. Above that Bespoke answers 429 with Retry-After, which the gateway relays (see Limits and failures). |
| Retention | Bespoke keeps request content for up to 30 days, to debug the service and check for abuse, and then deletes it. The deployment declares prompt_logging: "retained". Bespoke does not train on API content unless an organization opts in, and ours does not. See Data policy. |
Clef and Clef-flash
The gateway calls Cloudflare's own API, Workers AI. Clef-flash is the smaller model (9 billion parameters, against Clef's 27 billion). It costs $0.09 per million input tokens, against $0.24 for Clef.
| Clef and Clef-flash | |
|---|---|
| Route | Workers AI's REST API only, as clef and clef-flash. The path picks the model, and the gateway sets the same model in the body. It removes Cloudflare's envelope (result, success, errors, messages), so you get Jev's answer shape. A 200 whose envelope does not say success: true is a 502 upstream_error and costs nothing. |
| Questions | 1 to 64 questions per call. Each question id (a key of questions) must match ^[A-Za-z0-9_.-]{1,100}$. A choice takes 2 to 255 options, so a one-option choice (which Jev takes) is refused. A score takes 2 to 10 levels. Cloudflare refuses a body outside these limits with a 422 (or 400). |
| Structured content | instructions, a score level and a choice option's description may be a JSON object or array, as on Jev. An option's description may be null. Cloudflare's schema allows both, and a probe with JSON instructions and JSON option descriptions answered 200 (2026-10-02). |
| Context | 65,536 tokens, the state and every question together. Before the model runs, Cloudflare estimates the request's tokens at about 4 characters a token. It refuses a request estimated above 65,536 tokens (about 262,000 characters) with a 413. The gateway returns it as 413 context_overflow, and it costs nothing. Cloudflare's schema says a long state is cut to fit, but in our test (2026-10-02) a request of 522,286 characters was refused, not cut. |
| Images | Not accepted through the gateway. Clef's images field, Cloudflare's addition to System One, is dropped and recorded in x-tm-dropped-params. With "provider": {"require_parameters": true} it is a 400. |
request_id | Do not send a top-level request_id. The gateway drops and records it. Workers AI reads that field as a lookup in its queue of async requests, and answers 404. |
| Numbers | 4 decimals. Cloudflare reports output_tokens: 0 on every answer. confidence is on Clef's own scale, which Cloudflare describes only as derived from the probabilities. On one live answer (three options, the top one at 0.8274), Clef's confidence was 0.5654, where Jev's formula gives 0.7411. A threshold that you tuned on Jev's confidence does not transfer to Clef. |
| Busy | When Workers AI is busy, Cloudflare answers 429 with code 3040, "Capacity temporarily exceeded, please try again." The gateway relays it with Cloudflare's words. Without a Retry-After, the relayed 429 has no retry-after either, and the pool pauses for 1 s. A 5xx (for example Cloudflare's 500, "Model execution failed") counts against the health circuit. A 408, Cloudflare's own timeout, is a retryable 504 upstream_error that does not count. |
| Our limit | Clef and Clef-flash share one pool. It takes 200 requests a minute (150 per organization), at most 8 requests at a time (6 per organization) and 2,000,000 tokens a minute (1,500,000 per organization). |
| Cloudflare's limit | Cloudflare publishes 300 requests a minute for Clef's task type, Text Generation. It does not say whether Clef and Clef-flash share that limit, so our pool stays below it. Cloudflare's 429 with code 3036 means that our account's free daily allocation is used up. The gateway returns 503 model_unavailable with metadata.retryable: false. This is our account, never yours, and it never counts against the health circuit. |
| Retention | Cloudflare says that it does not read, store or train on requests to Clef, or on their responses. The deployment declares prompt_logging: "none". The gateway calls Workers AI directly, never through Cloudflare's AI Gateway, which logs prompts and responses by default. See Data policy. |
Decider 2B and Kev 4B
RouterPlus runs its own decision-model service. Both models run on our own GPUs in the United States, and each reads English.
| Decider 2B | Kev 4B | |
|---|---|---|
| Catalog id | routerplus/decider-2b | routerplus/kev-4b |
| Name at RouterPlus | decider-2b | kev-4b |
| Input, per million tokens | $0 | $0 |
| Context | 25,600 tokens. A longer input is cut (see below). | 8,192 tokens. A longer input is refused. |
Rules for both:
| Decider 2B and Kev 4B | |
|---|---|
| Route | RouterPlus's endpoint only. One deployment serves both models, so one pool, one health circuit and one pause after a 429 cover both. |
| Price | Free: input and output are $0, and usage.cost is 0 on every call. RouterPlus runs these models itself and charges nothing for them. See Billing. |
| Questions | The gateway checks the System One rules above. Both models take every form that Jev takes: instructions as a string or as a JSON object or array, score levels as strings or as JSON objects (the answer's legend gives the levels as you sent them), and a choice option's description as a string, JSON or null. In probes on 2026-10-02, each model also answered a choice with one option, a choice of 255 options, a score of 2 and of 10 levels, a yes/no with described true and false, a JSON object or array as the state, and 200 questions in one request. A question that RouterPlus calls malformed is its 400, relayed with RouterPlus's reason for each question id (see Errors). It costs nothing. |
| Long input | Decider 2B reads up to 25,600 tokens and cuts a longer input. It keeps the question and its options first, then the start of the state, and drops the rest without an error. Kev 4B never cuts: above 8,192 tokens it refuses the request with a 400. The gateway bills RouterPlus's usage.input_tokens, which counts only the tokens the model read: at most the model's context. In a test on 2026-10-02, a Decider 2B request of 30,053 tokens billed 25,600. RouterPlus can refuse a long input instead of cutting it (its truncate field), but the gateway drops that field. Through the gateway, a long input on Decider 2B is always cut. Put what matters at the start of the state. Use Kev 4B for an input up to 8,192 tokens and Decider 2B up to 25,600. |
| Response cache | RouterPlus keeps each answer, with its usage, for 600 s (10 minutes). The cache key is the RouterPlus API key, the model and the exact request body, byte for byte. The gateway sends every request with our one RouterPlus key, so an identical request from any customer within 600 s can get the cached answer: the same answers and the same input count in usage. Your response keeps its own id. RouterPlus marks a hit with usage.cached: true and the header X-Cache: HIT in its own response. A cached answer costs $0: the listing prices cached input at $0. The gateway's response marks it with usage.cached_input_tokens, equal to usage.input_tokens. RouterPlus skips its cache for a request with "cache": false or a Cache-Control: no-cache header. The gateway forwards neither, so you cannot skip the cache through the gateway. |
| Request id | RouterPlus sends X-Request-Id (req_…) on every answer. The gateway stores it with the request's record as the provider's id. Your response's id and its x-request-id header are the gateway's own. |
| Cold start | After a quiet period, the first request starts the model on a GPU. This takes 15 to 20 s. The gateway waits up to 60 s for a decision, so a cold start is a slow 200. |
| Our limit | The pool takes 11,400 requests a minute, 15,000,000 tokens a minute and 59 requests in flight at once. Each organization may use 75 % of each: 8,550 requests a minute, 11,250,000 tokens a minute, 44 at once. The 59 in flight is the burst limit, since a pool has no limit per second. At RouterPlus's typical 0.31 s a call, 59 in flight carry about 190 a second, just above the pool's 11,400 a minute. With our test environment's 10 a second, that stays within RouterPlus's own limit. Your organization's own limits bind first. |
| RouterPlus's limit | About 200 requests a second for the whole endpoint, shared with our test environment. A burst of repeated requests can reach it, because cached answers return sooner. Above it RouterPlus answers 429. The gateway relays it and pauses the pool for both models. |
| Busy or failed | A 5xx from RouterPlus counts against the health circuit: two in a row open it for 30 s for both models. A RouterPlus 401, 402 or 403 refuses our key, never yours: you get 503 model_unavailable. |
| Retention | The RouterPlus model service writes no request logs. Its answer cache keeps each answer and its usage for 600 s, under a key made from our RouterPlus key and the request. The cache is all it keeps, so the deployment declares prompt_logging: "none". Nothing is used for training. See Data policy. |
Perplexity Decider v1 27B
The gateway calls Perplexity's own API. This model reads and bills the state once per question, so the cost grows with the number of questions.
| Perplexity Decider v1 27B | |||||
|---|---|---|---|---|---|
| Route | Perplexity's API only, as pplx-decider-v1-27b. Perplexity answers in Jev's shape, with no envelope. The gateway stores Perplexity's x-request-id with the request's record as the provider's id. | ||||
| The state, once per question | Each question runs its own prompt, which holds and bills the whole state. In a test on 2026-10-02, a state of 1,843 tokens billed 1,843 tokens with one question and 9,215 with five. One call with ten questions saves round trips, not tokens: it costs about what ten calls cost. The question ids are not billed. output_tokens is one per question, at $0. A choice with one option runs no prompt and costs nothing: a request of only such questions returns 0 tokens and costs $0. | ||||
| Questions | 1 to 128 questions per call. Above 128 is Perplexity's 400, "Each request needs between 1 and 128 questions". A choice takes 1 to 255 options, as on Jev. A score takes 2 to 10 levels. A question id is any key the gateway takes. | ||||
| Structured content | instructions, a score level and a choice option's description may be a JSON object or array, as on Jev. An option's description may be null. A probe with JSON instructions, JSON option descriptions and JSON score levels answered 200 (2026-10-02). | ||||
| Context | 262,144 tokens for each question's prompt: the state and that one question. A request with several questions can bill more than that in all: a state of 52,348 tokens with six questions billed 314,088 tokens (2026-10-02). Above the limit, Perplexity refuses the request with a 400, "Input length (262144) exceeds or equals model's maximum context length (262144)". The gateway returns it as 400 context_overflow, with metadata.retryable: false, and it costs nothing. Perplexity does not cut a long state. | ||||
| Image parts | Decision models take text and JSON. An object with "type": "image_url", anywhere in the state or in a question, is a 400 from the gateway before any money is reserved, for example state[1]: an object with "type": "image_url" is an image part, and model "perplexity/pplx-decider-v1-27b" takes text and JSON only. Other JSON, including a field named type with another value, goes as you sent it. | ||||
| Numbers | Full precision, as Nimble's. On a choice, confidence is Jev's: (p_max − 1/n) / (1 − 1/n), so a threshold that you tuned on Jev's choice confidence transfers. On a score it does not. Perplexity's score confidence is `1 − Σ pᵢ· | i − m | / ((1/L)·Σⱼ | j − m | ), where mis the most likely level andLthe number of levels. Jev measures the second sum about the central level, so the two agree only when the most likely level is the central one. On one live answer (three levels at 0.756, 0.193 and 0.051), Perplexity'sconfidence` was 0.7051, where Jev's formula gives 0.5577. Tune a score threshold on Perplexity Decider. |
| Busy | Perplexity answers 504 when the model does not answer in about a minute. The body is an HTML page. The gateway returns a retryable 504 upstream_error that counts against the health circuit. In Perplexity's tests, a few hundred input tokens answered in under 2 s, and a prompt near the context in 23 s. | ||||
| Our limit | The pool takes 540 requests a minute, at most 8 requests at a time and 15,000,000 tokens a minute. Each organization may use 75 % of each: 405 requests a minute, 6 at a time, 11,250,000 tokens a minute. Tokens are held at the gateway's estimate while a request runs, with the state counted once per question, and corrected to Perplexity's count when it settles. A long state with many questions can be larger than a token limit on its own. Such a request is a 429 rate_limit with x-tm-limit-kind: tpm before anything reaches Perplexity, and it cannot pass as it is. Ask fewer questions per call, or send a shorter state. | ||||
| Perplexity's limit | Perplexity allows our organization 10 requests a second, over a rolling second, shared with our test environment. Our pools keep to that as a minute budget (600 a minute in all) and do not count seconds, so a burst inside one second can get Perplexity's 429. Perplexity also limits large bursts of tokens, with no published number. Above a limit Perplexity answers 429, "Request rate limit exceeded, please try again later.", with Retry-After: 1, which the gateway relays. | ||||
| Retention | Perplexity says that it does not retain query data sent through its API and does not train on it: its API has zero day retention of prompt data by default. Its compute runs on AWS in North America. The deployment declares prompt_logging: "none". See Data policy. |
GPT-6 Luna Decisions
The gateway calls OpenAI's own API. OpenAI's API has its own format, so the gateway translates your System One body on the way out and OpenAI's answers on the way back.
| GPT-6 Luna Decisions | |
|---|---|
| Catalog id | openai/gpt-6-luna-decisions. Released 2026-10-06, the day OpenAI's Decisions API opened to every developer as a public beta. |
| Route | OpenAI's API only, as gpt-6-luna. One provider, no failover. The gateway stores OpenAI's x-request-id (req_…) with the request's record as the provider's id. |
| Each question's prompt | OpenAI bills the state once per call, but it runs a prompt for each question, and each question adds about 150 input tokens. In tests on 2026-10-07, one yes/no about a two-character state billed 156 tokens, two billed 307 and five billed 760. With a state of 2,795 tokens, each added question cost 151 tokens more. The question ids are not billed. One call with many questions saves round trips and the state's tokens, but each question still costs about 150 tokens. The gateway holds 192 tokens for each question (see Our limit below). |
| Questions | 1 to 200 questions per call. A choice takes 2 to 255 options, so a one-option choice (which Jev takes) is refused. A score takes 2 to 10 levels. A question id is any key the gateway takes: a 300-character id and an id with a space and an accent answered 200 (2026-10-07). OpenAI refuses a body outside these limits with a 400. |
| Translation | The state and instructions: a string goes as you sent it, and a JSON object or array goes as compact JSON text. A choice: each option becomes one of OpenAI's choices, with the option name as its value and the description beside it (a JSON description as JSON text, and no description for null). A score: each level becomes one of OpenAI's levels, with the level's text as its label (a JSON level as JSON text). The answer's legend gives the levels as you sent them. A noul: OpenAI's predicate, the probability that the statement is true. A predicate has no criteria, so the gateway writes your true and false descriptions under the instructions, as two lines: true: … and false: …. In a live test the descriptions moved the probability the way they say: from 0.82 to 0.90 on an urgent ticket, and from 0.10 to 0.02 on a calm one. |
| Error names | OpenAI names a question in an error by its place in OpenAI's list. The gateway puts your question id (at most its first 64 characters) and System One's field name in its place: OpenAI's choices and levels are both criteria. |
| Refusal | OpenAI can decline one question and answer the rest. That question's answer is {"type": "refusal"}, under its id, with no value. No other System One model sends this type, so check each answer's type before you read choice, score or noul. The call is still billed: OpenAI bills the input of every question, the declined one too. In our tests (2026-10-07), OpenAI declined a question with empty instructions, and a choice of 255 options whose names meant nothing. The Playground shows such an answer as Refused. |
| Numbers | 2 decimals, as Jev's: OpenAI rounds them. confidence is OpenAI's own, and OpenAI does not document it as Jev's formula. A threshold that you tuned on Jev's confidence does not transfer to GPT-6 Luna Decisions. |
| Context | 272,000 tokens, the state and every question together. OpenAI's listed price holds up to there. OpenAI bills input above 272,000 tokens at twice the rate. The model itself reads up to 1,050,000 tokens. The request body is at most 1 MB. |
| Images | Not through the gateway. The state goes to OpenAI as text, so an object such as {"type": "image_url", ...} in the state is read as text, not as an image. Decision models take text and JSON. |
| Busy | In our probes a call took about 0.1 to 0.3 s. A 5xx from OpenAI counts against the health circuit. OpenAI's 404 model_not_found means that our access to the model was withdrawn. You get 404 model_unavailable with OpenAI's words, The model `gpt-6-luna` does not exist or you do not have access to it. |
| Our limit | The pool takes 24,000 requests a minute, 150,000,000 tokens a minute and 64 requests in flight at once. Each organization may use 75 % of each: 18,000 requests a minute, 112,500,000 tokens a minute, 48 at once. Tokens are held at the gateway's estimate while a request runs, with 192 tokens for each question, so a request with many questions is held near what OpenAI bills. The hold is corrected to OpenAI's count when the request settles. |
| OpenAI's limit | OpenAI allows our organization 30,000 requests and 180,000,000 tokens a minute on the model. Our test environment uses the same OpenAI key, and its pool takes 600 requests a minute, 6,000,000 tokens a minute and 8 in flight. Above OpenAI's limit, OpenAI answers 429. The gateway relays it with OpenAI's retry-after when it sends one. |
| Retention | OpenAI keeps abuse-monitoring logs of /v1/decisions requests for up to 30 days. Zero Data Retention needs an eligible account and an agreement with OpenAI, and our organization does not have it. So the deployment declares prompt_logging: "retained". OpenAI does not train on API data by default. See Data policy. |
Sage 1.2
The gateway calls Levanto's System One API. Like Perplexity Decider, Sage reads and bills the state once per question.
| Sage 1.2 | |
|---|---|
| Route | Levanto's API only, as sage-latest. Levanto answers in Jev's shape. |
| The state, once per question | Levanto reads and bills the state once for each question, so one call with ten questions saves round trips, not tokens. The gateway holds the state once per question too. |
| Questions | A choice takes 2 to 120 options and a score 2 to 26 levels. Every question gets an answer, never null. Levanto refuses a request outside these limits. |
| Not offered | Levanto's own /decide format (kind, sort, tags), images, grounding and reasoning. A question with kind is a 400. reasoning and latency_mode are dropped and recorded. |
| Retention | The deployment declares prompt_logging: "retained". See Data policy. |
GLiNER schema
GLiNER-2.5-Decide is a small encoder model. It reads one text and fills in a schema. It classifies the text against your labels, and it can extract entities, structured fields and relations.
State. Text only: a non-empty string, or {"kind": "text", "value": "..."}. A list, an image or any other state is a 400 that names the field. To judge a record, send it as text, for example the JSON as a string.
Schema. GLiNER takes no questions. It takes schema, Fastino's own schema object, which the gateway checks and forwards unchanged. schema has only these four keys. Send at least one, and each key you send must be non-empty (an empty list or object is a 400 that names the key). A flat list in place of the object is a 400 (Fastino has deprecated it).
| Key | Form | Limits |
|---|---|---|
classifications | A list of {"task", "labels", "multi_label"?, "top_k"?, "cls_threshold"?} | 1 to 50 tasks. task is 1 to 256 characters, unique in the schema, and not __proto__. labels is 1 to 100 unique non-empty strings. multi_label is a boolean. top_k is an integer, 1 or more. cls_threshold is a number from 0 to 1. Any other key is a 400 that names it. |
entities | A list of entity names, or of {"name", "description"?} | 1 to 50 entities. Each name is a non-empty string, unique in the list. |
structures | An object of structure name to a list of fields, each "field::type::description" | 1 to 50 structures. A name is 1 to 256 characters and not __proto__. 1 to 50 non-empty field strings per structure. |
relations | A list of relation names, or an object of name to {"description"?, "threshold"?} | 1 to 50 unique non-empty names. threshold is a number from 0 to 1. A head or tail key is a 400. |
- Labels are plain strings. A label cannot be an object with a description. To describe a label, put the description in the label itself:
"credit: store credit". Descriptions change results. On the same refund ticket, the labelsrefund,credit,denychoserefund, and the same labels with descriptions chosecredit. The answer carries the label text as you sent it. - The task name is the question. A classification has no
instructionsfield. GLiNER reads the task name, so a task named"Does the customer ask for money back?"with the labels["yes", "no"]is a yes/no question. - Result names share one namespace. GLiNER puts every result at the top level of its answer: each task under its task name, each structure under its structure name, the entities under
entitiesand the relations underrelation_extraction. At Fastino, two results with one name overwrite each other without an error. The gateway refuses such a collision with a 400 that names both, for example a task namedentitiesin a request that also asks for entities.
{
"model": "fastino/gliner-2.5-decide",
"state": "I was charged twice for my March invoice. Please refund the order from Acme Corp. Signed, Jane Doe.",
"schema": {
"classifications": [
{ "task": "action", "labels": ["refund", "credit", "deny"] },
{ "task": "issues", "labels": ["double_charge", "angry_customer", "fraud"], "multi_label": true }
],
"entities": ["person", { "name": "organization", "description": "business or institution name" }]
}
}GLiNER options
Three optional top-level fields go to Fastino unchanged. Leave them out to use Fastino's defaults.
| Field | Values | What it does |
|---|---|---|
threshold | A number from 0 to 1. Default 0.5. | The confidence a result needs to be returned. A task's own cls_threshold wins for that task. Lower values return more results. Higher values return fewer, surer ones. |
include_confidence | Boolean. Default true. | With false, a single-label answer is the bare label string, and a multi-label answer is a list of label strings. |
include_spans | Boolean. Default true. | With true, each entity carries start and end: its character offsets in the text. |
- A single-label task returns its top label only.
top_kdoes not add more labels to the answer. - When no label reaches the threshold, the task's answer is
null. Set"cls_threshold": 0on the task to always get the top label and its confidence. - A task with
multi_label: truereturns every label at or above the threshold, so"cls_threshold": 0returns every label with its confidence.
Store
Fastino keeps each inference unless the request says store: false. The gateway always sends store: false. A store field that you send is dropped and recorded, never forwarded. Our Fastino account also has Zero Data Retention on, so Fastino does not train on your text. See Data policy.
Not offered yet
The gateway does not reach these parts of Fastino's API:
- Fine-tuned GLiNER models (Fastino's training-job ids).
- Fastino's own batch and async routes.
- Several messages. The gateway sends Fastino one user message: the state. GLiNER does not read a system message.
Parameters that do not apply
Decision models take no sampling or output controls. The gateway forwards no temperature, max_tokens, stream, seed, user or metadata to them. None offers prompt caching, so cache_control does nothing. RouterPlus's answer cache (see Decider 2B and Kev 4B) is not prompt caching, and cache_control does not reach it.
A top-level field that the model does not take is dropped and recorded, never forwarded. RouterPlus's own truncate and cache fields are dropped in this way:
| Model | Top-level fields it takes | Examples of fields dropped |
|---|---|---|
| Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage, ScaleDown Classify-1 | model, state, questions, provider | temperature, reasoning, latency_mode, images, request_id, truncate, cache |
| GLiNER | model, state, schema, provider, threshold, include_confidence, include_spans | store, temperature, messages |
The dropped names are in the x-tm-dropped-params response header and on the attempt's ledger row. To refuse instead, send "provider": {"require_parameters": true}. A field that would be dropped is then a 400 before any money is reserved.
Response
HTTP 200, application/json. Every decision model uses the same envelope: id, object, model, answers and usage.
System One answers
Every System One model returns this shape. The differences:
| Model | model | Numbers | output_tokens |
|---|---|---|---|
| Jev | typesafe/jev-1.13 | 2 decimals | As reported |
| Nimble | its catalog id, for example "model": "bespokelabs/nimble-v4" | full precision | As reported |
| Clef, Clef-flash | "model": "cloudflare/clef" or "model": "cloudflare/clef-flash" | 4 decimals | "output_tokens": 0 |
| Decider 2B, Kev 4B | their catalog ids | As reported | As reported |
| Perplexity Decider | "model": "perplexity/pplx-decider-v1-27b" | full precision | one per question |
| GPT-6 Luna Decisions | "model": "openai/gpt-6-luna-decisions" | 2 decimals | "output_tokens": 0 |
{
"id": "5d0c1c4e-3f7a-4c55-9d1b-2f0e7f6f2a10",
"object": "decision",
"model": "typesafe/jev-1.13",
"answers": {
"team": { "type": "choice", "choice": "billing",
"probabilities": { "billing": 0.88, "technical": 0.12, "sales": 0 }, "confidence": 0.81 },
"frustration": { "type": "score", "score": 1.05,
"legend": { "0": "Calm", "1": "Annoyed", "2": "Angry" },
"probabilities": { "0": 0, "1": 0.95, "2": 0.05 }, "confidence": 0.92 },
"urgent": { "type": "noul", "noul": 0.95 }
},
"usage": { "input_tokens": 436, "output_tokens": 71, "cost": 0.000018 }
}answers holds one typed answer per question, under the ids you sent, as the provider returned them. GPT-6 Luna Decisions can also return {"type": "refusal"} for a question that OpenAI declined (see GPT-6 Luna Decisions). Check each answer's type before you read its value.
GLiNER answers
answers is GLiNER's result, unchanged. Fastino returns it as a JSON string in choices[0].message.content, and the gateway parses it into an object. For the request in GLiNER schema:
{
"id": "0b7e4c2a-6f1d-4a39-9c85-3e2d1f7a8b64",
"object": "decision",
"model": "fastino/gliner-2.5-decide",
"answers": {
"action": { "label": "refund", "confidence": 0.9327635765075684 },
"issues": [ { "label": "double_charge", "confidence": 0.872347354888916 } ],
"entities": {
"person": [ { "text": "Jane Doe", "confidence": 0.99609375, "start": 90, "end": 98 } ],
"organization": [ { "text": "Acme Corp", "confidence": 0.98828125, "start": 71, "end": 80 } ]
}
},
"usage": { "input_tokens": 33, "output_tokens": 91, "cost": 0 }
}| You asked | Key in answers | Value |
|---|---|---|
| A single-label task | The task name | {"label", "confidence"}: the top label. null when no label reaches the threshold (0.5 unless you set one; "cls_threshold": 0 always returns the top label). With include_confidence: false, the bare label string. |
| A multi-label task | The task name | [{"label", "confidence"}, ...]: every label at or above the threshold, possibly none. With include_confidence: false, a list of label strings. |
| Entities | entities | An object of entity name to [{"text", "confidence", "start", "end"}, ...]. start and end are there when include_spans is on. |
| A structure | The structure name | A list of records, each field as {"text", "confidence"}, for example {"order": [{"id": {"text": "4411", "confidence": 1.0}}]}. |
| Relations | relation_extraction | {"relations": [...]}. |
- A confidence is not a calibrated probability. It is GLiNER's score for that label or span, from 0 to 1. Multi-label confidences are independent and do not sum to 1. Compare them within one model, not with System One's probabilities.
- GLiNER has no "not sure" answer. A low confidence shows that it is unsure.
- The gateway accepts a 200 only when the content parses to a JSON object with every task and structure name you asked for, plus
entitiesandrelation_extractionwhen you asked for them. Anything else is anupstream_errorand costs nothing.
Usage and headers
| Field | Meaning |
|---|---|
id | The gateway's request id, also in the x-request-id header. Look the request up with GET /v1/generation?id=. |
model | The catalog id that was billed. The provider's own name for the model is in the x-tm-upstream-model header. |
usage.input_tokens | Input tokens, billed at the model's input rate. The provider's count (Fastino's prompt_tokens for GLiNER). On Decider 2B, at most the model's context. On Perplexity Decider and Sage, the state once per question. |
usage.output_tokens | Output tokens, billed at the model's output rate. Every decision model prices output at $0. Sage, Clef, Clef-flash and GPT-6 Luna Decisions report 0, and Perplexity Decider one per question. For GLiNER, Fastino's completion_tokens. |
usage.cached_input_tokens | Present only on an answer from RouterPlus's cache (Decider 2B, Kev 4B). Equal to usage.input_tokens, billed at the cached-input price, $0. |
usage.cost | USD, the full debit for the request after any discount, including any attempt that failed over before this one. 0 on BYOK. Absent on static dev keys. |
usage.cost_before_discount | Present only when a discount applied. What the request costs at the list price. |
usage.discount_percent | Present only when a discount applied. The percent taken off. |
| Header | Value |
|---|---|
x-request-id | The gateway's request id. |
x-tm-provider | typesafe, bespokelabs, workers-ai, routerplus, perplexity-decisions, openai-decisions, levanto or fastino |
x-tm-upstream-model | The provider's own name for the model, for example clef or clef-flash from workers-ai, decider-2b from routerplus, pplx-decider-v1-27b from perplexity-decisions, gpt-6-luna from openai-decisions |
x-tm-attempts | The attempts made. |
x-tm-upstream-status | The provider's status, when a provider answered. |
x-tm-discount-percent | When a discount applied. |
x-tm-dropped-params | When something was dropped. |
Billing
A decision is metered like any other request: key limits, the reservation, the ledger and usage.cost all work as on the chat routes. Every decision model is priced per million tokens. Each charge rounds down to the micro-dollar.
Decider 2B and Kev 4B are free. The other models charge for input only. On Perplexity Decider and Sage the cost grows with the number of questions as well as with the state.
| Model | Input, per million tokens | Cached input, per million tokens | Output, per million tokens | What one call costs |
|---|---|---|---|---|
| Jev | $0.042 | – | $0 | A three-question call of about 400 input tokens costs about $0.000017. |
| Nimble v4 | $0.04 | – | $0 | A three-question call of 178 input tokens costs $0.000007. Bespoke's own price, with no markup. bespokelabs/nimble-latest costs the same: $0.04 is Bespoke's price for nimble-latest. |
| Nimble v4 Max | $0.09 | – | $0 | The same call costs $0.000016. Bespoke's own price, with no markup. |
| Clef | $0.24 | – | $0 | A call of 400 input tokens costs $0.000096. Cloudflare's own price, with no markup. |
| Clef-flash | $0.09 | – | $0 | A call of 400 input tokens costs $0.000036. Cloudflare's own price, with no markup. |
| Decider 2B | $0 | $0 | $0 | Nothing. The call is still metered, limited and recorded in GET /v1/generation, and it still needs an organization with credit. |
| Kev 4B | $0 | $0 | $0 | Nothing, as on Decider 2B. |
| Perplexity Decider | $0.04 | – | $0 | Five questions about a state of 1,843 tokens bill 9,215 input tokens: $0.000368. Perplexity's own price, with no markup. |
| GPT-6 Luna Decisions | $0.10 | – | $0 | A three-question call (a choice, a score and a yes/no about a short complaint) billed 396 input tokens: $0.000039. OpenAI's own price, with no markup. |
| Sage | $0.05 | – | $0 | Three questions about a short ticket read 204 input tokens: $0.00001. Levanto's own price, with no markup. |
| GLiNER | $0.03 | – | $0 | A call of 1,000 input tokens costs $0.00003. A call of 33 input tokens, as in the example in GLiNER answers, rounds down to $0. |
A dash means the model has no cached-input price.
If a provider answers 200 without a usage object, the gateway bills its own estimate (the amount it reserved) and marks the attempt estimated:
- For GLiNER, the estimate comes from the size of the text and from the tasks, labels and extraction keys in the schema.
- For Perplexity Decider and Sage, it counts the state once per question.
- For GPT-6 Luna Decisions, it counts 192 tokens for each question.
These 200s are an upstream_error and cost nothing:
- A 200 that does not answer every question.
- A GLiNER 200 whose content is not the object described in GLiNER answers.
- A GPT-6 Luna Decisions 200 that answers a question in another type, or with an option or a level that was not asked.
After a provider answers 200, the gateway never sends the same request to a second provider.
Discount
A discount may apply to some or all decision models, each at its own percent, from every key, including the playground's. The list price does not change. The discount can change or end, so read it from each response. A discounted model's row in /api/models.json carries discount_percent, and its page shows the percent beside the struck list price.
usage.costis what you pay, after the discount.usage.cost_before_discountis the list price of the request.usage.discount_percentand thex-tm-discount-percentheader give the percent.- When no discount applies, these three are absent.
A discounted charge is the list charge × (100 − percent) / 100, rounded down to the micro-dollar. At 100 % it is 0, and the request is still metered, limited and recorded in GET /v1/generation. The discount is for organizations with credit. A balance of $0 is refused with 429 insufficient_quota whatever the discount. A new account must complete verified browser sign-in and buy credits before its first decision.
On Decider 2B and Kev 4B the list price is already $0, so the discount changes nothing: usage.cost and usage.cost_before_discount are both 0. When a discount covers one of them, the percent is still reported, so your code can read one shape for all twelve.
Limits and failures
These rules hold for every decision model. Each model's section gives its own numbers.
Pool limits. Each deployment has a pool with limits on requests a minute, tokens a minute and requests in flight. Each organization may use 75 % of each. Your organization's own limits bind first: by default 300 requests a minute, 1,000,000 tokens a minute and 8 in flight (see Rate limits & spend caps). A burst above any limit is a 429 rate_limit from the gateway, before anything reaches the provider. It carries retry-after and the x-tm-limit-* headers. x-tm-limit-kind names the limit: rpm, tpm, concurrency, or cooldown for the pause after a relayed provider 429.
A provider's 429. The gateway relays it as 429 rate_limit with x-tm-upstream-status: 429, the provider's words, and its Retry-After when it sends one. The gateway then pauses the deployment's pool for that retry-after, at least 1 s and at most 60 s. A request inside the pause gets the gateway's own 429 rate_limit, with x-tm-limit-kind: cooldown, x-tm-limit-id: pool:… and no x-tm-upstream-status. A relayed 429 never counts against the deployment's health circuit.
The health circuit. A provider 5xx is a retryable upstream_error that counts against the deployment's health circuit: two in a row open it for 30 s, and in that window every request but one probe gets 503 gateway_error, "all deployments cooling down". Retry after a few seconds, with backoff. A 408, a provider's own timeout, is a retryable 504 upstream_error that does not count.
Our account. A 401, 402 or 403 from any System One provider refuses our account, never yours. You get 503 model_unavailable, "out of capacity at the provider". Each refusal counts against the circuit, so after two of them most requests get the 503 gateway_error above.
A provider's refusal. A 4xx that says the request is wrong is relayed as an upstream_error with the provider's words and x-tm-upstream-status. It is not tried on a second provider, and it costs nothing.
Context
| Model | Context | A longer request |
|---|---|---|
| Jev | 64 000 tokens per request through TypeSafe (32 000 for the state plus the longest question) | Refused by TypeSafe |
| Nimble | 262,144 tokens per question's prompt, state included | Refused by Bespoke |
| Clef, Clef-flash | 65,536 tokens per request, as Cloudflare estimates them | Refused with a 413 context_overflow |
| Decider 2B | 25,600 tokens | Cut, and only the tokens read are billed |
| Kev 4B | 8,192 tokens | Refused with a 400 |
| Perplexity Decider | 262,144 tokens per question's prompt, state included | Refused with a 400 context_overflow |
| GPT-6 Luna Decisions | 272,000 tokens per request at the listed price, up to 1,050,000 at twice the rate | The request body is at most 1 MB |
| Sage | 32 768 tokens per request | Refused by Levanto |
| GLiNER-2.5-Decide | 8,192 tokens | Refused by Fastino |
Errors
Errors use the gateway's normal shape. See Errors.
| Status | error.code | When |
|---|---|---|
| 200 | none | GPT-6 Luna Decisions: OpenAI declined one question and answered the rest. That answer is {"type": "refusal"}, and the call is billed. |
| 400 | invalid_request | The body failed a rule above, such as a question in another model's format or a field a question does not take. The message names the field, for example questions.team.criteria: at most 255 options. |
| 400 | invalid_request | The request does not fit the model's grammar: schema on Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage or ScaleDown Classify-1 (the message says the model takes questions), or questions on GLiNER (the message says the model takes a GLiNER schema). |
| 400 | invalid_request | GLiNER only: a state that is not text, a flat-list schema, a schema key or classification key GLiNER does not take, a limit above, two tasks with one name, or two results with one name (the message names both). |
| 400 | invalid_request | The model is not a decision model. The message names the route that serves it. |
| 400 | invalid_request | Perplexity Decider: an object with "type": "image_url" anywhere in the state or in a question. The message names where. |
| 400 | context_overflow | Perplexity Decider: one question's prompt (the state and that question) is above 262,144 tokens. The message is Perplexity's, prefixed upstream perplexity-decisions returned 400:. metadata.retryable is false: make the state shorter. |
| 400 | upstream_error | GPT-6 Luna Decisions: the request is outside OpenAI's limits, for example more than 200 questions, or a choice with one option. OpenAI's 400, with x-tm-upstream-status: 400 and your question id in place of OpenAI's index. For example upstream openai-decisions returned 400: Invalid 'questions.team.criteria': array too short. Expected an array with minimum length 2, but got an array with length 1 instead. or upstream openai-decisions returned 400: Invalid 'questions': array too long. Expected an array with maximum length 200, but got an array with length 201 instead. It does not count against the circuit. |
| 404 | model_unavailable | No decision model has this id. gpt-6-luna-decisions without openai/ is such an id: send openai/gpt-6-luna-decisions (gpt-6-luna is the chat model, a 400). |
| 404 | model_unavailable | GPT-6 Luna Decisions: OpenAI answered 404 model_not_found, because our access to the model was withdrawn. It is our account, never yours. |
| 413 | request_too_large | The body is larger than 1 MB. |
| 413 | context_overflow | Clef and Clef-flash: Cloudflare estimated the request above 65,536 tokens (its 413, code 5021), for example upstream workers-ai returned 413: The estimated number of input and maximum output tokens (130569) exceeded this model context window limit (65536). metadata.retryable is false: make the state shorter. |
| 422 and other 4xx | upstream_error | The provider refused the request as wrong. The message has the provider's reason, and x-tm-upstream-status its status. Cases by model follow this table. |
| 429 | rate_limit | Nimble: more than its pool allows, or Bespoke already runs 8 requests for our account, over all the Nimble models (Bespoke's 429). |
| 429 | rate_limit | Clef and Clef-flash: more than their shared pool allows, or Workers AI is busy (Cloudflare's 429 with code 3040). The pool pauses for at most 60 s (1 s when Cloudflare sends no Retry-After). Every other Cloudflare 429 is relayed the same way, except code 3036 (a 503). |
| 429 | rate_limit | Decider 2B and Kev 4B: more than their shared pool allows (59 in flight at once, 44 per organization), or RouterPlus's 429 at about 200 requests a second. |
| 429 | rate_limit | Perplexity Decider: more than its pool allows (a long state with many questions can exceed the token limit on its own, x-tm-limit-kind: tpm), or Perplexity's limit of 10 requests a second for our organization. |
| 429 | rate_limit | GPT-6 Luna Decisions: more than its pool allows, or OpenAI's limit for our organization (30,000 requests and 180,000,000 tokens a minute). |
| 429 | rate_limit | GLiNER: a Fastino 429. |
| 503 | model_unavailable | The provider refused our account. The message says the model is out of capacity at the provider. It is our account, never yours. See the cases after this table. |
| 503 | gateway_error | "all deployments cooling down", with retry-after: 5: the deployment's health circuit is open. While a provider refuses our account, only one request in each 30 s reaches it and gets the 503 model_unavailable. A provider's 429 never counts, so Nimble's limit of 8 requests at once, Cloudflare's busy answer on Clef, RouterPlus's limit of about 200 a second, Perplexity's limit of 10 a second and OpenAI's limit of 30,000 a minute stay 429s, and Cloudflare's code 3036 stays a 503 model_unavailable. |
| 504 | upstream_error | A System One provider answered 408, its own timeout, for example upstream workers-ai returned 408: Request timeout on Clef. Retry after a few seconds. A 408 does not count against the circuit. The gateway's own wait for a decision (60 s) also ends in a 504. |
| 5xx (and 529) | upstream_error | The provider failed. Retryable. Cases by model follow this table. |
| 401, 402, 403 | auth | The provider refused our own account. This is never your key's fault. For GLiNER a 402 or 403, and for every System One model (Jev, Nimble, Clef, Clef-flash, Decider 2B, Kev 4B, Perplexity Decider, GPT-6 Luna Decisions, Sage and ScaleDown Classify-1) a 401, 402 or 403, is the 503 above. |
Provider refusals (422 and other 4xx), by model. Each of these costs nothing.
- GLiNER. A Fastino 404 (an unknown upstream model id, which is our misconfiguration, never yours) is a 502
upstream_error, withx-tm-upstream-status: 404. - Nimble: a request above Nimble's own limits (more than 64 questions, a
choicewith one option, or a question's prompt with the state above 262,144 tokens on Nimble v4, Nimble v4 Max orbespokelabs/nimble-latest) is Bespoke's 422, relayed with Bespoke's words. - Clef and Clef-flash: a request outside Clef's own limits (more than 64 questions, a question id that does not match
^[A-Za-z0-9_.-]{1,100}$, or achoicewith one option) is Cloudflare's 422 (code 5012) or 400 (code 5006), relayed with Cloudflare's words, for exampleupstream workers-ai returned 422: Request body failed validation: questions: Dictionary should have at most 64 items after validation, not 65. - Decider 2B and Kev 4B: a request that RouterPlus refuses is RouterPlus's 400, relayed here with RouterPlus's reason and
x-tm-upstream-status: 400. RouterPlus refuses a malformed question (invalid questions, then the reason for each question id, for examplescore needs criteria: a list of 2 to 10 levels) and a Kev 4B request above 8,192 tokens (the question's id, thenContextOverflow: branch too long). - Perplexity Decider: a request with more than 128 questions is Perplexity's 400, relayed with Perplexity's words (
Each request needs between 1 and 128 questions).
Refusals of our account (503 model_unavailable), by model:
- Sage: Levanto refused our account (its 401, 402 or 403, for example when our month's usage is used up).
- GLiNER: Fastino refused our account for credit or billing (its 402 or 403).
- Nimble: Bespoke refused our account for credit (its 402).
metadata.retryableisfalse. - Clef and Clef-flash: Cloudflare refused our account, for our token or plan (its 401 or 403), or because the account's free daily allocation is used up (its 429 with code 3036, until 00:00 UTC).
metadata.retryableisfalse. - A 401, 402 or 403 from any System One provider is handled the same way. That covers TypeSafe (Jev), RouterPlus (Decider 2B and Kev 4B), Perplexity (Perplexity Decider) and OpenAI (GPT-6 Luna Decisions).
On a connection with your own provider key, that provider's 401, 402 or 403 is your account, and it keeps its status. Other models are not affected.
Provider failures (5xx and 529), by model. Each 5xx counts against the circuit, as in Limits and failures.
- Sage: Levanto answers 503 while Sage loads. Retry after a few seconds.
- GLiNER: Fastino answers 425 or 503 while GLiNER starts (a cold start). A cold start can also outlast the gateway's wait for a decision (60 s, a 504). Both are a retryable
upstream_error, so retry after a few seconds. - Nimble, Clef, Decider 2B and Kev 4B, Perplexity Decider and GPT-6 Luna Decisions: see the Busy row in each model's section. A Perplexity 504 is an HTML page, so the message has no words of Perplexity's.
Examples
The examples at the top of this page use Jev. The examples below send the same System One body to other models, then a GLiNER schema.
Nimble
Send the Jev body under a Nimble model's id, or bespokelabs/nimble-latest to let Bespoke pick. Keep to 1 to 64 questions, and 2 or more options in a choice.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"bespokelabs/nimble-v4",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "bespokelabs/nimble-v4",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {
"team": {"type": "choice", "instructions": "Which team?",
"criteria": {"billing": "Payments", "technical": "Bugs"}},
"urgent": {"type": "noul", "instructions": "Is this urgent?"},
},
},
)
answers = r.json()["answers"]
print(answers["team"]["choice"], answers["urgent"]["noul"]) # e.g. billing 0.997817283712868const r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "bespokelabs/nimble-v4",
state: { ticket: "Help! My payouts have been failing for 3 days." },
questions: { urgent: { type: "noul", instructions: "Is `ticket` urgent?" } },
}),
});
const { answers, usage } = await r.json();
console.log(answers.urgent.noul, usage.input_tokens, usage.cost);Clef
Send the Jev body under Clef's or Clef-flash's id, within Clef's limits. The Node example uses Clef-flash.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"cloudflare/clef",
"state":"Checkout has been failing for every customer for the last hour.",
"questions":{"urgent":{"type":"noul","instructions":"Is this support request urgent?"}}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "cloudflare/clef",
"state": "Checkout has been failing for every customer for the last hour.",
"questions": {
"urgent": {"type": "noul", "instructions": "Is this support request urgent?"},
"team": {"type": "choice", "instructions": "Which team should handle this request?",
"criteria": {"billing": "Payments and invoices", "technical": "Bugs and outages",
"sales": "Plans and upgrades"}},
"severity": {"type": "score", "instructions": "How severe is the customer impact?",
"criteria": ["No impact", "Minor", "Major", "Critical"]},
},
},
)
answers = r.json()["answers"]
team = answers["team"]
print(team["choice"], team["probabilities"][team["choice"]], team["confidence"]) # e.g. technical 0.8274 0.5654const r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "cloudflare/clef-flash",
state: { ticket: "Checkout has been failing for every customer for the last hour." },
questions: { urgent: { type: "noul", instructions: "Is `ticket` urgent?" } },
}),
});
const { answers, usage } = await r.json();
console.log(answers.urgent.noul, usage.input_tokens, usage.output_tokens); // output_tokens is always 0RouterPlus models
Send the Jev body under Decider 2B's or Kev 4B's id. A cold start can take 15 to 20 s, so give your client a timeout above 60 s, the gateway's own wait.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"routerplus/decider-2b",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "routerplus/decider-2b",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {
"team": {"type": "choice", "instructions": "Which team?",
"criteria": {"billing": "Payments", "technical": "Bugs"}},
"urgent": {"type": "noul", "instructions": "Is this urgent?"},
},
},
timeout=70,
)
answers = r.json()["answers"]
print(answers["team"]["choice"], answers["urgent"]["noul"], r.headers["x-tm-upstream-model"]) # e.g. billing 0.97 decider-2bconst r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "routerplus/decider-2b",
state: { ticket: "Help! My payouts have been failing for 3 days." },
questions: { urgent: { type: "noul", instructions: "Is `ticket` urgent?" } },
}),
signal: AbortSignal.timeout(70_000),
});
const { answers, usage } = await r.json();
console.log(answers.urgent.noul, usage.input_tokens, usage.cost);Perplexity
Send the Jev body under Perplexity Decider's id, with 1 to 128 questions. Each question is billed with the whole state, so ask only the questions you need.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"perplexity/pplx-decider-v1-27b",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "perplexity/pplx-decider-v1-27b",
"state": "Help! My payouts have been failing for 3 days.",
"questions": {"urgent": {"type": "noul", "instructions": "Is this urgent?"}},
},
)
body = r.json()
print(body["answers"]["urgent"]["noul"], body["usage"]["input_tokens"]) # e.g. 0.9297849172276498 95const r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "perplexity/pplx-decider-v1-27b",
state: { ticket: "Help! My payouts have been failing for 3 days." },
questions: {
team: { type: "choice", instructions: "Which team should handle `ticket`?", criteria: { billing: "Payments", technical: "Bugs" } },
urgent: { type: "noul", instructions: "Is `ticket` urgent?" },
},
}),
});
const { answers, usage } = await r.json();
// Two questions: Perplexity bills the state twice.
console.log(answers.team.choice, answers.urgent.noul, usage.input_tokens, usage.cost);GPT-6 Luna Decisions
Send the Jev body under GPT-6 Luna Decisions' id, with 1 to 200 questions and 2 or more options in a choice. Check each answer's type, because OpenAI can decline one question.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"openai/gpt-6-luna-decisions",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "openai/gpt-6-luna-decisions",
"state": {"ticket": "Help! My payouts have been failing for 3 days."},
"questions": {
"team": {"type": "choice", "instructions": "Which team should handle `ticket`?",
"criteria": {"billing": "Payments, invoices, refunds",
"technical": "Bugs, outages, integrations", "sales": None}},
"frustration": {"type": "score", "instructions": "How frustrated is the customer?",
"criteria": ["Calm", "Annoyed", "Angry"]},
"urgent": {"type": "noul", "instructions": "Is this urgent?",
"criteria": {"true": "Explicitly time-sensitive", "false": "No urgency expressed"}},
"business": {"type": "noul", "instructions": "Is the customer a business?"},
},
},
)
for qid, answer in r.json()["answers"].items():
if answer["type"] == "refusal":
print(qid, "declined by OpenAI") # billed, but no value
else:
print(qid, answer)const r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "openai/gpt-6-luna-decisions",
state: { ticket: "Help! My payouts have been failing for 3 days." },
questions: {
team: { type: "choice", instructions: "Which team should handle `ticket`?", criteria: { billing: "Payments", technical: "Bugs" } },
urgent: { type: "noul", instructions: "Is `ticket` urgent?" },
},
}),
});
const { answers, usage } = await r.json();
// A declined question comes back as { type: "refusal" }, with no value.
for (const [id, a] of Object.entries(answers)) console.log(id, a.type === "refusal" ? "declined" : a);
// Two questions: about 150 input tokens each, on top of the state.
console.log(usage.input_tokens, usage.output_tokens, usage.cost); // output_tokens is always 0A sample answer to the Python request:
{
"id": "8c1f4b2e-7d3a-4e90-a6b5-1f2e3d4c5b6a",
"object": "decision",
"model": "openai/gpt-6-luna-decisions",
"answers": {
"team": { "type": "choice", "choice": "billing",
"probabilities": { "billing": 1, "technical": 0, "sales": 0 }, "confidence": 1 },
"frustration": { "type": "score", "score": 1.02,
"legend": { "0": "Calm", "1": "Annoyed", "2": "Angry" },
"probabilities": { "0": 0.03, "1": 0.92, "2": 0.05 }, "confidence": 0.88 },
"urgent": { "type": "noul", "noul": 0.96 },
"business": { "type": "noul", "noul": 0.49 }
},
"usage": { "input_tokens": 561, "output_tokens": 0, "cost": 0.000056 }
}Sage
Send the Jev body under Sage's id. Each question is billed with the whole state, so ask only the questions you need.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"levanto/sage-1.2",
"state":"Help! My payouts have been failing for 3 days.",
"questions":{"urgent":{"type":"noul","instructions":"Is this urgent?"}}}'GLiNER
Each answer sits under its task name.
curl https://dev2-api.routerplus.com/v1/decisions \
-H "Authorization: Bearer $TM_API_KEY" -H "content-type: application/json" \
-d '{"model":"fastino/gliner-2.5-decide",
"state":"Help! My payouts have been failing for 3 days.",
"schema":{"classifications":[{"task":"Is this urgent?","labels":["yes","no"]}]}}'import os, httpx
r = httpx.post(
"https://demo-api.routerplus.com/v1/decisions",
headers={"Authorization": f"Bearer {os.environ['TM_API_KEY']}"},
json={
"model": "fastino/gliner-2.5-decide",
"state": "Help! My payouts have been failing for 3 days.",
"schema": {"classifications": [{"task": "team",
"labels": ["billing: payments", "technical: bugs"]}]},
},
)
answer = r.json()["answers"]["team"]
print(answer["label"], answer["confidence"]) # a confidence, not a calibrated probabilityconst r = await fetch("https://demo-api.routerplus.com/v1/decisions", {
method: "POST",
headers: { authorization: `Bearer ${process.env.TM_API_KEY}`, "content-type": "application/json" },
body: JSON.stringify({
model: "fastino/gliner-2.5-decide",
state: "Help! My payouts have been failing for 3 days.",
schema: {
classifications: [
{ task: "topics", labels: ["billing", "outage", "fraud"], multi_label: true, cls_threshold: 0 },
],
},
}),
});
const { answers, usage } = await r.json();
console.log(answers.topics, usage.input_tokens, usage.output_tokens, usage.cost);Model discovery
curl -s "https://demo-api.routerplus.com/v1/models?output_modalities=decisions" \
-H "Authorization: Bearer $TM_API_KEY"Decision models carry architecture.output_modalities: ["decisions"] in GET /v1/models, and output_modalities: ["decisions"] in the public feed https://demo.routerplus.com/api/models.json. The Anthropic shape of GET /v1/models never lists them.
For agents: /docs/api-decisions.md and /llms.txt