Decision models (Jev) in your app

Typed answers with probabilities, on the Mac or hosted, behind one API — new in 2.0

Most of us met language models as chatbots: you type, it writes back. A decision model is a different animal. It writes nothing. You give it a piece of text and a few fixed questions, and it answers each one with one of the options you allowed, plus how sure it is. TypeSafe's Jev made the idea popular in September 2026. This page starts with what that means if you're used to chatbots, then shows how the LocalLM Lab SDK runs decision models on the Mac and in the cloud, and how an app pairs one with the chat model it already has.

If you know chatbots, start here

With a chatbot, your app writes a prompt and gets back prose. When the app needs a decision — which team should handle this message, does this request need the web, is the user upset — the usual trick is to ask the chatbot and parse its reply. That works until it doesn't: the reply comes back as “Billing, probably”, or a paragraph, or a label you never offered, and nothing tells you how close the call was.

A decision model (a decider) skips the prose. Think of it as a reader filling in a form you designed: it reads one input and ticks one box per question.

InputQuestionAnswer
“Nothing loads since this morning, my whole team is stuck.” Which team should handle this? (billing / technical / account)technical 100%
Is the customer blocked from using the product right now?Yes 96%
Does the customer explicitly ask for money back?No 97%
JevDK asking the customer-support questions about the message 'I see two charges for my Pro plan on this month's statement. Please refund one of them.' Both the local Qwen3-4B decider and Featherless's hosted classifier answer team: billing, 100%, with a bar for each option (billing, technical, account), and urgent: No, 100%.
One input, typed answers: JevDK asks the same questions of a local decider (Qwen3 4B, 716 ms) and a hosted one (Featherless), and each answers with one of the options you allowed and a probability for every option. No reply text to parse.

Your app then acts on the answers in ordinary code: send it to the technical team, mark it urgent, don't open a refund. Side by side:

Chat modelDecision model
You sendA prompt, often a whole conversationOne input and a few typed questions
You get backText you have to read or parseOne allowed answer per question, with probabilities
Can it answer “something else”?Yes, any textNo: only the options you listed
Knowing how sure it isNot really (“I'm fairly confident…”)A number per option, which your code can put a threshold on
SpeedSeconds, longer for long repliesTens to hundreds of milliseconds for several questions
Where the work goesWriting the promptWriting the questions and testing them on real inputs

That last row is the real change. A chatbot is forgiving: a vague prompt still gets a plausible reply. A decider does exactly what its questions say, so small wording changes can move answers, and you only find out by trying the questions on many inputs with the right answers marked. The SDK's playground, JevDK, exists for that (its guide).

↑ Top

Three kinds of question

The SDK uses Jev's own names, so its documentation and ours line up:

All the questions about one input are answered together, in one call. There is no system prompt: whatever a question needs to know goes in the input or in the question itself.

↑ Top

What a decider is good at, and what it isn't

Good fits are the small, fixed choices inside an app:

Poor fits:

What if the decision needs knowledge about your product?

Say the decider routes bug reports to teams. It needs to know something about your software: which parts exist, and who owns what. But a decider only knows what's in the input and the questions; there's no background briefing to give it. So you hand it the knowledge in one of these ways:

Whichever you choose, test it the same way: real reports, the right answers marked, in JevDK. The JevDK guide has the code for each.

↑ Top

“How sure” is the hard part

The probabilities are what make a decider useful: act when it's sure, and when it isn't, do something safer — ask the user, ask a bigger model, take the cautious path. But a number is only useful if it means what it says. If a decider says 80%, it should be right about 80% of the time on answers like that. That property is called calibration.

What that looks like

Here is a test run in JevDK: the same questions asked about real customer messages, with the right answers marked under each cell. Each cell has two answers: ① is a small decider running on the Mac (Qwen3 4B), ② is a hosted one (Featherless). A tinted cell is where the two disagree.

JevDK's batch grid before calibration: five customer messages, each answered by a local Qwen3-4B decider (1) and Featherless's hosted classifier (2), with the correct answer marked under each cell. On 'Is the customer blocked?', the local decider answers No at 100% for 'Nothing loads, my whole team is stuck' and for 'the password reset link has expired, so I can't sign in'; both are marked wrong and outlined with the letter A. Featherless answers Yes at 100% on both, and No at 82% on the cancellation threat.
Before calibration.

JevDK can fit a calibration for the local decider from these marked answers: one adjustment per kind of question that makes its confidence honest. Here is the same grid with it applied:

The same batch grid after calibration. The local decider's two wrong answers, outlined with the letter A, now read No 91% and No 93% and are still marked wrong. Its right answers on the same question, outlined with the letter B, now read No 94%, No 94% and No 95%. Every answer that won before still wins.
After calibration (fitted on these 36 marked answers, as a demonstration; JevDK asks for 30 or more per kind of question for one you'd ship).

That's the lesson in one picture: a decider's confidence is only as good as the testing behind it. Every answer from the SDK says which kind of probability it is (Decision.fidelity), and until you've calibrated a local decider on your own marked answers, don't put tight thresholds on its numbers.

Calibrate once in JevDK, then use it in your app

What a calibration is, in plain terms: three numbers that tell your app how much to tone down (or turn up) a local decider's “how sure” figures, one number for each kind of question: yes/no, choice, and scale. You work them out once, on your Mac, in JevDK (the free app for designing and testing these questions, download below), from answers you've marked right or wrong. Your app never does that work; it only uses the three numbers.

Getting the numbers in JevDK:

  1. Switch to Batch, paste a few dozen real inputs under Inputs, one per line, and click Ask all. A grid appears: one row per input, one column per question.
  2. Mark the right answers: under each answer in the grid is a small mark… link. Click it and pick the correct answer for that input; it then reads “correct: billing” (say), and each answer gets a ✓ or ✗. Already have the right answers in a spreadsheet? Save it as CSV and use File → Import Answers (CSV)… instead (an input column, then one column per question).
  3. With 30 or more marked answers per kind of question, click Calibrate… above the grid, then Fit calibration. A window like this opens:
JevDK's Calibrate window. In the Fitted table's Temperature column, X marks the yes/no (Noul) value, 9.11, and Y the Choice value, 0.25. Below, the Copy code snippet shows the same numbers in place: DecisionCalibration(noulTemperature: 9.11 marked X, choiceTemperature: 0.25 marked Y, scoreTemperature: 1.0 marked Z).
JevDK's Calibrate window, after Fit calibration (two parts of it, stacked). Top: the Fitted table, where you read the numbers in the Temperature column. Bottom: the code under For your app, which Copy code copies, with the same numbers in place. X yes/no questions, Y choice, Z score (this example has no score questions, so Z stays 1.0: no change).

Putting them in your app. Pick one of two ways:

  1. Let them travel with your questions (easiest). When your questions are ready, JevDK can save them as a small file for your app (File → Export Questions…). That file also holds the three numbers. Add it to your app; when your app sets up its decider from the file, the numbers are used automatically. In code, that's OpenJevDecisionProvider(mlx:tunedWith:) with the loaded file.
  2. Put the numbers in your code yourself. Click Copy code in the Calibrate window and paste the result where your app sets up its decider: it already has X, Y and Z filled in, along with the system instructions you tested with, which you should keep (the numbers only fit those instructions).

Afterwards: the numbers fit the model and instructions you tested, so if you switch to a different model or change the instructions, fit them again in JevDK and re-export. Your app can tell it's using calibrated numbers: each answer's fidelity says .tokenScored(calibrated: true). Hosted TypeSafe answers don't need any of this; they come calibrated.

↑ Top

Where Jev comes from: further reading

The idea and its vocabulary come from TypeSafe. Good places to read more:

↑ Top

One API, local or hosted

The SDK asks the same questions the same way whichever decider answers. You choose the decider with a route, like a chat model:

DeciderWhere it runsModuleProbabilities
OpenJev (ours)On the Mac, on an MLX model you chooseInferenceRead off the model; calibrate them yourself
TypeSafe JevHosted, through OpenRouter (your key)RemoteFrom a model trained to decide
Featherless Simple JevHosted (your key, or a free rate-limited demo)RemoteRead off the model, like OpenJev
try lab.models.register(decision: OpenJevDecisionProvider(mlx: mlx))
lab.models.route(decision: "support", to: ModelID("openjev:mlx-community/Qwen3-4B-4bit")!)

let d = try await lab.decide(route: "support", state: .text(message), questions: [
    .choice("team", "Which team should handle this message?", criteria: [
        "billing": "payments, invoices and refunds",
        "technical": "bugs, outages and product errors",
        "account": "profile, login and subscription changes",
    ]),
    .noul("refund", "Does the customer explicitly ask for money back?"),
    .score("urgency", "How urgent is it?", criteria: ["can wait", "soon", "now"]),
])
d.choice("team")?.choice      // "technical"
d.noul("refund")              // 0.03
d.score("urgency")?.score     // 1.6

To use hosted Jev instead, register JevDecisionProvider(.openRouter(key:)) (or a Featherless preset) and point the route at openrouter:typesafe/jev-1.13; the questions and the code that reads the answers don't change. Hosted deciders are off until you register one, and the questions and input are sent to that service when you do. It works the other way round too: jev-serve answers hosted Jev's API from your Mac, so code written for hosted Jev can use OpenJev.

↑ Top

One model or two: the decider and your chat model

A decider rarely works alone. The pattern the SDK is built for: a decider makes the typed choices in a feature (does this need the web, which tool, is it sensitive), and a chat model writes the text. Both on the Mac, so the routing itself costs no privacy. That raises a practical question for a local app: one model in memory, or two?

Share one model

The decider can run on the chat model's own weights: route OpenJev and your chat route to the same MLX model, and only one copy is in memory. This is the low-memory option. It works when the chat model is dense (not MoE); a big chat model also makes a slower decider than a small dedicated one.

Keep two models resident

Usually better: a small dedicated decider (Qwen3 4B, 2.6 GB) next to the chat model you already use. The catch is memory. By default the SDK keeps one local model loaded, so a decider asked on every turn would push the chat model out and force a reload. One call sets the pair up:

lab.models.route(decision: "triage", to: ModelID("openjev:mlx-community/Qwen3-4B-4bit")!)
lab.models.route("writer", to: ModelID("mlx:mlx-community/Qwen3.8-27B-4bit")!)
let pairing = try await lab.models.pair(decision: "triage", generator: "writer")

pair(decision:generator:) checks that both models' weights together fit the memory budget (70% of the Mac's RAM by default), makes room for both in the model cache and loads them. If they don't fit it throws with the numbers and changes nothing. A shared model counts once; a hosted decider costs no memory, so only the chat model is loaded. Measured on a 64 GB Mac with a Qwen3 4B decider:

Chat modelPaired inModels in memoryNext decision
The same Qwen3 4B (shared)1.0 s159 ms, no reload
Qwen3.8 27B (15 GB)2.3 s263 ms, no reload
Apple's on-device model0.9 s162 ms

Work in progress. Two co-resident models work today, and these parts are still being worked on:

  • Keeping the pair loaded under memory pressure. When the system is short of memory, the model cache can still unload everything except the most recently used model, so the next call reloads the other one.
  • Smaller Macs. The measurements above are from a 64 GB Mac; a pair on a 16 GB Mac hasn't been measured yet.
  • Loading speed of vision-capable models. A text-only use of a model that also handles images currently loads its vision version, which was about 50% slower as a decider on Qwen3.8 27B.
↑ Top

From playground to shipped app

Because the questions are the work, the SDK carries them from testing to shipping intact:

  1. Write and try the questions in JevDK on real inputs, local and hosted deciders side by side.
  2. Mark the right answers on 50–150 real inputs (or import them as a CSV). Now every run is scored.
  3. Fix the questions that miss, compare models (each run can be appended to a results CSV), and fit a calibration for the local decider you pick.
  4. Export the questions for your app (File → Export Questions): a file (DecisionQuestionSet) with the questions exactly as tested, plus the model, version and calibration they were tested with. Your app loads it, so what ships is what you tested:
    let set = try DecisionQuestionSet(contentsOf: Bundle.main.url(forResource: "support.decisions", withExtension: "json")!)
    try lab.models.register(decision: OpenJevDecisionProvider(mlx: mlx, tunedWith: set))
  5. Or serve it to other programs. Not writing a Swift app? JevDK can also export a config for jev-serve, a small server that answers hosted Jev's API (the same requests OpenRouter and Featherless take) using your tested setup on the Mac. Code that already calls hosted Jev, in Python, JavaScript or anything else, just points at http://127.0.0.1:8746 instead: private, free, and the same answers your tests saw.
  6. Keep testing in CI: lab.evaluate runs your answer set through any decider and reports accuracy per question, so a model, SDK or wording change that breaks answers fails a test.
↑ Top

Start from working code

JevDK is the playground above, open source and built only on the SDK's public API: write questions, run them on a local MLX model and on hosted Jev, see every option's probability and the exact prompt or request, run a batch grid against your marked answers, calibrate, compare models and export for your app. Its developer's guide walks through the whole workflow in plain English, with a worked customer-support example. The API itself is in the SDK guide (“Decision models”), and the rest of the SDK on the SDK overview.

⬇ Download JevDK 0.2.2 macOS 27 on Apple Silicon · signed & notarized · includes jev-serve · verify checksum · source and releases · developer guide
↑ Top

Contact

Building with decision models, or hit a rough edge? neuron@thisbrain.ai or our Discord.

↑ Top