The problem
Product catalogs, patent classifications, medical subject headings and file trees all have the same shape: thousands of leaves hanging off a handful of top-level branches. A choice question takes at most 255 options, so the whole taxonomy in one question is not on the table for anything of real size.
The tree gives you the way out. At any node, the question is only ever “which of these direct children”, and that is a small choice. Walk from the root and every step is a small decision made with the full document in view. TypeSafe’s hierarchical classification cookbook does this over the CPC patent scheme, the Shopify product taxonomy, MeSH biomedical subjects and a source-code repository.
Decomposing it this way buys more than a size workaround. You can count how often each node and edge gets traversed, and unit test a change to one branch without re-running the whole corpus. commit-miner applies the same tree-walking idea to repository history.
What the state looks like
The document stays the same at every level. Only the question changes, which is what makes the walk cheap to reason about: the model is never asked to remember where it has been.
{
"listing_id": "SKU-44812",
"title": "Suction cup cat hammock for windows",
"body": "Holds up to 12kg. Four industrial suction cups fix it to any glass pane so your cat can nap in the sun. Machine washable cover."
}
Keep it tight. Accuracy drops as the state fills with material the decision does not need, so strip boilerplate before you start walking.
The questions you ask
One choice per sibling set, with the option keys mapped so you can reverse them afterwards.
from typesafe_sdk import Choice, RetryPolicy, TypeSafeClient
client = TypeSafeClient(
model="jev-1.13.0",
retry=RetryPolicy(max_retries=5, backoff_initial=1.0, backoff_max=20.0),
)
def choose(state: dict, labels: tuple[str, ...]) -> dict[str, float]:
"""Return the probability distribution over one node's direct children."""
if len(labels) == 1:
return {labels[0]: 1.0}
keys = {f"c{i}": label for i, label in enumerate(labels)}
response = client.system_one(
state,
{"child": Choice(
instructions="Which direct child category best matches this document?",
criteria=keys,
)},
)
probabilities = response.answers["child"].probabilities
return {label: probabilities[key] for key, label in keys.items()}
The useful part of that response is not the winning label but probabilities, the full distribution over the options, which sums to 1. That is what turns a walk into a search: it tells you how close the runner-up was.
Short opaque keys like c0 keep long category names out of the request and give you a clean mapping back. The primitives guide covers what else a choice answer carries.
Decision policy
Greedy takes the highest-probability child and throws the rest away, which means one early mistake is unrecoverable. Beam search keeps the best K paths alive and scores each one by the geometric mean of its edge probabilities.
BEAM_WIDTH = 3
EPSILON = 1e-9
def extend(candidate: dict, label: str, probabilities: dict[str, float]) -> dict:
is_decision = len(probabilities) > 1
product = candidate["product"] * (max(probabilities[label], EPSILON) if is_decision else 1.0)
decisions = candidate["decisions"] + is_decision
return {
"path": candidate["path"] + (label,),
"product": product,
"decisions": decisions,
"score": product ** (1 / decisions) if decisions else 1.0,
}
Taking the nth root normalises for depth, so a shallow leaf and a deep one can be compared fairly. Every frontier path runs as its own question in the same request, and since there is no speed cost for additional questions, a beam of three costs about what a beam of one does in wall-clock time.
Then gate on separation: the top path’s score divided by the second path’s. A ratio near 1 means two branches fit the document about equally well, which is a review queue item rather than a classification. A large ratio means the walk found a clear winner. Your code owns the cut line and the review queue, and it should cap depth so a malformed tree cannot loop forever. The cookbook uses a beam width of 3 and a depth cap of 12.
Beam search is worth the complexity when the top of the tree is ambiguous. On TypeSafe’s four published examples, beam search with K=3 matched the expected leaf 4 times out of 4 while greedy matched 2 out of 4, with the recovered cases being the patent and the product taxonomy. That is four examples, not a benchmark, so measure your own tree first. The cascade recipe has the runnable version.
When not to use this
A flat label set under 255 options does not need any of this. One choice question answers it in one request, and every extra level is another place for an early error to send the walk down the wrong branch.
Trees whose branches are told apart by numbers rather than by meaning will struggle. Model years and version numbers are comparisons, not judgments, and Jev handles semantic distinctions far better than numeric ones. Route those levels in code and let the model handle the ones that turn on meaning.
Watch for nodes whose children overlap or contradict each other, since contradictory criteria are a documented failure mode. If two siblings could both be right, the probability splits and every path below inherits the doubt.
Multi-hop branches are a risk too. A node asking “which department owns the team that maintains this file” is several hops of indirection, so resolve it in code and ask about what you resolved.
A long document costs accuracy at every level, not just once, because the same state goes out at each step. Filter first, then walk.
FAQ
How many requests does one document take?
One per level for greedy, or one per level for beam search too, since the frontier paths go out as parallel questions in a single call. A twelve-deep taxonomy is twelve round trips. TypeSafe’s own measurement puts end to end latency at 70ms to 500ms per request, so a deep walk is seconds rather than minutes.
What if a node has more than 255 children?
Split it. Insert an intermediate grouping level in your own tree so no single question exceeds the documented limit, grouping siblings by whatever distinction reads most clearly in words. The same two-stage narrowing appears in structured extraction when a document yields more candidate spans than a choice can hold.
Should I send the child descriptions or just the names?
Send descriptions wherever the names are ambiguous, since criteria are read as an extension of the instruction and a bare label leaves the model guessing what it covers. Names alone are fine near the root, where the branches are obviously different. Deep in a taxonomy, where siblings differ by one qualifier, the description is doing most of the work.
How do I find where my errors come from?
Record the full distribution at every node, not just the chosen child. Then group your known misclassifications by the node where the path first diverged from the correct one. That usually points at one ambiguous sibling pair or one badly worded description, which you can fix and re-test without touching the rest of the tree.
Examples in the wild
Ran Jev against an existing classifier eval that previously used Gemini 2.5 Flash Lite
Vercel CTO reports Jev saturated an existing classifier eval that had used Gemini 2.5 Flash Lite and ran about 6x faster. Original post; TypeSafe's quote-tweet is a separate item.
SetFit: efficient few-shot text classification
Fine-tunes Sentence Transformer embeddings plus a lightweight head for high-accuracy classification from a handful of labeled examples, with no prompting. The standard cheap alternative to an LLM classification call when the label set is fixed. Star count is GitHub's rounded display figure.
kyotofin/tax-doc-classifier
A TypeScript classifier that sends each PDF page's text to Jev as a choice over 261 IRS forms and 7 page kinds, with a second call only for five corporate forms and their schedules. Its eval reports 0 wrong pages on 314 filled TaxCalcBench forms at about $0.001 per page, 34x cheaper and 6x faster than the Sonnet pipeline it replaced.
commit-miner
Rust CLI that classifies Git commit diffs and messages with Jev, labelling bug fixes, security fixes with CWEs, and change types. Scans a local repo or remote URL and exports HTML or CSV reports.
reachjalil/jev-tree
Recursive Choice over a taxonomy. Jev can only list 255 options in one choice question, so jev-tree walks a JSON shape and calls it once per level or partition, letting you select among thousands of leaves.
rashedInt32/jev-mcp
MCP server exposing Jev as typed judgment tools (classify, score, check, batched ask) and shipping as a Claude Code plugin. Runs via npx jev-mcp.
Meet Jev: The AI Built to Make Decisions
Tests Jev on the creator's own inbox, classifying 100 then 1,000 emails by category, priority, spam and reply-needed at around 200ms average response time. Ends by checking the actual bill.
Jev 1.13 on OpenRouter
Model page for Jev 1.13 on OpenRouter, reachable through OpenRouter's OpenAI-compatible endpoint. Listed at $0.042/M input, $0/M output, 32K context.
Jev: TypeSafe's System One Model Explained
Explainer covering how Jev differs from token-by-token LLMs, the three typed primitives, and RLCD. Reports roughly 68% accuracy on TypeSafe's four-workflow benchmark, comparable to GPT-5.6 Terra, and notes Jev is unsuitable for open-ended generation.
@ai-sdk/typesafe-ai (Vercel AI SDK provider)
Official AI SDK provider exposing Jev through the experimental evaluate API in AI SDK 7. Supports Choice (1-255 options), Score (2-10 ordered levels) and Boolean/Noul, batching all questions in one request against the same state.
Cookbook: Classification using confidence
Uses the confidence value returned with Choice and Score answers to decide when to act automatically and when to escalate.
Cookbook: Hierarchical classification
Classifies into a deep taxonomy by walking the tree one Choice question at a time instead of flattening every leaf into one list.