Skip to content

Typed decisions (judge)

Every tab below is an excerpt from a real file in the repo, and every one of these commands was run to produce the output at the bottom of this page. Each one picks its backend from the environment: TYPESAFE_API_KEY calls TypeSafe’s own API, OPENROUTER_API_KEY calls the same wire through OpenRouter’s gateway, and with no key at all they replay one recorded decision through the static backendno network, no credential, no cost. The snippets below show the TypeSafe branch, which is also the library’s default configuration.

language file command
JavaScript js/examples/judge.ts cd js && npm run build && npm run example:judge
Python python/examples/judge.py cd python && uv run python examples/judge.py
Go golang/examples/judge/main.go cd golang && go run ./examples/judge
Java java/…/examples/Judge.java cd java && ./gradlew runJudge
C# csharp/examples/Toolnexus.Examples/Judge.cs cd csharp/examples/Toolnexus.Examples && dotnet run -- judge
Elixir elixir/examples/judge.exs cd elixir && mix run examples/judge.exs
Clojure clojure/examples/src/examples/judge.cljc cd clojure/examples/clj && clojure -M -m examples.judge

The state is one support ticket. The three questions ask three different kinds of thing about it, in one round trip.

The map keys (wants_money_back, department, urgency) are yours, and they are addressing, not content — they are never transmitted to the model, so a key may be a tool, skill or agent name verbatim.

import { createClassifier, noul, choice, score } from "toolnexus"
const QUESTIONS = {
wants_money_back: noul("Is the customer asking for money to be returned?"),
department: choice("Which desk should own this ticket?", {
billing: "own it here when the problem is money that moved: a duplicate charge, a wrong invoice, a refund owed",
shipping: "own it here when the problem is a physical parcel: a late delivery, a package damaged in transit",
technical: "own it here when the problem is the product itself: a login that fails, a feature that errors",
}),
urgency: score("How fast does this ticket need a human?", [
"the customer is working normally and is waiting on an answer",
"the customer is inconvenienced and will chase if nobody replies today",
"the customer is blocked from working right now and every hour costs them",
]),
}
const judge = createClassifier({
baseUrl: "https://api.typesafe.ai/v1", // TypeSafe's own API — also the library default
model: "jev-latest",
apiKeyEnv: "TYPESAFE_API_KEY", // the NAME of an env var, never the value
onMetric: (ev) => ev.event === "classifier.warning" && console.log("warning:", ev.warning),
})
const d = await judge.evaluate(TICKET, QUESTIONS)
const want = d.noul("wants_money_back")
const dept = d.choice("department")
const urg = d.score("urgency")

Reading an answer as the wrong type is an error, not a surprise: the typed accessors fail loudly (an exception, an {:error, _}, an err) rather than handing you a zero.

One live TypeSafe decision, through the JavaScript port:

backend: systemone via api.typesafe.ai (live)
model answering: jev-1.13.0
wants_money_back: 0.99 (a noul carries NO confidence — the number IS the answer)
department: billing p={"technical":0,"billing":1,"shipping":0} confidence=1
urgency: 0.48 of 0..2 p={"0":0.52,"1":0.48,"2":0}
level 0: the customer is working normally and is waiting on an answer (a score MAY fall between levels)
calibrated: true
nearUniform(department): false
usage: 516 in / 72 out / cost: not reported by this backend

Which key you set changes one line, and one field

Section titled “Which key you set changes one line, and one field”

TYPESAFE_API_KEY goes to TypeSafe’s own API; OPENROUTER_API_KEY goes to the same wire through OpenRouter’s gateway. They are equivalent in latency — 339 ms / 449 ms (p50 / p95) against 351 ms / 400 ms, warm and interleaved, which is a tie — so pick on dependencies, not speed: a gateway in the path versus a first-party key. The one functional difference is usage.cost, a gateway field that TypeSafe does not return; that is why the line above says cost: not reported by this backend rather than $0, and why a cost-based budget only works through the gateway. The backends page has the full comparison.

With no key at all the first line reads backend: static (recorded — set TYPESAFE_API_KEY or OPENROUTER_API_KEY to go live), the model answering is the recorded typesafe/jev-1.13-20260917, and the usage line carries the gateway’s $0.000021672 — the same decision, replayed.

Three things in that output are worth a second look.

  • urgency is 0.48. Not 0 and not 1: the model is genuinely split between “waiting on an answer” and “will chase today”, and the number says so. p={"0":0.52,"1":0.48} says it again, with the disagreement visible instead of averaged away. It was 0.49 on the recording and 0.48 live — the backend is non-deterministic, which is why only static may be asserted against.
  • calibrated: true — these probabilities came from a calibrated backend, so a threshold tuned here transfers. An llm-style backend reports false, and your thresholds do not carry over.
  • nearUniform(department): false — the model had something to rank on. true would be the symptom of an encoding that told it nothing.

That is why every option in the example above carries a real sentence, why all three use the same template (“own it here when the problem is X: a, b, c”), and why the host does the counting and hands over the conclusion rather than shipping raw numbers.

The encoding rules has the four rules with the measurement behind each, the degenerate-criteria warning the library emits when you get this wrong, and the precise limits of nearUniform, confidence and calibrated as health signals.

page what it answers
Typed decisions what a Classifier is, the three question types, the Decision shape
The encoding rules what to write inside a question, with the measurements
Backends & configuration systemone, llm, custom, static, and every option
Measured on a live backend latency, cost, non-determinism, and the one control that can fail