Three shapes, one call
Every question you ask a System One model is one of three types. Which one you pick is decided by the shape of the answer, not by the difficulty of the judgment.
Choice when the answer is one item from a list. Score when the answer is a position on a scale. Noul when the answer is yes or no. You can mix all three in a single request, and they run in parallel, so a call asking five questions costs about what a call asking one costs in latency terms.
Choice
A Choice selects one option from a set you define. You name the options and describe each one; the model returns the winner, a probability for every option, and a confidence value.
from typesafe_sdk import Choice, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state="My running shoes arrived in the wrong size.",
questions={
"department": Choice(
instructions="Which team should handle this?",
criteria={
"returns": "Exchanges, refunds, wrong items",
"shipping": "Delivery delays, lost packages",
"billing": "Charges, payment problems",
},
),
},
)
print(response.answers["department"].choice)
What comes back:
{
"department": {
"type": "choice",
"choice": "returns",
"confidence": 1.0,
"probabilities": {"returns": 1.0, "shipping": 0.0, "billing": 0.0}
}
}
A wrong-size delivery is not a hard call, and the distribution shows it: all the mass on one option, confidence 1.0.
The limit is 255 options. That is generous enough for a product taxonomy and tight enough that a 4,000-item catalogue needs a cascade, running a coarse Choice first and a narrow one second. Hierarchical classification covers that shape.
One piece of advice from the docs is easy to skip and costly when you do: include an “other” or “none of the above” option when your taxonomy might not cover everything. Without it, the model has to put the mass somewhere, and a ticket about something you never anticipated gets filed under whichever option is least wrong.
Score
A Score places content on an ordered scale. You write the levels as descriptions, in order, and the model returns a position on that scale.
from typesafe_sdk import Score, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state="The export button crashes the settings page in Safari.",
questions={
"bug_severity": Score(
instructions="How severe is the reported issue?",
criteria=[
"Cosmetic; no impact to functionality",
"Broken or degraded feature, but workaround exists",
"Blocking issue; no workaround exists",
],
),
},
)
print(response.answers["bug_severity"].score)
{
"bug_severity": {
"type": "score",
"score": 1.3,
"confidence": 0.54,
"legend": {"0": "Cosmetic...", "1": "Broken...", "2": "Blocking..."},
"probabilities": {"0": 0.0, "1": 0.7, "2": 0.3}
}
}
The 1.3 is not a rounding artefact. The score is the probability-weighted mean of the level numbers: (0 x 0.0) + (1 x 0.70) + (2 x 0.30) = 1.30. The model is mostly on “workaround exists” with meaningful weight on “blocking,” and the returned number carries that split. Confidence of 0.54 says the same thing: this one is ambiguous.
That in-between value is the feature. If you only wanted the top level you could read it off the distribution. A continuous score lets you sort a backlog, set a numeric cutoff, or feed the number into a weighted total, which is what composite scoring does.
Minimum 2 levels, maximum 10. Levels must be ordered and each described in words; the model reads the descriptions, not the numbers. Scales past about five levels tend to blur, because the difference between “fairly severe” and “quite severe” is not stated anywhere in your criteria.
Noul
A Noul asks a yes/no question and returns one number: the probability that the answer is yes.
{
"is_human_escalation": {
"type": "noul",
"instructions": "Is the customer asking for a human agent?"
}
}
{
"answers": {
"is_human_escalation": {
"type": "noul",
"noul": 0.99
}
}
}
Values near 1 mean a strong yes, near 0 a strong no, and near 0.5 that the model finds both about equally likely.
Noul returns no separate confidence value. The docs are explicit about this, and the reason is that a probability from 0 to 1 already is a certainty measure: 0.5 is maximum uncertainty and both ends are maximum certainty. A second number would say the same thing twice.
That changes how you threshold. With a Choice you test confidence > 0.9. With a Noul you test distance from the middle, so a band like noul > 0.9 or noul < 0.1 is the equivalent, and anything between goes to a human. The prompt injection screen recipe uses exactly that shape.
TypeSafe’s docs do not say where the name Noul came from.
How confidence is derived
Confidence is not the model reporting a feeling. TypeSafe describes it as “a statistic computed from the probability distribution the answer already gives you.” Mass concentrated on one option gives a high number. Mass spread across several gives a low one.
Nothing asks the model how sure it is, which matters: a model trained on human preference learns that confident-sounding text scores better, and that failure mode has no way in here. RLCD is the training objective aimed at making that distribution honest.
The docs suggest three bands. High confidence: act automatically. Medium: proceed with care, ask the user to confirm, or flag for review. Low: do not act, and route to a human or fall back to another system. The worked example uses 0.5 as the floor for genuine uncertainty and 0.9 as the bar for destructive operations that run without confirmation.
The exact numbers are yours to pick. The docs put it in one line: “Your code encodes the risk tolerance.”
Picking the right primitive
Ask what your code does next with the answer.
If it switches on a value, use Choice. If it compares against a numeric cutoff or sorts a list, use Score. If it gates a single branch, use Noul.
Two things not to do. Do not build a Choice with options like “low,” “medium” and “high” when what you want is a Score: you lose the ordering and the in-between value. And do not build a Score out of unordered categories, because the probability-weighted mean of things that have no order is a meaningless number.
When a judgment has several parts, split it. TypeSafe’s build guidance is to “ask the most explicit, narrow, specific, atomic questions you can,” and since parallel questions add almost no latency, four narrow ones beat one broad one. Speculative fan-out takes that to its conclusion: ask everything you might need in one call and let your code ignore what turned out irrelevant.
FAQ
How many options can a Choice have?
Up to 255. Past that you need a cascade: a coarse Choice to pick a branch, then a second call with the options inside that branch. The same trick helps below the limit too, since a shorter option list with clearly distinct descriptions usually produces a sharper distribution.
Why does my Score return a decimal?
The score is the probability-weighted mean of your level numbers. A distribution of 70% on level 1 and 30% on level 2 returns 1.3. The decimal carries information the top level alone would lose, which is what makes scores useful for sorting and for numeric thresholds.
Why does Noul not return confidence?
The probability already is the certainty measure. A Noul value of 0.99 is a confident yes, 0.01 a confident no, and 0.5 maximum uncertainty. Choice and Score need a separate number because their distributions run over more than two outcomes, so spread cannot be read off a single value.
Can I ask several questions in one request?
Yes, and you should. Questions in a single call are evaluated in parallel, so adding more typically does not add latency. The limit is the 64k context budget for the request. TypeSafe does not document a maximum number of questions per call.
Should I use Choice or Noul for a yes/no decision?
Noul. A two-option Choice gives you the same information in a heavier shape, with a probability pair plus a confidence value where one number would do. Reserve Choice for three or more options, or for cases where you need named criteria describing each side.