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).
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.
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).
- Chatbots like ChatGPT, plus reasoning models
- Returns: text, code, explanations of reasoning
- You don't know the answer's shape until you read it
- Returns: predefined options, levels, or yes/no, with a probability
- Doesn't write text, code, or explanations
- Model name in the API:
jev-latest - Text input only (no images, audio, or video)
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).
| Aspect | LLMs like ChatGPT | Jev |
|---|---|---|
| What it returns | Generated text | Preset options or levels + probability |
| Writes text or code | Yes | No |
| Good at | Text, code, extended reasoning | Classifying, scoring, yes/no questions |
| Adding questions | Slower: every answer is written out | Parallel, so wait time stays flat |
| What you pay for | Input and output tokens | Input 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.
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.
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
}
}
- choice: the chosen candidate →
technical - probabilities: one per candidate (they sum to 1)
- confidence: how concentrated they are → 0.78
- input_tokens / output_tokens: tokens used
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 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.
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.
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
instructionsandcriteria. - 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.
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.
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.
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.