Skip to main content
This cookbook takes one auto-insurance claim, runs a 14-question rubric over it 15 times, and checks whether each answer holds still across the repeats. Every check is a Noul, so each answer is P(true) for one True/False question. In a claims-triage pipeline, which sorts incoming claims into pay, deny, or send-to-a-human, that probability is the decision. When it wobbles from one run to the next, the same claim can be handled differently for no good reason. The rubric is 14 Nouls, and each run is one call that answers all 14. We do NUM_SAMPLES = 15 repeats per condition, where a condition is one model plus one setting, and show every probability that came back. The conditions:
  • Fast LLMs claude-haiku-4-5 and gpt-5.4-mini, at temperature 0 and at the API default.
  • The same two fast models in True/False mode: one bare yes or no per question instead of a probability, mapped to 1.0 and 0.0.
  • Reasoning LLMs gpt-5.5 and claude-opus-4-8, which have no temperature dial.
  • TypeSafe: one system_one call over the 14 Nouls, with a fresh uid field (a throwaway unique value) on each call.
What to look for: the LLM answers move from run to run, at temperature 0 too, and on the judgment calls the models disagree with themselves. TypeSafe is deterministic per input, but here every call carries a different uid, so its nouls move a little too - a few hundredths - measuring its sensitivity to an irrelevant field, not sampling noise.

Setup

then set TYPESAFE_API_KEY, ANTHROPIC_API_KEY, and OPENAI_API_KEY.

The state: an auto-insurance claim, as JSON

One claim with a few borderline calls built in:
  • The loss happened at a track-day event (the policy excludes “track/competitive driving”), but in the parking lot while the car was stationary, not on the circuit.
  • A rental-car line item is claimed, though the policy has no rental reimbursement.
  • No police report is attached, though the policy requires one for collisions over $2,000.
  • An auto-triage note already marks the claim “approved, pay full amount” before any human review, and without withholding the deductible.
Some rubric questions below are clear-cut; several are the borderline kind where sampled LLM answers scatter and the models disagree. The claim is a JSON structure. The LLMs get json.dumps(CLAIM) in the prompt; TypeSafe takes the structure as the state directly.

The rubric: 14 Nouls

One key -> question entry per row, phrased so a yes means the thing we are checking for is true. That keeps every row comparable: each model’s probability and TypeSafe’s noul measure the same thing.

How we ask

Each LLM call is one prompt holding json.dumps(CLAIM) and all 14 questions. The model returns a JSON object mapping each question’s key to a probability. Calls route to Anthropic or OpenAI by model name: fast models take a temperature (0 or the API default), reasoning models think first and take no temperature. The fast models also run a True/False variant: instead of a probability, they answer each question with a bare yes or no, which we map to 1.0 and 0.0. This forces a hard decision, and shows what the fast models do when they cannot leave any mass in the uncertain middle. The TypeSafe call is one system_one request over the same claim and the same 14 Nouls. Each answer’s noul is P(true). Every query also gets a fresh uid, a throwaway unique value (a nonce) that changes each run. In the LLM prompt it stops the provider from serving a cached response, while leaving the claim and rubric unchanged. In the TypeSafe state it is one extra field, and since it changes every call the state is never sent twice: TypeSafe is deterministic per input (std ~= 0 on a byte-identical state), so its row measures sensitivity to an irrelevant field, not sampling noise.
Note - despite the “ONLY a JSON object” instruction, claude-haiku-4-5 wraps nearly every reply in a ```json ... ``` fence that strict json.loads rejects (the other models return bare JSON). The helper peels the fence; a reply that still fails to parse becomes a parse failure, counted but not scored.
Each helper returns the answer, an estimated cost, and the round-trip latency.

Conditions

  • Fast models (claude-haiku-4-5, gpt-5.4-mini): probabilities at temperature 0 and at the API default. Temperature 0 is the usual “make it deterministic” advice, so we test it head-on.
  • Fast models, yes/no (claude-haiku-4-5 yes/no t=0, gpt-5.4-mini yes/no t=0): a bare yes or no per question at temperature 0, mapped to 1.0 / 0.0.
  • Reasoning models (gpt-5.5, claude-opus-4-8): one probability condition each, since they have no temperature dial.
  • TypeSafe (typesafe_noul): one condition.
We draw NUM_SAMPLES = 15 repeats per condition. Each repeat has its own cache key and counts as a distinct draw, and the cache (json_cache.json) ships with the cookbook, so re-rendering reuses it and spends no API calls. Delete the cache to sample live again.

Cost + speed (per rubric query)

One row is one full 14-question rubric call. time/call and cost/call average the 15 calls, and the vs ts_noul columns divide by the TypeSafe figures.
In this run TypeSafe is the cheapest and fastest condition. The reasoning LLM calls cost two to three orders of magnitude more, because they spend many more tokens and much more time per rubric.

Plot: every sample as a heatmap

How to read it:
  • Outer row group: the question.
  • Inner row: the condition.
  • Column: one full rubric call.
  • Cell color: red is a higher P(yes), green is lower. For the risk questions, red usually means flagged.
typesafe_noul is a near-flat row: the uid field moves it a few hundredths at most. The LLM rows vary, at temperature 0 too, and on the judgment calls the conditions disagree.
output The clear factual checks hold steady across most conditions. The judgment-heavy ones are where the LLM rows move: exclusion, rental_eligible, fraud_flag, and manual_review shift across samples or disagree across models. typesafe_noul stays nearly flat despite the changing uid.

Plot: distribution of emitted probabilities

How to read it:
  • Each row pools one condition’s 210 outputs: 14 questions x NUM_SAMPLES.
  • Bars near 0 or 1 mean decisive answers; bars near 0.5 mean the condition left mass in the uncertain middle.
  • The annotation reports the extreme-answer rate (<= 0.05 or >= 0.95) and the parse- failure rate. Parse failures are left out of the bars but counted in the annotation.
  • True/False rows are forced to 0 or 1, so they are 100% at the extremes by construction.
output The True/False rows are at the extremes by design. The probability rows show how often each condition reaches for a near-certain value instead of leaving mass in the uncertain middle.

Open it in the TypeSafe playground

The link below opens the same claim and rubric in the playground: one claim, the same 14 Nouls, and TypeSafe jev-1.12. Re-running it sends a byte-identical state each time, so the 14 nouls come back the same on every run.
Open this claim + rubric in the TypeSafe playground →