Skip to main content
This cookbook takes one borderline user post, runs a moderation rubric over it 15 times, and checks whether each answer holds still across the repeats. Every check is a Choice, so each answer is one label from a fixed set. In a moderation pipeline that label is the routing decision: remove or leave up, escalate or auto-resolve, send to the threat, spam, or general queue. When the label wobbles from one run to the next, the same post routes to different places for no good reason. The rubric is 8 Choices, and each run is one call that answers all 8. We do 15 repeats per condition, where a condition is one model plus one setting, and plot every label that came back. The conditions:
  • Non-reasoning LLMs claude-haiku-4-5 and gpt-5.4-mini, at temperature 0 and the API default.
  • Reasoning LLMs gpt-5.5 and claude-opus-4-8, which have no temperature dial.
  • TypeSafe: one system_one call over the 8 Choices, with a fresh uid field (a throwaway unique value) on each call, matching the noul cookbook setup.
What to look for: picked labels can flip inside a single condition, including TypeSafe, and conditions disagree with each other. In this run the LLM distribution settings repeat their plurality labels 87.5% to 100% of the time, compared with TypeSafe’s 90.8%. TypeSafe has lower mean probability variation than five of the six LLM distribution conditions; Haiku at temperature 0 varies less. Close probabilities still permit routing changes: TypeSafe flips on 2 of the 8 questions. For application decisions, we also require a top probability of at least 0.60; otherwise the result is uncertain and goes to human review. TypeSafe’s agreement then rises to 99.2%, with automatic labels on 74.2% of answers. We show the raw outputs and apply the same threshold to LLM probability conditions, keeping abstentions and changes visible.

Setup

then set TYPESAFE_API_KEY, ANTHROPIC_API_KEY, and OPENAI_API_KEY. This run uses jev-latest on the production API, sampled on 2026-09-11.

The state: a borderline user post, as JSON

The post below is built to sit on the fence. The language is heated and insulting, aimed partly at one person and partly at the argument and the community. It carries an off-platform invite (a link pulling people to another site), one prior strike on the account, and four user reports, and the threat-like wording is never cleanly phrased. There is no single obvious answer here, and that is the point: small wording differences should not randomly move the same post between enforcement paths. The LLMs get json.dumps(POST) in the prompt. TypeSafe gets the Python dict directly.

The rubric: 8 Choices

Each question has a key, a line of instructions, and a fixed label set. The labels within a question are mutually exclusive (exactly one applies), and each carries a short description. TypeSafe returns a picked choice plus a probabilities distribution over the labels. The LLMs are asked to use the same label sets, which keeps every row comparable.

How we ask

Each LLM call is one prompt holding json.dumps(POST), all 8 questions, and every allowed label. There are two answer formats. In distribution mode the model returns one JSON object per question with a probability on each label. In single-pick mode it returns one bare label per question, and our analysis puts all the probability mass on that label. The TypeSafe call is one system_one request over the same post and the same 8 Choices, returning one distribution per question. Every query also gets a fresh uid, a throwaway unique value that changes each run while leaving the post and rubric unchanged. It appears in the LLM prompt and as an extra field in the TypeSafe state. This setup cannot separate sensitivity to the irrelevant field from variation that would occur on identical requests. Each helper returns the answer, an estimated cost, and the round-trip latency.

Experimental Conditions

Experiment Grid

  • Each ✓ marks one condition with 15 repeats; — means the combination is not tested.
  • The default column sends no temperature argument: non-reasoning models use the API default, and reasoning models and TypeSafe run without a temperature setting.
  • Single-pick conditions return one label per question.
  • Temperature 0 is commonly suggested for repeatability, so we compare it with the API default.
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)

Costs below use the historical price assumptions in Setup, including the speed_latest rate for TypeSafe. They are not verified jev-latest prices or current billing amounts. One row is one full 8-question rubric call. time/call and cost/call average the 15 calls, and the vs ts_choice columns divide by the TypeSafe figures. The LLMs run in a 16-way pool.
In this run typesafe_choice has a mean round-trip latency of 114ms. The LLM conditions range from 826ms to 13.0 seconds per call under the concurrency settings above.

Plot: every sample’s decision as a heatmap

How to read it:
  • Outer row group: the question.
  • Inner row: the condition.
  • Column: one full rubric call.
  • Cell text: the application decision plus the probability on the top label.
  • Cell color: the label’s position within that question, so the same color all the way across a row means the same decision every time.
  • Gray uncertain: the top probability is below 0.60, so the case goes to human review.
  • Hatched n/a: the reply did not parse into usable labels (a parse failure).
  • Blank rows are just spacers.
Single-pick conditions keep their returned labels: they provide no uncertainty estimate.
output The clearer questions hold steady: target reads Person and severity reads High across the board. The borderline ones split across conditions: category, primary_risk, action, review_path, and link_handling. Some conditions also flip within their own 15 repeats. Before abstention, TypeSafe changes its top label on primary_risk (Harassment 11 times, Violence 4 times) and link_handling (RmLink 8 times, Brigade 7 times). Both rows now show uncertain throughout because their top probabilities are below 0.60.

Probability std dev

This looks at the full probability vectors, not just the picked label. For each condition we collect all 15 distributions for every question, take the standard deviation of each label’s probability across the repeats (how much it moves from run to run), then average those std devs over all labels and questions. We also report the single largest label std dev, and count parse failures separately. The table compares every probability-output LLM condition against TypeSafe. The single-pick rows are left out, since they emit hard labels rather than probability distributions.
In this run TypeSafe has a mean probability std dev of 0.0098 and a max single-label std dev of 0.0515. Haiku at temperature 0 has a lower mean std dev of 0.0012. The other five LLM probability conditions range from 0.0245 to 0.0543, about 2.5x to 5.6x the TypeSafe mean. Small changes can still switch the top label when two labels are close.

Plot: decision agreement with an uncertain outcome

Return uncertain when the top probability is below 0.60. For each probability-output condition and question, count the most common application decision, including uncertain, and divide by all 15 draws. Parse failures count against agreement. Each bar averages the score over all 8 questions, with the highest agreement first. Single-pick LLM conditions are excluded because they provide no uncertainty estimate.
output With the same 0.60 rule, Haiku at temperature 0 scores 100%, TypeSafe scores 99.2%, and the other LLM conditions range from 84.2% to 94.2%. TypeSafe returns uncertain on 25.8% of answers and acts automatically on 74.2%; Haiku at temperature 0 has no abstentions here. This measures repeatability, not correctness. The table below keeps raw agreement and abstention rates visible alongside the policy agreement shown in this chart.

Let uncertain probabilities produce an uncertain decision

A small probability change can swap two close labels. The application does not have to act on the winner: return uncertain when the top probability is below 0.60, and send that case to a human. At exactly 0.60, select the top label. This uses the returned probabilities, not the API’s separate confidence field, and adds no model calls. The threshold is an illustrative application policy, not a calibrated guarantee or a threshold chosen to maximize this run’s agreement. Choose production thresholds using labeled examples and the cost of incorrect actions and human review. We apply the same rule to every probability-output condition. Single-pick LLM responses have no probability estimate; their synthetic one-hot vectors cannot measure uncertainty, so they are excluded from the agreement chart and table.
policy agree counts uncertain as a decision; parse failures count against agreement. automatic is the share of all answers that select a label. conflicts counts questions with more than one concrete label across the repeats, ignoring abstentions. These measures describe repeatability and how often the application acts, not whether its actions are right. TypeSafe’s agreement rises from 90.8% to 99.2%, with 25.8% uncertain and 74.2% automatic. primary_risk and link_handling are uncertain on every repeat. category sometimes crosses the action threshold, alternating between Violence and uncertain. No question produces two different concrete TypeSafe labels. These numbers do not establish accuracy or superiority: Haiku at temperature 0 still has 100% agreement with no abstentions here.
output Abstaining can replace competing labels with the same human-review outcome. A probability near 0.60 can still move between a concrete label and uncertain. This policy does not make the model deterministic. The probability statistics and the table’s raw agree column still report the original model outputs.

Open it in the TypeSafe playground

The link below opens the same post and rubric in the playground: one post, the same 8 Choices, and TypeSafe jev-latest.
Open this post + rubric in the TypeSafe playground →