Decisions API compatibility¶
Independent implementation of OpenAI's documented Decisions API schema — not affiliated with, endorsed by, or supported by OpenAI.
client.decisions.create(input=…, questions=[…]) takes the Decisions API's documented request and
returns its documented response: model, the answers in question order, and one usage for the
call. The questions are answered by the model behind whatever surface the client is configured with —
Chat Completions, Responses, or the Anthropic-compatible Messages API.
Why it exists¶
POST /v1/decisions is one route on one
endpoint, and it asks three kinds of question: predicate, choice and score. jevper asks the same
three, so an endpoint that does not serve the route — a local server, a gateway, a provider whose
Decisions route has not shipped — can answer the same questions here, in the same schema. Moving to
the endpoint itself later is a client swap, not a rewrite.
A call¶
from openai import OpenAI
from jevper import SystemOneClient
client = SystemOneClient(OpenAI(), model="gpt-5.6-terra")
decision = client.decisions.create(
input="I was charged twice for the same subscription this month.",
questions=[
{
"type": "choice",
"name": "department",
"instructions": "Which department should handle this complaint?",
"choices": [
{"value": "billing", "description": "Payments, invoices, and refunds."},
{"value": "technical", "description": "Problems using the product."},
{"value": "sales", "description": "Pricing, plans and upgrades."},
],
}
],
)
answer = decision.answers[0]
print(answer.name, answer.choice, answer.confidence)
print([(entry.value, entry.probability) for entry in answer.probabilities])
print(decision.usage.total_tokens)
AsyncSystemOneClient carries the same accessor, with await. A dump is the wire's own payload and
nothing else — decision.model_dump() and decision.model_dump_json() carry exactly model,
answers and usage:
{"model":"gpt-5.6-terra",
"answers":[{"type":"choice","name":"department","choice":"billing",
"probabilities":[{"value":"billing","probability":0.82},
{"value":"technical","probability":0.12},
{"value":"sales","probability":0.06}],
"confidence":0.73}],
"usage":{"input_tokens":312,"input_tokens_details":{"cached_tokens":null,"cache_write_tokens":null},
"output_tokens":24,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":336}}
The request¶
| Wire field | Accepted here | Carried as |
|---|---|---|
model |
A string | model=, defaulting to the client's own model |
input, as a string |
A string | state, asked as one quoted document turn |
input, as messages |
A list of {"role": "user", "content": …} |
The state as turns: one per message, in order, with a message's input_text parts joined by a newline into its text |
questions |
1..200 question objects | One question per entry, keyed by position |
predicate |
type, instructions, optional name |
Noul(instructions=…) |
choice |
type, instructions, choices[2..255] |
Choice(instructions=…, criteria=…), one criterion per option: its description, or None |
choice.choices[].value |
A string or a boolean | The criterion key; a boolean keeps its value on the answer (see below) |
score |
type, instructions, levels[2..10] |
Score(instructions=…, criteria=…), one criterion per level: "<label>: <description>", or the label alone |
An option's description, a level's description and a question's instructions are what the model
is asked with, and the labels, names and values are echoed back on the answers, a boolean value among
them. What a criterion key cannot be is text that reads two ways: the wire counts a string "true"
and a boolean true as distinct options, and jevper asks with one key per option, so a request whose
options would collapse onto one key — a repeated value, or a string beside a boolean that reads like
it — is refused, naming both values, rather than merged.
Everything else on the call is system_one's own option, with the same meaning and the same default:
examples, model, method, api, reasoning, temperature, prompt_cache_key, extras and
keep_alive. See system_one for each of them, and Few-shot examples
for how examples is keyed here: by question name, since names are what the caller wrote, while
the ids the questions travel as are positional — and examples set on the client, which system_one
reads by id, are recognised by name here the same way, so a client built with
SystemOneClient(..., examples={"department": […]}) still guides a question named department.
What is not carried¶
| Wire field | What happens |
|---|---|
An input_image part |
Refused with a JevperError before any request: no jevper surface renders images, so an image would be evidence the model never saw |
safety_identifier |
No parameter. An unknown keyword is a TypeError, as everywhere in jevper |
An answer of type refusal |
A refusal the model gives the whole question raises ModelRefusalError, naming what was refused — see Failures. The type is in the Decision model so a payload from the endpoint itself still validates; jevper never produces one |
| Token counts the provider omitted | null, not 0 — a count nobody reported is not a count. input_tokens + output_tokens is null when either is, and cache_write_tokens is always null: jevper reads no cache-write counter from any surface |
The answers¶
Answers come back in question order, and each carries the name its question was asked with —
null when it was asked without one, exactly as the wire documents. One answer object per question
kind:
| Answer | Fields | From |
|---|---|---|
predicate |
name, probability |
The Jev noul, passed through unchanged |
choice |
name, choice, probabilities[].value, probabilities[].probability, confidence |
The winning option's own value, every option's own value, and the distribution read back entry by entry, each labelled with the option it belongs to — the order a service sends its options in is its own. confidence is the same number ChoiceAnswer carries |
score |
name, score, probabilities[].value, probabilities[].label, probabilities[].probability, confidence |
The Jev score (the probability-weighted level index), each level's index and the label the caller wrote for it, and the Jev distribution. confidence is the same number ScoreAnswer carries |
refusal |
name |
Nothing here; it exists for payloads from the endpoint itself |
The numbers a decision carries are jevper's where jevper computed them and the model's or the service's
own where it did not: a confidence is in [0, 1] when it comes from this wrapper (that number is
computed under a bound), while a probability, a distribution or a score is read back as it arrived,
unbounded as the wire's number is — which is also how a payload from the real endpoint parses through
Decision whatever numbers it carries. The wire's own type is what sets the shape here: one number,
no range.
One thing the usage shows that the endpoint's own does not: on the prompt surfaces each question is
its own provider call — the native systemone surface asks them all in one request — so the token
counts here are summed over those calls. A ten-question rubric is ten calls' worth of tokens and
latency; the answers are the same, the bill is per question.
Converting a system_one response¶
A response you already have converts with SystemOneResponse.to_decision():
response = client.system_one(state=…, questions={"intent": Choice(criteria=…)})
decision = response.to_decision()
Only what the response itself remembers can be echoed: a question's name is its id, a choice
value is the option key it was asked with, and a score level's label is that level's criteria
entry rendered as text — or, for a level the response's own legend does not name, its index, which is
how a response spells its levels everywhere else. client.decisions.create echoes the caller's own
names, values and labels instead, because it still holds the request.
Either way, decision.raw keeps the whole jevper response behind the conversion — reasoning,
debug, and each answer's laya extras where the service sent them. It is excluded from every
dump, so a payload here stays the wire's own shape.