/v1/decisions turns a chat model into a decision model. You send an input and a list of typed questions, and each answer comes back with the probability of every option, read from the model’s next-token scores at the answer position. No text is generated and no output is parsed. It needs no special checkpoint: any generation model served with a Jinja chat template can answer, as long as the checks below pass for it.
/v1/decisions is an SGLang extension under /v1, like /v1/score and /v1/rerank. It is not part of the OpenAI API, so call it over HTTP rather than through an OpenAI SDK method. Until a release contains it, install a nightly build, which is built from the main branch.Supported models
Other chat models are served when the answer labels are single tokens at the answer position and the answer does not start inside a reasoning block. The server checks these per request and returns a 400 that names the reason when one fails, see Errors. It does not check how the chat template renders the question or where the template itself would put an answer, so inspect
prompt_token_ids from return_prompt_token_ids before relying on a new model.
Launch command
Command
/v1/chat/completions and /generate, and decisions can run alongside that traffic. A server launched with --enable-mis or --dllm-algorithm, or one that uses a built-in conversation template instead of the tokenizer’s Jinja template, refuses decisions.
Example request
Example
type: the question type,choice,score, oryes_no.probabilities: one value per option name, level index, oryesandno, summing to 1. For a yes or no question,probabilities["yes"]is the answer.choicefor a choice question (the most probable option) orscorefor a score question (the probability-weighted mean level index).label_mass: the full-vocabulary probability of the answer labels at the answer position. A low value means the model puts most of its probability outside the offered answers.
How answers are computed
The server renders each question as one user message with the model’s chat template and thinking turned off, and labels the answersA to Z for options, 0 to 9 for levels, and yes and no. It checks that each label is one distinct token at the answer position, then runs one prefill pass per question through the scoring path of /v1/score and reads the next-token log-probabilities of the labels over the full vocabulary. For a yes or no question with log-probabilities lp_yes and lp_no and request temperature T:
probabilities["yes"] = exp(lp_yes / T) / (exp(lp_yes / T) + exp(lp_no / T)), which equals a softmax of the two label logits divided byT, because the vocabulary normalizer cancels.label_mass = exp(lp_yes) + exp(lp_no), which does not depend onT.
Request and response reference
Request fields:
Question fields:
question, descriptions, and levels can also be JSON objects or arrays. Each question becomes one user message: the input, a blank line, the question line, one line per option, level, or described yes or no answer, and a closing instruction to answer with one label only. An option renders as A: billing or A: billing - <description>, a level as 0: <description>, and a described yes or no answer as yes: <description>.
The response has object set to decisions, model, prompt_format_version, answers keyed by question id, and usage with prompt_tokens for all questions and completion_tokens 0.
Errors
The request fails with HTTP 400 for:- an unknown question type or an unknown field anywhere in the request
- option or level counts outside the ranges above
- a blank input, question, question id, or level
- a repeated question id, or option names that are blank, contain control or line break characters, or repeat another name after trimming and case folding
- a label that is not one distinct token at the answer position for the served tokenizer
- a prompt that does not fit the model’s context length
- a chat template that always thinks before answering or starts every answer with a reasoning block,
chat_template_kwargsthat set the thinking toggle to anything butfalse, a rendered prompt that leaves a reasoning block open, or a model whose reasoning parser expects answers to start inside a reasoning block when the rendered prompt does not close one - a
prompt_format_versionother than the served one, or amodelthat names a LoRA adapter - a server launched with
--enable-misor--dllm-algorithm, or one that uses a built-in conversation template, whether named with--chat-template, loaded from a JSON template file, or inferred from the model path - a model whose chat route uses a built-in encoder instead of a chat template, a tokenizer that does not encode the rendered chat text back to the same ids, or a model that is not a generation model
- a chat template that raises an error for the question message, or a server launched with
--skip-tokenizer-init
questions.
Pin and replay the prompt
The server owns the prompt wording of/v1/decisions and versions it. Every response carries prompt_format_version, and a change to the wording ships as a new version. Send prompt_format_version to fail loudly instead of receiving answers from different wording after a server upgrade.
To keep an answer reproducible independently of the server’s wording, ask for the scored ids and replay them through /v1/score, which only scores the ids you send:
Example
temperature and run in the same cache state. If the decision set temperature, send the same value in the /v1/score body. A replay right after the decision reuses its cached prefix, so it can differ slightly, as described in Reproducibility.
Thinking models
Qwen3.8-27B and Qwen3.5-35B-A3B think by default./v1/decisions turns thinking off for every question, refuses a request that turns it back on, and refuses a rendered prompt that leaves a reasoning block open, so the answer is never read inside the reasoning. Chat requests to the same server keep the model’s default, and launching with --reasoning-parser qwen3 affects chat only.
Reproducibility
These models mix full and linear attention. Answer probabilities can move by up to several hundredths (0.07 in our checks), andlabel_mass by up to about 0.14, between cold and prefix-cached requests and across batch compositions, while the chosen option stayed the same in our checks. On Qwen3.5-35B-A3B, --disable-radix-cache gave identical values across sequential repeats, at the cost of prefix reuse.
System One compatible API
POST /v1/systemone serves the same decisions in the request and response shape of the System One API, with a state, a map of noul, choice, and score questions keyed by your ids, and one answer per id. Clients written for that API, including the official TypeSafe SDKs, get decisions by pointing their base URL at the server, with the exceptions listed below. The route uses the same rendering, label checks, and scoring as /v1/decisions, and refuses the same servers, chat templates, and prompts, listed in Errors. Its request fields are checked as described below.
Command
Example
- Any
modelname is accepted, including the SDK defaultjev-latest, unless it names a LoRA adapter after a colon, as inbase:adapter. The responsemodelis always the served model. - A
noulanswer is the probability of yes. Achoiceanswer has the most probable option and ascoreanswer the probability-weighted mean level, each withprobabilitiesandconfidence, and a score answer echoes its levels inlegend. confidencefollows the formulas TypeSafe publishes for its adapter. It is a statistic of the returned probabilities, which are renormalized over the answer labels and ignore probability outside them. It is not calibrated, it can read 0 for a score split between distant levels, and thresholds tuned on another provider may not transfer.- Each answer also carries
x_label_mass, the full-vocabulary probability of its labels, which/v1/decisionsreturns aslabel_mass. The Python SDK drops fields it does not define, so read it fromresult.raw_http_response.json(). - A choice takes 1 to 255 options and a score 1 to 10 levels. Option names that are blank, contain control or line break characters, or repeat another name after trimming and case folding are refused, and so are blank or null levels, as on
/v1/decisions. - Beyond 26 options, every option gets a two-letter label (
AA,AB, and so on) checked to be one token for the served tokenizer. These labels include common words and have unequal priors, so answers above 26 options can depend on option order. More than 26 options also need a Hugging Face tokenizer that reports its added tokens, and a chat template with an added token between the message and the answer position whose following text tokenizes the same on its own, as the Qwen models do with</think>. Without them, a choice is limited to 26 options. instructionsare optional. A choice or score question without them drops its question line, and anoulquestion is answered from itstrueandfalsedescriptions. Anoulquestion with neither, such as the SDK’s bareNoul(), is refused.- Unknown top-level fields are ignored. Unknown keys inside a question or inside
noulcriteria are refused, and so are the/v1/decisionsfieldstemperature,prompt_format_version, andreturn_prompt_token_idswith any value other than null.chat_template_kwargsis accepted as an SGLang extension. - Unlike
/v1/decisions, an emptystateand empty or blank question ids are accepted, as the System One schema allows. - Invalid requests return 422 with the location of the error, other refusals return 400, and neither is retried by the SDKs.
usage.input_tokenscounts the prompt of every question, so the state is counted once per question, andusage.output_tokensis 0.- The SDKs’
models.list()and responserequest_idare not supported, becauseGET /v1/modelskeeps its OpenAI format and no request id header is sent. - The SDKs time out after 10 seconds and retry, so raise their timeout for requests with many questions over a long state. A request whose client gives up still finishes its encoding and part of its prefill, so each retry repeats at least the encoding.
Build decisions without /v1/decisions
When you need your own prompt or labels, build the prompt on the client and score it through /v1/score, which takes token ids and one label list per item. This needs transformers and jinja2 on the client. Every label must be exactly one token at the answer position:
Example
enable_thinking=False renders a closed think block, so the answer token follows it directly. Do not score a prompt that leaves the think block open, because the scored position then falls inside the reasoning.
Structured output gives the same decision through generation. A one-token regex such as (A|B|C) with temperature 0 and logprobs returns probabilities renormalized over the labels, equal to the /v1/score values for one-character labels in the same cache state. A JSON schema with enum fields fills several fields in one response, but gives no clean per-option probability, because an enum value can span several tokens. See Structured outputs for the request formats.
Limitations
- Labels must be one token at the answer position.
/v1/decisionsassigns one-token labels for you, and a client-built prompt must do the same. - For
yes_no,label_masscounts only the lowercaseyesandnotokens. The model also puts probability onYesandNo, so this value reads lower than for choice and score questions even on clear cases, whileprobabilities["yes"]is unaffected. - The server tells from the chat template and its reasoning parser whether answers could start inside a reasoning block, using the parser’s reasoning tags in the generation prompt and in the template’s own rendering of a finished answer. Reasoning that shows neither is not detected, so check
prompt_token_idsfor such a model. Some templates of models that do not reason still contain reasoning tags without ever closing a block in the generation prompt, and those models are refused. - A default temperature in
--preferred-sampling-paramscan reach scoring requests and scale both the probabilities andlabel_masswhen a decision shares a batch with generation or runs under speculative decoding. Leave it unset for decisions. - The server tokenizes each question’s full prompt once on the HTTP event loop, as
/v1/scoredoes for its items, and lets other requests run between questions. Tokenizers whose label check falls back to the full prompt encode it once more per label. - A decision takes a single
input, not a chat history. To decide about a conversation, pass the history as the input text or as a JSON array.
