← All posts

Jev: a model that picks instead of writes

A small figure facing an oversized bank of levers, with exactly one lever thrown.

Most of the AI in the apps we build writes text: a reply, a summary, some JSON. But a lot of what our code needs is smaller than that. Which team gets this ticket? Is this message about an order? Which of these five tasks did the user mean? For those we've been paying a chat model to write an answer we then parse, and waiting seconds for it.

TypeSafe's Jev is built for that smaller job. It never writes text. You send it an input and questions, and it returns numbers.

Three kinds of question

  • Yes or no. Returns the probability of yes. Your code sets the cutoff.
  • Pick one. Picks from a list you give it (tool names, item IDs, teams) and scores every option. It can't answer outside the list.
  • Score. A point on a scale you define, for example 0 (a simple lookup), 1 (needs a person), 2 (needs a senior).

What a call looks like

Say a support ticket comes in and you need to know which team gets it, whether the customer wants money back, and how urgent it is. With TypeSafe's TypeScript SDK that's one request with three questions:

import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();

const { answers } = await client.systemOne({
  state: { message: "Charged twice for invoice 1042, please fix today." },
  questions: {
    team: choice("Which team should handle `message`?", {
      billing: "Charges, invoices, refunds",
      technical: "Bugs, crashes, outages",
      other: "Anything that fits neither team",
    }),
    wantsRefund: noul("Does `message` ask for money back?"), // noul = yes/no
    urgency: score("How urgent is `message`?", [
      "Can wait a week.",
      "Should be handled today.",
      "Blocking the customer right now.",
    ]),
  },
});

Nothing comes back as prose. You get typed numbers and one of your own keys:

answers.team.choice;        // "billing", typed as one of your keys
answers.team.probabilities; // e.g. { billing: 0.94, technical: 0.04, … }
answers.team.confidence;    // e.g. 0.91
answers.wantsRefund.noul;   // P(yes), e.g. 0.88
answers.urgency.score;      // e.g. 1.2: past "today"

So the next lines are ordinary code:

if (answers.team.confidence < 0.5) return sendToTriageQueue(ticket);

switch (answers.team.choice) {
  case "billing":
    if (answers.wantsRefund.noul > 0.8) flagForRefundApproval(ticket);
    return assign(ticket, "billing", { urgent: answers.urgency.score >= 1.5 });
  case "technical":
    return assign(ticket, "technical", { urgent: answers.urgency.score >= 1.5 });
  case "other":
    return sendToTriageQueue(ticket);
}

No prompt asking for "JSON only", no parsing, no "what if it answers Billing team". The model can only return a key you defined, and TypeScript knows which keys those are.

Responses come back in roughly a tenth of a second, so the call sits inline in a request instead of in a background job. TypeSafe lists the price at $0.042 per million input tokens, output free, and says itself it "can't prove it isn't subsidized." Don't build a business case on that number staying put.

The useful mental model: this isn't a chatbot with tools. It's a line in your code that returns a decision, and the next line is a switch statement.

Patterns worth taking

Pass IDs, never the user's words

The user's words decide which option is picked. Only an ID from your own list reaches the function. "Move the invoice for Acme to Friday" becomes a call with the action from your action list and the invoice's ID from your candidates. The free-text date goes to a parser, not to the model. That's why you can trust the call: it can't contain anything you didn't put on the menu.

Ask every question at once, read the ones that apply

One request can carry many questions about the same input: which action, which record, which teammate, how urgent, is this a complaint. They all come back together, at about the cost of one. Your code reads the action first, then only the answers that action needs. TypeSafe's docs call this speculative fanout.

The catch: each question is answered on its own. The model will still score "bug severity" on a billing ticket, so your code has to know which answers apply.

Route on confidence, with a bar per action

Don't act on the top score directly. Use confidence: how far the top score sits above an even split. Then:

  • below one floor, a person handles it;
  • above the floor, each action gets its own bar, set by the cost of a mistake. Showing an order status needs nothing extra; issuing a refund needs a high bar, or a confirmation.

TypeSafe calls this confidence routing. Confidence drifts a little between calls on the same input, so set your bars from real traffic and keep them clear of where everyday messages land.

It always picks something

A pick-one question returns one of your options, even when none fits. Ask for an action that isn't on the menu and you get the nearest one that is. The fix is a better menu: add the missing option, add a "none of these" option, and ask the user when confidence is low.

Keep the menu short, narrow in code first

Long menus get expensive and fuzzy: more options means a longer request and more options that sound alike. Rule of thumb:

JobWho does it
A short list of options you can write downThe decision model
A list that grows with your dataSearch or filter in code first, then the model picks from the few left
Exact values like dates and amountsA parser
Anything that must be right every timePlain code
New textA chat model
Low confidenceAsk the person

What they want, and how hard it is

One request, two questions: the intent and its complexity. Three lines of code route it: simple cases to the right queue, complex ones to a senior, low confidence back to the customer with a question. TypeSafe calls this intent routing, and it's the shape of most triage we build.

Score each quality separately, weigh in code

To rank leads, products or candidates, score each quality on its own scale and combine the scores with weights in code. A different user or a different goal gets different weights, with no new requests, and you can always explain why something ranked first. TypeSafe calls this composite scoring.

Start with the model, then replace what repeats

Put a model call at a decision and log every pick. When the log shows the same input getting the same answer over and over, replace that slice with a rule or a cached answer in front of the call, and let everything else fall through. You start with inference everywhere and end with plain code wherever the answer kept repeating.

Before you ship

  1. Add an "other" option and gate on confidence.
  2. Answers don't see each other. If two depend on each other, combine them in code.
  3. The model knows only what's in the request. Send current prices, lists and user data.
  4. TypeSafe's docs say it doesn't count, do arithmetic or handle dates reliably. Let it pull out the parts and do the maths in code.
  5. Test your confidence bars on real messages.
  6. Option order can flip a close call. Shuffle the menu while testing.
  7. Pin the model version. jev-latest moves, and scores move with it.

What I'd try first

Almost every system we ship has a triage step that's either a human reading a queue or a chat model call that's slow enough to be batched overnight: support tickets, inbound leads, applications, document intake. That's exactly the "pick one from a short list, with a confidence" shape. I'd start with one of those, log every pick, and replace what repeats.

If you'd rather not depend on a closed model, Jared Palmer's Kev is an open-source (Apache-2.0) model built the same way, and it speaks Jev's API.

Has anyone already put Jev or Kev behind a real queue? Tell me what broke.

Bring the napkin sketch.

Thirty minutes with a full-stack engineer, plus a short written report that's yours to keep.

30 minGoogle Calendarno sales deck

Prefer to write? Send us a message →