How to Use Jev in Python: Choice, Noul, Score + OpenAI Comparison
Most AI models are built to write. You ask ChatGPT or Claude a question and you get back a paragraph, some code, or a poem.
Jev, the first model from TypeSafe AI, doesn't write at all. You give it some information and a question with answers that you define, and it gives you back a decision with probabilities attached. Jev was released on September 15, 2026, in early access.
That makes Jev a good fit for the small decisions inside software. Which team should get this ticket? Is this customer asking for a refund? How bad is this outage? Which tool should my AI agent use? TypeSafe calls it a System One model, a name taken from Daniel Kahneman's book Thinking, Fast and Slow, where System 1 is fast, intuitive thinking. Jev itself is named after the economist William Stanley Jevons.
In this article I'll show you how to use Jev from Python. We'll go through all three question types, send several questions in one request, use Jev to route an AI agent, and compare it with OpenAI's GPT-5.4 nano on the same task. All API outputs below are real results from my runs with jev-1.13.0. Prices and API details were checked on September 21, 2026.
Jev has three question types:
| Type | The question it answers | What you get back |
|---|---|---|
Choice | Which one? | the selected option, a probability for every option, and a confidence |
Noul | Is this true? | one number from 0 to 1 |
Score | How much? | a position on your scale, a probability for every level, and a confidence |
Get an API key
Jev is in early access, so you may need to join the waitlist first. Once you're in, open the API Keys page in the TypeSafe console. On a new account the page is empty. Click Create key.

Give the key a name (I called mine python-script) and click Create key again. Keys belong to your organization, not to you personally, so they keep working even if the person who made them leaves the team.

Copy the key and save it in a .env file in your project folder:
TYPESAFE_API_KEY=your_api_key_here
Keep .env out of version control by adding it to .gitignore.
While you're in the console, take a look at the Playground too. It's a quick way to try a question before you write any code.
Install the Python SDK
pip install typesafe-sdk python-dotenv
typesafe-sdk is the official client. python-dotenv loads your key from the .env file, and TypeSafeClient reads it from the environment on its own. That way the key never appears in your code.
The examples below use the SDK's default model. At the time of these runs, that was jev-1.13.0. To keep using this version as the default changes, pass model="jev-1.13.0" to client.system_one(...). See the model list for current versions.
Choice: pick one option
Let's start with a simple support ticket:
I was charged twice for my subscription. Please refund the duplicate payment.
We want to put it into one of four groups: billing, technical, sales, or other. That's a job for Choice.
from dotenv import load_dotenv from typesafe_sdk import Choice, TypeSafeClient load_dotenv() ticket = """ I was charged twice for my subscription. Please refund the duplicate payment. """ with TypeSafeClient() as client: response = client.system_one( state={"ticket": ticket}, questions={ "category": Choice( instructions="What is the main category of this support ticket?", criteria={ "billing": "Payments, invoices, charges, or refunds.", "technical": "Bugs, errors, or technical problems.", "sales": "Questions about purchasing or upgrading.", "other": "Anything that does not fit the other categories.", }, ), }, ) answer = response.answers["category"] print(answer.choice) print(answer.confidence) print(answer.probabilities) print(response.usage)
Every request has two parts. state is the information Jev looks at, which here is just the ticket. questions is what you want to know about it. Each question gets a name you choose (category), a short instruction, and the possible answers with a one-line description of each.
Here's what came back:
billing 1.0 {'sales': 0.0, 'technical': 0.0, 'other': 0.0, 'billing': 1.0} input_tokens=387 output_tokens=45
You get three things. answer.choice is the winning option. answer.probabilities shows how likely Jev thinks each option is. answer.confidence is one number that says how sure Jev is overall. This ticket was easy, and Jev returned the maximum confidence. That is the model's estimate, not a guarantee of correctness.
A small tip from the question docs: the question name is only for your code and is never sent to the model, so write the full question in instructions even when the name already seems clear.
You can think of Choice as an AI version of if / elif / else. You write the branches. Jev only picks one, based on text that normal code can't read.
Noul: yes or no, as a probability
The second type has an unusual name, but the idea is simple. Noul answers a yes or no question. Instead of True or False, it gives you the probability that the answer is yes.
from dotenv import load_dotenv from typesafe_sdk import Noul, TypeSafeClient load_dotenv() ticket = """ I was charged twice for my subscription. Please refund the duplicate payment. """ with TypeSafeClient() as client: response = client.system_one( state={"ticket": ticket}, questions={ "wants_refund": Noul( instructions="Is the customer explicitly asking for a refund?" ), }, ) answer = response.answers["wants_refund"] print(answer.noul)
The result:
0.99
Jev assigns about 99% probability to the customer wanting a refund. A value near 1 is a strong yes, near 0 is a strong no, and around 0.5 means neither answer is strongly favored. Noul has no separate confidence value, because the number itself already expresses uncertainty.
This fits nicely into plain Python. Jev makes the judgment, and your code decides what to do with it. The functions below are just placeholders for your own logic:
if answer.noul > 0.9: start_refund_process() elif answer.noul > 0.6: send_to_human_review() else: continue_normal_flow()
One thing to watch out for: a Noul of 0.5 means yes and no are equally likely. It doesn't mean "a medium amount". If you want to measure how much of something there is, such as a skill level, the docs recommend a Score with clearly defined levels.
Score: where on the scale?
Now let's find out how urgent a ticket is. Unlike an unordered category, urgency has levels, from "can wait" to "drop everything". That's what Score is for.
from dotenv import load_dotenv from typesafe_sdk import Score, TypeSafeClient load_dotenv() ticket = """ Production is completely unavailable. None of our customers can log in and payments are failing. We are losing transactions every minute. """ with TypeSafeClient() as client: response = client.system_one( state={"ticket": ticket}, questions={ "urgency": Score( instructions="How urgent is this customer support ticket?", criteria=[ "Not urgent — can wait several days.", "Normal — should be handled during normal support hours.", "Urgent — should be investigated soon.", "Critical — immediate business impact requiring attention now.", ], ), }, ) answer = response.answers["urgency"] print(answer.score) print(answer.probabilities)
The result:
3.0 {0: 0.0, 1: 0.0, 2: 0.0, 3: 1.0}
The levels are numbered from 0, in the order you wrote them, so 3.0 means Critical.
The score doesn't have to be a whole number. Later in this article Jev returns 2.97 for another ticket. A score is a position along your levels, so it can land between two of them. In that run, Jev gave 3% to High (level 2) and 97% to Critical (level 3), and 2 × 0.03 + 3 × 0.97 = 2.97. This is a probability-weighted position on the scale; inspect the distribution too, because different distributions can have the same average.
Use Jev to route an AI agent
This is the use case I find most interesting. Say your AI agent has four tools: Python, SQL, web search, and a plain LLM answer. A user writes:
Load sales.csv and create a chart showing monthly revenue by country.
Before the agent does anything, it has to pick a tool. Let's ask Jev.
from dotenv import load_dotenv from typesafe_sdk import Choice, TypeSafeClient load_dotenv() user_request = """ Load sales.csv and create a chart showing monthly revenue by country. """ with TypeSafeClient() as client: response = client.system_one( state={ "user_request": user_request, "available_tools": ["python", "sql", "web", "llm"], }, questions={ "tool": Choice( instructions=( "Which tool should the AI agent use as its primary " "tool to complete the user request?" ), criteria={ "python": "Analyze files, manipulate data, calculate statistics, or create visualizations.", "sql": "Query data stored in a relational database.", "web": "Search for current information on the internet.", "llm": "Answer using language reasoning without external tools.", }, ), }, ) answer = response.answers["tool"] print(answer.choice) print(answer.probabilities)
The result:
python {'python': 1.0, 'llm': 0.0, 'sql': 0.0, 'web': 0.0}
Now your code takes over. As with the refund example, these functions represent your own implementations:
if answer.choice == "python": run_python_agent() elif answer.choice == "sql": run_sql_agent() elif answer.choice == "web": run_web_agent() else: run_llm()
The agent never has to parse a sentence like "I think Python would be the best choice here." It gets one of four strings that it already knows. A single Choice can hold up to 255 options.
Ask many questions in one request
So far we've asked one question per request. You can send many at once, and you should when they share the same context. All the questions in a request look at the same state, and each one is answered on its own, so one answer never affects another.
Here's a harder ticket:
Hi, our production API has been returning HTTP 500 errors since this morning. Around 40% of our customers cannot complete checkout. We have already restarted the service twice but the problem persists. This is blocking our business, so we need somebody to look at it as soon as possible. Thanks, John
We want to know five things about it: its category, whether it's urgent, whether production is down, how bad the damage is, and what support should do next. We also add a few facts about the customer to the state, because they can help Jev decide.
from dotenv import load_dotenv from typesafe_sdk import Choice, Noul, Score, TypeSafeClient load_dotenv() ticket = """ Hi, our production API has been returning HTTP 500 errors since this morning. Around 40% of our customers cannot complete checkout. We have already restarted the service twice but the problem persists. This is blocking our business, so we need somebody to look at it as soon as possible. Thanks, John """ with TypeSafeClient() as client: response = client.system_one( state={ "ticket": ticket, "customer": { "plan": "enterprise", "years_as_customer": 4, }, }, questions={ "category": Choice( instructions="What is the primary category of this ticket?", criteria={ "billing": "Payments, invoices, charges, or refunds.", "technical": "Software failures, bugs, outages, or errors.", "sales": "Purchasing, pricing, demos, or upgrades.", "other": "Anything outside the above categories.", }, ), "urgent": Noul( instructions="Does this ticket communicate an urgent problem requiring fast attention?" ), "production_down": Noul( instructions="Is the customer's production system significantly unavailable or impaired?" ), "severity": Score( instructions="How severe is the customer impact?", criteria=[ "Low — little or no customer impact.", "Moderate — some users affected but business continues.", "High — major functionality unavailable.", "Critical — severe production or business disruption.", ], ), "next_action": Choice( instructions="What should the support system do next?", criteria={ "normal_queue": "Handle through the normal support queue.", "priority_queue": "Prioritize the ticket but no immediate escalation.", "escalate": "Immediately escalate to an engineer or incident team.", }, ), }, ) print(response.answers["next_action"].choice) print(response.answers["severity"].score)
Here's everything that came back:
| Question | Type | Result |
|---|---|---|
| Category | Choice | technical, confidence 1.00 |
| Urgent? | Noul | 0.98 |
| Production down? | Noul | 0.97 |
| Severity | Score | 2.97 out of 3 |
| Next action | Choice | escalate, confidence 0.99 |
All five answers came from one call that used 671 input tokens. At Jev's price, that's about $0.000028, or less than three thousandths of a cent for five decisions.
TypeSafe evaluates all questions in a request in parallel, so adding one more question barely changes the response time. Their docs even suggest asking questions you might not need and letting your code ignore the answers it doesn't use. In one of their examples, putting 13 questions into a single call instead of 13 separate calls was 11.5 times cheaper and 9.6 times faster, with the same answers.
Can Jev hallucinate?
You may have seen the claim that Jev "can't hallucinate". TypeSafe uses that phrase in a narrow sense: Jev's answers stay within the schema you define. It does not mean that every decision is correct.
If your choices are billing, technical, and sales, Jev will never reply legal, or security_team, or "You should probably ask Bob." TypeSafe says its 0% hallucination number isn't something they measured; it follows from how the model constrains its outputs. See their launch explanation.
That's useful for software. Successful responses arrive as typed answers, so you don't need to extract a category from free-form prose. Your application still needs to handle API errors and decide whether to act on the result.
If a ticket is really technical and Jev picks billing, that's still a valid answer. It's just the wrong decision.
Jev can't give you an answer outside your options. It can still pick the wrong option.
That's why the probabilities matter. An ambiguous result might look like this (an illustration, not a recorded API output):
technical 0.52 billing 0.43 sales 0.05
A split like this is a good signal to let a person decide. You can use the returned confidence to set a review threshold:
if answer.confidence > 0.9: route_ticket(answer.choice) else: send_to_human_review()
To be fair, Jev isn't the only way to keep answers inside a list. OpenAI's Structured Outputs can constrain a successful answer with a JSON schema, and we'll use that in the next section. Applications must still handle refusals and incomplete responses.
What Jev adds is that a probability for every option comes back every time, as part of the answer. TypeSafe describes its probabilities as calibrated: higher confidence should correspond to higher accuracy across many examples. My examples were all easy, so everything came out near 1.0. Checking that claim properly needs harder, messier tickets, including confidently wrong answers. Choose thresholds based on tests with your own data. The confidence guide explains how TypeSafe intends these values to be used.
Jev vs GPT-5.4 nano
Let's put Jev next to a regular LLM on the same task. I picked GPT-5.4 nano, the small model OpenAI built for classification, extraction, and high-volume tasks. I used Structured Outputs with a strict enum, so a successful OpenAI response must also use one of the four categories.
Install the OpenAI client:
pip install openai
Add your OpenAI API key to the same .env file:
OPENAI_API_KEY=your_openai_api_key_here
Then run:
import json from dotenv import load_dotenv from openai import OpenAI load_dotenv() ticket = """ I was charged twice for my subscription. Please refund the duplicate payment. """ client = OpenAI() response = client.responses.create( model="gpt-5.4-nano", input=f"Classify the following customer support ticket.\n\nTicket:\n{ticket}", text={ "format": { "type": "json_schema", "name": "ticket_category", "strict": True, "schema": { "type": "object", "properties": { "category": { "type": "string", "enum": ["billing", "technical", "sales", "other"], } }, "required": ["category"], "additionalProperties": False, }, } }, ) answer = json.loads(response.output_text) print(answer["category"])
This short example shows the successful-response path. In an application, check for refusals or incomplete responses before parsing response.output_text.
On the Jev side I used the same Choice question from the start of this article. Both models answered billing. Here's one run:
| Jev 1.13 | GPT-5.4 nano | |
|---|---|---|
| Answer | billing | billing |
| Latency | 894.6 ms | 1254.2 ms |
| Input tokens | 377 | 64 |
| Output tokens | 45 | 13 |
| Cost of this call | about $0.000016 | about $0.000029 |
| Probabilities returned in this example | yes | no |
Jev was faster here, but please don't read too much into a single call. Network, server load, and luck all play a part. The original timing also included client creation for Jev but excluded it for OpenAI. A repeatable benchmark should create both clients before starting either timer, repeat the calls, and report a latency distribution. These numbers are a single observation, not a general speed ranking.
TypeSafe reports typical responses of 70 to 500 ms. Your location and network connection can add to the time you measure.
The prompts differ too: Jev receives descriptions for each category, while this OpenAI request only supplies category names. That affects both input length and potentially decision quality. The two recorded Jev runs also reported slightly different input token counts (387 in the first example and 377 in the comparison); the cost table uses the comparison run's usage.
Price is where it gets interesting. Jev costs $0.042 per million input tokens, and output tokens are free. GPT-5.4 nano costs $0.20 per million input tokens and $1.25 per million output tokens. These are standard API rates, without caching or batch discounts.
Using the recorded token counts:
Jev: 377 × $0.042 / 1,000,000 = $0.000015834 OpenAI: (64 × $0.20 + 13 × $1.25) / 1,000,000 = $0.00002905
For this ticket, that works out to about $16 per million tickets for Jev and about $29 for GPT-5.4 nano. So Jev cost about half as much, even though it used more tokens. This is an extrapolation from one request, not a measured million-ticket workload.
That's far from the "444x cheaper" number you may have seen. That number comes from TypeSafe's own workflow tests, rather than this short classification task. With a small model and a simple task, the gap is smaller. Longer inputs may change the gap because Jev's input-token rate is almost five times lower, though tokenization, question overhead, and output length all affect the total.
Still, the biggest difference in this example is what comes back. The OpenAI schema asks for a category: billing. Jev gives you the category plus its probability for every option. OpenAI can also be prompted to report confidence, but that is different from receiving Jev's native probability distribution. If your system has to decide by itself whether to act or ask a human, those uncertainty estimates are worth evaluating.
Jev and LLMs work well together
Jev won't replace ChatGPT or Claude, and it doesn't try to. They do different jobs.
Use an LLM when you need words: explaining a result, writing code, summarizing a document, or replying to a customer. Use Jev when you need a decision: which tool to use, whether something is urgent, whether to escalate, or whether an answer is good enough. TypeSafe's advice is to ask Jev for judgments a knowledgeable person could make in about a second. If a question needs slow thinking, break it into smaller questions and combine the answers in your code.
Put together, this gives a useful design for an AI agent. Jev sits at the front and picks the path. The LLM does the actual work. Then Jev checks the result and decides whether to accept it, try again, or ask the user.
User request │ ▼ Jev: which tool? ──► Python / SQL / Web / LLM │ ▼ Result │ ▼ Jev: good enough? ──► accept / retry / ask user
You can experiment with these Python calls in a notebook in MLJAR Studio. To turn a ticket-routing prototype into an app with input widgets, see Mercury.
Summary
Jev is a new kind of building block. You describe a question and its possible answers, and Jev returns a typed decision with probabilities. Choice picks one option, Noul tells you how likely something is true, and Score places things on a scale. Your Python code stays in charge of what happens next.
Jev can't answer outside the options you give it, but it can still be wrong. Use the probabilities to decide when to trust it and when to bring in a person.
In my small test, Jev gave the same answer as GPT-5.4 nano, had lower recorded latency, cost about half as much, and also returned probabilities. The timing and prompt differences mean this isn't a controlled benchmark. The real test is how it handles messy, ambiguous cases, because that's where those probabilities earn their keep.
About the Author

Piotr Płoński
Piotr Płoński is a software engineer and data scientist with a PhD in computer science. He has experience in both academia—working on neutrino experiments at leading research labs and collaborating on interdisciplinary projects—and in industry, supporting major clients at Netezza, IBM, and iQor. In 2016, he founded MLJAR to make data science easier and more accessible, creating tools like AutoML, Mercury, and MLJAR Studio.
Related Articles
- Why ipynb is a perfect format for saving AI data analysis conversations
- 10 ways to make predictions with Machine Learning model
- Build a Web App for your Machine Learning model
- How to Run a Local LLM in 2026
- XGBoost Vector Leaf: Multi-Output Regression Explained
- GitHub Outages, Day by Day: A GitHub-Style Activity Calendar
- 3 New Mercury Widgets for Interactive Python Data Apps
- I Analyzed 4.5 Years of GitHub Incidents. Here Is What Changed.
- How Hard Is It to Find a Remote Python Data Job? I Checked 88,975 Hacker News Job Posts
- Are iPhones Really More Expensive? An Inflation-Adjusted Dashboard in Python
Private AI data analysis
AI Data Analyst on Your Computer
Use MLJAR Studio to explore data, discover insights, and create reports with AI.
Runs locally · Your data stays private