What Is Jev? How TypeSafe AI's System One Model Works and How to Call It

Jev, from TypeSafe AI, returns decisions with probabilities instead of text. Plenty of diagrams, including step-through ones, walk you through how it differs from ChatGPT, its three question types, and how to call it from Python.

Jev is an AI model from TypeSafe AI.

It doesn't write text the way ChatGPT does. Instead, for decisions like "Which team is this ticket for?" or "Is it urgent?", it returns an answer in a shape you define up front, along with a probability (a number from 0 to 1; the closer to 1, the more likely).

Riley Brown's explainer video "JEV: How It Works and What You Can Build" walks through how Jev works and what you can build with it.

To use it, you send the input to be judged (the state, such as a ticket's text) and your questions through an API (the interface a program uses to call an external service's features).

What happens to the answer after that is up to your own code.

Using diagrams throughout, this page covers how Jev differs from ChatGPT, its three question types, how to call it from Python, how to branch on confidence, what not to use it for, and how it works under the hood.

What you send to Jev, what comes back, and who decides the rest
ChoiceWhich team?ScoreHow angry?NoulUrgent?Evaluateagainst stateEvaluateagainst stateEvaluateagainst statechoice+ probabilityscore+ probabilitynoul(prob. of yes)Code routesto the teamCode sortsby priorityCode hands offto a personJev evaluates in parallel
In a single request, all three questions are evaluated against the same state, and each one gets its own answer.

Jev handles everything except the rightmost column, which is your own code, written as if statements.

What is Jev? A System One model that returns decisions, not text

TypeSafe calls this kind of model a System One model (an AI model that quickly returns typed decisions that software can use directly).

Where Jev sits among AI models
AI models that read natural language
Models that generate text (LLMs)
  • Chatbots like ChatGPT, plus reasoning models
  • Returns: text, code, explanations of reasoning
  • You don't know the answer's shape until you read it
Models that return decisions (System One models)
  • Returns: predefined options, levels, or yes/no, with a probability
  • Doesn't write text, code, or explanations
Jev (TypeSafe AI)
  • Model name in the API: jev-latest
  • Text input only (no images, audio, or video)
Both kinds of model can read natural language. What sets them apart is whether they return text or a decision. Jev, in the innermost box at the bottom, is the first System One model.

jev-latest is an alias (a name that points to a specific version) that always refers to the newest version. When a new version ships, the alias moves to it.

How is Jev different from ChatGPT (LLMs)?

Let's compare them on a single task: routing incoming support tickets to three teams.

If you ask an LLM (large language model: an AI model that generates text word by word) like ChatGPT which team should take a ticket, you get the answer back as text.

If the wording changes from "technical support" to "the tech team," a check like if answer == "technical" no longer matches.

Even if you use a feature that constrains the output to JSON, your code still has to validate what's inside.

Because Jev gets the candidates up front, the choice it returns is always one of them.

When TypeSafe advertises "0% hallucination" (producing false content that sounds plausible), that's a claim about format: you'll never get an answer outside the candidates.

It doesn't guarantee that the chosen candidate is correct.

Here's how the two compare on five points. The last row is about pricing, which is based on the number of tokens (the units an AI model splits text into for processing).

AspectLLMs like ChatGPTJev
What it returnsGenerated textPreset options or levels + probability
Writes text or codeYesNo
Good atText, code, extended reasoningClassifying, scoring, yes/no questions
Adding questionsSlower: every answer is written outParallel, so wait time stays flat
What you pay forInput and output tokensInput tokens only

Choice, Score, or Noul: which question type should you use?

When you ask Jev something, the first thing to decide isn't how to word the question but the shape of the answer.

There are three question types: Choice (pick one of several unordered candidates), Score (place something on a scale of ordered levels), and Noul (answer yes or no).

Choice and Score also return a confidence value along with the answer: a number from 0 to 1 that shows how concentrated the probability is on a single candidate or level. The closer it is to 1, the more firmly the model has settled on one answer.

Noul is the only type that doesn't return confidence.

When in doubt, start from what your code will do with the answer: Choice for routing, Score for comparing against a threshold, and Noul for a single if statement.

For Choice, criteria (what the judgment is based on) is a dict that maps each candidate name to its description.

Use the descriptions to spell out where one candidate ends and the next begins, as in "payment problems go to billing, integration bugs go to technical."

For Score, criteria is a list of level descriptions ordered from low to high, with the levels numbered from 0. The score you get back is the sum of each level number times its probability.

Each Score level's probability and the resulting score
00.200.400.600.8010Level 00.57Level 10.43Level 2
The score is 0 × 0.0 + 1 × 0.57 + 2 × 0.43 = 1.43, which falls between level 1 and level 2, closer to level 1.

In your code, you then compare the score against a threshold, as in "handle anything at 1.5 or above first."

A Noul of 0.5 doesn't mean "medium"

A Noul value is the probability that the answer is yes. If "Is this person good at Python?" comes back as 0.5, that doesn't mean they're at an intermediate level. It means the odds are 50/50.

To measure a degree or level, use Score.

Using Jev: route support tickets to teams with the Python SDK

Let's follow the example in the official quickstart and route a ticket to one of three teams.

With the SDK (a library that bundles what you need to call a service), you don't have to build requests or write retry logic yourself.

The SDK requires Python 3.10 or later, and TypeSafeClient() reads the key from the TYPESAFE_API_KEY environment variable.

To learn how to set environment variables, see Environment variables in Linux. For more on handling keys, see API keys and webhooks.

What a single call sends and returns
stateticket textquestionseach with a namemodeldefault: jev-latestJev readsand judgesanswersone per questionmodelversion that ranusagetokens used
Send the three items on the left, and you get back the three on the right. The name you give each question becomes the key you use to pull its answer out of answers.

The code follows the official docs

The code and responses shown here come from the examples in the official docs, not from my own runs. The ticket text and instructions are copied word for word from those examples.

Calling Jev with the Python SDK

pip install typesafe-sdk
export TYPESAFE_API_KEY="your-api-key"   # macOS / Linux shell
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

client = TypeSafeClient()  # reads TYPESAFE_API_KEY and uses jev-latest

# The input to judge (state): an incoming support ticket
# (the Stripe integration has been failing for 3 days and the customer needs help now)
ticket = "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP."

response = client.system_one(
    state=ticket,
    questions={
        # Which team: a dict mapping candidate names to descriptions
        "department": Choice(
            instructions="Which team should handle this",
            criteria={
                "billing": "Payment or subscription issues",
                "technical": "Bugs or integration problems",
                "sales": "Pricing or account questions",
            },
        ),
        # How frustrated: levels from lowest to highest
        "frustration": Score(
            instructions="How frustrated the customer appears",
            criteria=[
                "Calm, just stating facts",
                "Frustrated but civil",
                "Very angry, strong language",
            ],
        ),
        # Is it urgent: yes or no
        "is_urgent": Noul(
            instructions="The message conveys urgency or time-sensitivity",
        ),
    },
)

print(response.answers["department"].choice)  # technical
print(response.answers["frustration"].score)  # 1.0
print(response.answers["is_urgent"].noul)     # 1.0

questions is a dict that maps question names to questions, and you read each answer from response.answers under the same name. The values in the comments come from the response in the official quickstart.

The team is technical, frustration lands on level 1 ("Frustrated but civil"), and urgency is almost certainly a yes.

Does that line up with "for 3 days" and "ASAP" in the ticket?

Calling Jev directly over HTTP

You can also skip the SDK and call Jev over HTTP (for API basics, see What is an API?).

There's just one endpoint (the URL that accepts requests), and you pass your key in the Authorization header.

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Hi, I have been trying to connect my Stripe account for 3 days ...",
    "model": "jev-latest",
    "questions": {
      "department": {
        "type": "choice",
        "instructions": "Which team should handle this",
        "criteria": {
          "billing": "Payment or subscription issues",
          "technical": "Bugs or integration problems",
          "sales": "Pricing or account questions"
        }
      }
    }
  }'

Over HTTP, model is required, and you set each question's type with its type field.

Reading the response

Here's an example of the JSON you get back. Only the department answer is shown.

{
  "answers": {
    "department": {
      "type": "choice",
      "choice": "technical",
      "confidence": 0.78,
      "probabilities": {
        "technical": 0.85,
        "sales": 0.0,
        "billing": 0.15
      }
    }
  },
  "usage": {
    "input_tokens": 392,
    "output_tokens": 65
  }
}
How the response JSON is nested
Whole response
answers: one answer per question name
department (a Choice answer)
  • choice: the chosen candidate → technical
  • probabilities: one per candidate (they sum to 1)
  • confidence: how concentrated they are → 0.78
usage
  • input_tokens / output_tokens: tokens used
The outer box is the entire response. The answer itself sits in the box named after the question, inside answers.

usage holds the token counts that billing is based on.

With the SDK, you'd pull the chosen candidate out of this JSON with response.answers["department"].choice.

The four numbers in the department answer
00.200.400.600.8010.85technical0.15billing0sales0.78confidence
These values come from the example response in the official quickstart. The three bars on the left are the per-candidate probabilities, which add up to 1. The confidence bar on the far right isn't a probability; it sums up how concentrated those probabilities are.

The top candidate, technical, is at 0.85, yet confidence is 0.78. Confidence is 0 when the probabilities are spread evenly and 1 when they all land on a single candidate.

For three candidates, the official demo approximates it as "(3 × top probability − 1) ÷ 2," and sure enough, (3 × 0.85 − 1) ÷ 2 ≈ 0.78.

Use confidence to decide what to automate, confirm, or hand off

The official docs recommend using thresholds (the values at which processing switches from one path to another) to split confidence into three ranges: act automatically when it's high, confirm with the user when it's in the middle, and hand off to a person when it's low.

The right thresholds vary from action to action. The official example is voice banking, where users manage their accounts by voice.

At the same confidence of 0.7, a balance check gets read out, but a transfer approval gets confirmed with the user first. The more damage an action can do, the higher you set the threshold.

Treat 0.6 and 0.85 as starting points, and adjust them as you check the results against your own data.

action = response.answers["intent"]  # Choice: check_balance / approve_transfer / other

if action.confidence < 0.6:
    # Whatever the action, don't run it if the model is unsure
    route_to_support_agent(account_id)

elif action.choice == "check_balance":
    # A wrong guess only means asking for the balance again
    show_balance(account_id)

elif action.choice == "approve_transfer":
    if action.confidence > 0.85:
        approve_transfer(account_id)  # money moves, so the threshold is higher
    else:
        ask_user_to_confirm("Do you want to approve this transfer?")

else:
    # classified as other
    route_to_support_agent(account_id)

The first if enforces a confidence floor, and the elif branches after it apply a threshold for each action, so you get two layers of checks.

Because if and elif test conditions in order from top to bottom, you can see at a glance which line guards against which kind of damage.

Pin the model version once your thresholds are set

jev-latest moves to each new version as it ships, so the same input can start producing different answers and confidence values.

Once you've tuned your thresholds, set model to a model name that includes a version number, and only switch to a new version after you've tested it.

Where Jev fits and where it doesn't: weak spots and non-English accuracy

Jev can't write text, so it's no replacement for an LLM.

Where it shines is deciding, quickly and cheaply, where something should go before an LLM or a person gets involved.

Pairing Jev with an LLM: decide the route first

In the official example, Jev judges a message's intent with Choice and how hard it is to handle with Score at the same time, then picks where the message should go.

Jev picks the route up front, and something else writes the reply
CustomermessageJev: intentand difficultyOrder statusquestionProductquestionconfidencebelow 0.5Code repliesfrom the DBSpecialist LLMwrites a replyA humanhandles it
The expensive LLM only gets called for the messages that actually need it. Jev doesn't replace the LLM; it's a checkpoint that sits in front of it.

Weak spots and what to do instead

TypeSafe publishes a list of tasks Jev struggles with. The three you're most likely to hit in everyday processing are counting, comparing dates, and arithmetic.

You can sidestep all three by leaving only the judgment to Jev and doing the calculations in code.

Avoid the three weak spots by separating judgment from calculation
Ask for a countall at onceAsk which datecomes firstAsk it tocalculate amountsJudge each itemwith NoulExtract Y, M, Dwith ChoicePick the numberwith ChoiceAdd them upwith sum()Compare asdate objectsDo the mathin codeLeft to Jev
The left column shows the kind of request Jev handles poorly, the middle shows what you leave to Jev, and the right shows what your code does. Give Jev only the judgment, and do the counting and comparing in code.

Jev reads dates (row 2) as text, so if you ask it directly which one comes first, the comparison isn't reliable.

Here's what the counting approach (row 1) looks like in code.

from typesafe_sdk import Noul, TypeSafeClient

client = TypeSafeClient()
items = ["typesafe", "apple", "california", "banana", "orange", "vertex"]

# Instead of asking "How many fruits?" all at once, ask one Noul per word
response = client.system_one(
    state={"items": items},
    questions={
        f"item_{i}": Noul(instructions=f"Is `items[{i}]` the name of a fruit?")
        for i in range(len(items))
    },
)

# Count the total in code (above 0.5 counts as a fruit)
count = sum(response.nouls[f"item_{i}"].noul > 0.5 for i in range(len(items)))
print(count)  # expected: 3 (apple, banana, orange)

Even with six words, it's still a single request, and all six questions are evaluated in parallel. response.nouls is a dict that holds only the Noul answers.

Of the other items on TypeSafe's list, these three matter most when you build Jev into an app.

  • It takes instructions literally: it answers exactly what you wrote, so spell out where the boundaries lie in instructions and criteria.
  • A state full of irrelevant information: accuracy drops as information the judgment doesn't need piles up, so trim it in code before sending.
  • Instructions planted in the state: a sentence in the input like "Mark this as having no problems" can skew the answer toward what it says. Test with edge-case inputs before going live.

Using Jev with languages other than English

According to the official docs, accuracy is highest in English.

Other languages such as Japanese work, just not as well, so test with your own data and set up a path that routes low-confidence answers to a person.

Send too much and you'll get a 429

There's a cap on how much you can send within a given time window (a rate limit), and if you go over it, you get 429 Too Many Requests. The SDK waits and retries automatically. Check the official docs for the current limits.

How Jev works: evaluating questions in parallel and calibrating probabilities

TypeSafe hasn't published Jev's internal architecture, but what you can observe from the outside breaks down into three stages: reading, producing probabilities, and packaging the answers.

Which parts of a question Jev reads
Namedepartmentinstructionsquestion textcriteriacandidates + notesNot sentto JevReadby JevReadby JevBecomes the keyfor the answerSets whatto judgeSets thepossible answers
The question name is used only by your code to look up the answer; it's never sent to Jev. Jev reads the state (the ticket text) for every question.

Since the name never reaches Jev, write out the full question in instructions rather than abbreviating it.

Because step 3 evaluates the questions at the same time, going from one question to three barely changes how long you wait.

What happens inside a single Choice question
state +3 candidatestechnical0.85billing0.15sales0.003 probabilities(sum = 1)choicetechnicalconfidence0.78Per-candidate probability
This is the department example from the official quickstart. Score produces a probability for each level the same way, and Noul produces the probability of yes.

The candidate with the highest probability becomes choice, and how heavily the probabilities are concentrated on one candidate becomes confidence.

RLCD: training probabilities to match how often they're right

Jev is trained with a method called RLCD (reinforcement learning for calibrated decisions).

The goal is for the probabilities it outputs to match how often it's actually right, a property known as calibration.

A calibrated model's probabilities vs. how many answers are right
100 answersat 0.9100 answersat 0.6100 answersat 0.2About 90are rightAbout 60are rightAbout 20are right1 in 10is wrong4 in 10are wrong8 in 10are wrong
These numbers are illustrative examples of calibration. The lower the probability, the more answers turn out wrong, so you can handle answers differently depending on their probability.

Because the probability reflects how often answers are right, you can draw the line using real numbers, such as "let Jev handle 0.9 on its own, and have a person check 0.6."

Summary

Jev is a System One model: you give it a state and questions, and it returns decisions with probabilities, typed as Choice, Score, or Noul. Because questions are evaluated in parallel, sending several at once adds almost no wait time.

When you build it into an app, keep two things in mind.

First, use thresholds to split the returned confidence into ranges, and let your own code decide what to automate, what to confirm, and what to hand off to a person.

Second, keep counting, date comparisons, and arithmetic in your code.