Skip to main content

Jev API Tutorial: Building a Ticket Router With TypeSafe AI's System One Model

Learn how to set up TypeSafe AI's Python SDK, ask Choice, Score, and Noul questions in a single call, build a support ticket router that keeps the routing policy in your own code, and find out where Jev breaks before you ship it.
Sep 24, 2026  · 15 min read

Explore with AI

ChatGPTClaudePerplexity

There has been a lot of talk about Jev, TypeSafe AI’s System One Model, since it dropped last week: I saw many people hyping it and many others dunking on it, and the truth probably lies somewhere in between, depending on what you expect from the model. I was very curious to try it out, and finally got preview access earlier this week. 

In this tutorial, I will show you how to set up Jev using their Python SDK, using Jev’s three different question types, and how to build a ticket-routing layer, a use case that plays to the model’s strengths. We’ll also get into where Jev is having a hard time, and what a System One model is, in case you were wondering about the term.

Building with model APIs in Python? Developing LLM Applications with LangChain covers the generative side of the same stack, prompts, chains, and agents.

TL;DR

Jev is TypeSafe AI's System One model. It doesn't generate text. You send it state plus typed questions, it returns typed answers with probabilities, and your code decides what happens next.

  • Three question types. Choice picks one option from a set. Score rates against ordered levels. Noul returns a yes/no probability.
  • Questions are parallel. Six questions cost one call and barely more latency than one, so you ask everything you might want.
  • Confidence is the useful part. It lets you build three paths: automate, hand to a human, fall through.
  • It can't count, can't do date math, and reads your questions literally. TypeSafe publishes the jagged edges, and they matter.

We build a support ticket router in one call, then get into where Jev breaks.

Associate AI Engineer

Train and fine-tune the latest AI models for production, including LLMs like Llama 4. Start your journey to becoming an AI Engineer today!
Explore Track

Why Doesn't Jev Generate Text?

I mentioned already that the expectations have to match the model. This is true especially in Jev’s case, and it mostly has to do with its model class, referred to as System One models.

System One models answer typed decisions instead of tokens

A System One model returns typed decisions instead of text. You send a block of state plus a set of questions, each with an answer space you define, and the model returns one answer per question with probabilities attached. Nothing is produced token by token, so there is no string to parse and no malformed JSON to repair. TypeSafe coined this term to refer to Daniel Kahneman's famous split:

  • System 1 thinking: fast, intuitive judgment
  • System 2 thinking: low, deliberate reasoning

Because the answer space is a schema your code declares, System One models by design can never return a category you never defined. This means it can never be off-schema. That said, it still can be wrong, and we’ll get into a few tricky cases later on.

Where Jev stands

TypeSafe came out of stealth on September 15, 2026, with Jev in early access, reporting 70 to 500 ms responses and $0.042 per million input tokens with output free. On its own four-workflow benchmark, Jev lands near 68% accuracy, roughly mid-tier LLM territory at a fraction of the cost. 

For more information on features and benchmark performance, I recommend reading our Jev guide.

When to reach for Jev instead of an LLM

Write down the valid answers before you make the call. If you can enumerate them, you have a Jev-shaped problem:

  • Routing: which of six queues, which handler, which model
  • Filtering: Is this passage relevant? Is this a jailbreak attempt?
  • Rating against a rubric: how severe, how urgent, how complete
  • Gating: run the expensive step, or skip it

Reach for an LLM when the output is prose or code, when the answer space is open, or when the task needs several hops of reasoning chained together. Jev is also the wrong tool for anything numeric, and I'll come back to why later.

Inspecting the Question Types in the TypeSafe AI Playground

Before writing any code, create a TypeSafe account and open the Playground. Here, you can paste text as the state, add questions, and see the full answer objects without installing anything. It's the fastest way to understand what each question type hands back (and to find out if your question is worded badly).

I'll use one support ticket as the state for all three examples:

Export to CSV has been broken since Friday. 
It works in Chrome, but half our team is on Safari and they can't pull reports at all. 
We have a board meeting Thursday.

Every question type takes instructions, the plain-language question you want answered. What changes between them is criteria, and what comes back.

Choice for categorical routing

A Choice picks one option from a set you define. 

You pass criteria as a dictionary object mapping each option to a description, anywhere from 1 to 255 of them, and the answer comes back with: 

  • The winning option
  • A probability for every option
  • A confidence value
{
  "department": {
    "type": "choice",
    "instructions": "Which queue should own this ticket?",
    "criteria": {
      "bug_triage": "A defect in a specific feature, reproducible, goes into the backlog",
      "incident_response": "A live breakage affecting multiple users right now, needs a responder today",
      "customer_success": "The account needs managing, not the code"
    }
  }
}

To see the code output you’d also receive via API, click on the </> button in the upper right corner, and click Run to let Jev answer the question.

Testing a Choice question for Jev in the TypeSafe playground

In this case, incident_response is the choice with a 91% probability. Jev’s confidence for the pick is 86%.

The full distribution is the part worth your attention. 

  • choice only tells you which option won.

  • probabilities tells you by how much.

Those are different pieces of information when you're about to route a ticket automatically. A 0.41/0.38/0.21 split and the 0.91/0.09/0 split we received might both return the same choice.

Score for ordered rubrics

A Score rates the state against ordered levels. You pass criteria as an array of 2 to 10 level descriptions, lowest first, and the answer includes a score, a legend mapping each position to your description, a probability per level, and confidence.

{
  "goodwill_risk": {
    "type": "score",
    "instructions": "How much patience does this customer have left?",
    "criteria": [
      "Reporting a problem, no sign of frustration",
      "Mildly annoyed, still collaborative",
      "Visibly out of patience, mentions the cost to their work",
      "At the point of escalating over our heads or leaving"
    ]
  }
}

Testing a Score question for Jev in the TypeSafe playground

The score can land between your levels, and that's the whole point of the legend. A score of 1.93 here means the model is split between "mildly annoyed" and "visibly out of patience", strongly leaning toward the latter, which is a fair reading of a ticket that stays polite while naming a deadline it's going to miss. 

Again, read the probabilities spread rather than the number alone: probability concentrated on one level means a decisive answer, probability smeared across three means you got an average instead of a judgment.

Noul for yes/no probabilities

A Noul is the type for binary questions, and its name was coined by TypeSafe. criteria is optional here, though you can describe what true and false mean, which is worth doing whenever "yes" could be read two ways.

Phrase the question so that a high value means yes. TypeSafe's docs are explicit about this, and a Noul whose true maps to "no" performs measurably worse.

{
  "is_time_sensitive": {
    "type": "noul",
    "instructions": "The customer names a specific deadline",
    "criteria": {
      "true": "A date, day, or event the work must be done before",
      "false": "Urgency is implied but no deadline is given"
    }
  }
}

Testing a Noul question for Jev in the TypeSafe playground

Since a board meeting on Thursday is mentioned in the state, the high noul value of 0.97 was expected.

Why does a Noul have no confidence field?

Choice and Score return confidence alongside their probabilities. A Noul doesn't, and that trips people up, so it's worth being precise about why.

Confidence and probability are separate axes. For a Choice, probabilities says how the model distributes belief across your options, and confidence says how firmly it holds that answer, which is why a Choice can return a 0.85 top option with confidence of 0.78. A Noul has only two outcomes, so the single probability already carries both: 0.97 is a firm yes, 0.03 is a firm no, and 0.52 is the model telling you it has no idea.

That means the distance from 0.5 is your decisiveness signal, not a separate field to read. It also means you can't port a threshold from a Noul to a Choice, and I'll come back to that in the jaggedness section, because it bites harder than it sounds.

Setting Up the Jev Python SDK

To follow along, you’ll only need Python 3.10+ and a TypeSafe early-access key.

Installing the SDK

Install the SDK:

pip install typesafe-sdk

Or with uv:

uv add typesafe-sdk

Exporting your key

Then create a key in the TypeSafe console and export it. The client reads TYPESAFE_API_KEY from the environment, so you never pass it in code:

export TYPESAFE_API_KEY="your-key"

Importing answer types and the client

The following imports give you everything from the playground:

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

client = TypeSafeClient()

Choice, Noul, and Score are the same question types you just clicked through, as Python objects. 

Using the TypeSafeClient

TypeSafeClient is the synchronous client, and there's an AsyncTypeSafeClient with the same interface if you're calling Jev from an async service. Both work as context managers, which is what I'd use in anything longer than a script:

with TypeSafeClient() as client:
    ...

Pinning Jev’s version

Left alone, the client calls jev-latest, which moves whenever TypeSafe ships a new version, which is what we will use throughout the tutorial. For anything where you've already tuned a threshold, pin the version instead:

client = TypeSafeClient(model="jev-1.13.0")

The response tells you which model actually answered either way, and there's a section near the end on why you should be logging that.

Making Your First Jev API Call

To make an API call to Jev, you need to define a response item using TypeSafeClient and the system_one() function, which takes your question context as state parameter, and the questions themselves in the same format like in the playground. 

We can create one call that answers all our 3 playground questions, since they share the same state:

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

ticket = (
    "Export to CSV has been broken since Friday. It works in Chrome, "
    "but half our team is on Safari and they can't pull reports at all. "
    "We have a board meeting Thursday."
)

with TypeSafeClient() as client:
    response = client.system_one(
        state=ticket,
        questions={
            "queue": Choice(
                instructions="Which queue should own this ticket",
                criteria={
                    "bug_triage": "A defect in a specific feature, reproducible, goes into the backlog",
                    "incident_response": "A live breakage affecting multiple users right now, needs a responder today",
                    "customer_success": "The account needs managing, not the code",
                },
            ),
            "goodwill_risk": Score(
                instructions="How much patience does this customer have left",
                criteria=[
                    "Reporting a problem, no sign of frustration",
                    "Mildly annoyed, still collaborative",
                    "Visibly out of patience, mentions the cost to their work",
                    "At the point of escalating over our heads or leaving",
                ],
            ),
            "is_time_sensitive": Noul(
                instructions="The customer names a specific deadline",
                criteria={
                    "true": "A date, day, or event the work must be done before",
                    "false": "Urgency is implied but no deadline is given",
                },
            ),
        },
    )

Answers come back under those same names you chose for each question, which is the detail that makes the whole thing pleasant to work with:

print(response.model)
print(response.answers["queue"].choice, response.answers["queue"].confidence)
print(response.answers["goodwill_risk"].score)
print(response.answers["is_time_sensitive"].noul)
jev-1.13.0
Incidence_response 0.95
1.95
0.97

Troubleshooting TypeErrors

If your first call fails with a TypeError about output_buffer_limit: that's a version mismatch in the SDK's compression backend, not your code. The SDK ships its own HTTP client, httpx2, which decompresses responses through zstandard and brotli, and an older copy of either one is missing the argument it gets called with. pip install -U typesafe-sdk httpx2 zstandard brotli cleared it for me.

What the output tells us

Two things in that output are worth pausing on.

First, response.model returned jev-1.13.0, not jev-latest. You asked for the moving alias and Jev told you which version actually answered, which is the only reason logging that field is cheap enough to be worth doing.

Second, the answer objects are typed per question type, so .choice, .score, and .noul are real attributes and your editor knows about them. There is no JSON string anywhere in this code, nothing to parse, and no branch for the call that came back malformed. If you prefer grouping, the SDK also exposes response.choices, response.scores, and response.nouls, keyed the same way.

Check response.usage, too:

print(response.usage.input_tokens, response.usage.output_tokens)
524
78

Output tokens are single digits and free. You pay for the state and the questions, so you can completely control the cost lever by deciding how much context you send. That matters more than it looks, and it comes up again in the jaggedness section: a bloated state costs you money and accuracy at the same time.

Building a Ticket Router in One Jev Call

This is one of the use cases Jev is built for. A support ticket arrives, and something has to decide which queue it goes to, whether a human should look at it first, and how fast. Every one of those is a judgment with an answer space you can write down before the ticket arrives.

The design rule worth stating up front: Jev decides what is, your code decides what happens. Jev never routes anything. It returns numbers, and the routing lives in an ordinary function you can read, test, and change without touching the model.

Asking every question in one request

Questions in a single request are evaluated in parallel, so a sixth question costs you the tokens it's written in and almost no extra latency. That changes how you ask. With an LLM, you'd batch carefully to save round-trip time, but here you ask everything you might want about a certain context, including questions you'll probably ignore.

Let’s expand our previous questions with three more Nouls that give us important information on how to handle the ticket:

  • Does the ticket contain enough information to reproduce the problem?
  • Does it mention lost revenue or additional costs associated with the issue?
  • Does the ticket need a human response?
QUESTIONS = {
    "queue": Choice(
        instructions="Which queue should own this ticket",
        criteria={
            "bug_triage": "A defect in a specific feature, reproducible, goes into the backlog",
            "incident_response": "A live breakage affecting multiple users right now, needs a responder today",
            "customer_success": "The account needs managing, not the code",
        },
    ),
    "goodwill_risk": Score(
        instructions="How much patience does this customer have left",
        criteria=[
            "Reporting a problem, no sign of frustration",
            "Mildly annoyed, still collaborative",
            "Visibly out of patience, mentions the cost to their work",
            "At the point of escalating over our heads or leaving",
        ],
    ),
    "is_time_sensitive": Noul(
        instructions="The customer names a specific deadline",
        criteria={
            "true": "A date, day, or event the work must be done before",
            "false": "Urgency is implied but no deadline is given",
        },
    ),
    "has_reproduction": Noul(
        instructions="The ticket contains enough detail to reproduce the problem",
    ),
    "mentions_money": Noul(
        instructions="The customer mentions lost revenue, refunds, or cancelling",
    ),
    "is_automated": Noul(
        instructions="This ticket is a machine-generated notification, not a person writing in",
    ),
}

with TypeSafeClient() as client:
    response = client.system_one(state=ticket, questions=QUESTIONS)

Six questions, all answered in one call and one bill. is_automated is the speculative one: it's false on almost every real ticket, and it's worth asking anyway because the one time it's true, it saves a person from opening a mailer daemon bounce. 

Two habits I'd pick up here. 

  • Keep the question set as a module-level constant rather than building it inline, because it's the thing you'll version alongside your thresholds. 

  • And name questions after what they measure, not what you'll do with the answer, since is_time_sensitive survives a policy change and route_to_incident doesn't.

  • Framed questions positively: We could have also called is_automated something like needs_no_reply, but according to TypeSafe, performance for positively formulated questions is better.

Turning answers into actions with confidence thresholds

Now the part Jev doesn't do. Every answer arrives with either a confidence value or a probability, and that second number is what lets you build three paths instead of two:

  • High confidence: act automatically
  • Middle band: route to a human, with the model's answer attached as a suggestion
  • Anything the policy doesn't cover: fall through to the default queue

Jev decides what. Your code decides what happens.

Let’s turn that into a couple of rules for routing tickets:

  • If the ticket is likely machine-generated, archive the ticket and act automatically
  • If the customer seems frustrated and mentions financial losses, route the ticket to a human in the customer success team
  • If the model is not certain enough which queue applies, route it to a human in the most likely queue
  • If the ticket is likely urgent, mark it to be handled today
AUTO_ROUTE_CONFIDENCE = 0.75
YES = 0.8
FRUSTRATED = 2.0

def route(answers):
    if answers["is_automated"].noul > YES:
        return "archive", "auto"

    queue = answers["queue"]
    urgent = answers["is_time_sensitive"].noul > YES
    unhappy = answers["goodwill_risk"].score >= FRUSTRATED

    if unhappy and answers["mentions_money"].noul > YES:
        return "customer_success", "human_first"

    if queue.confidence < AUTO_ROUTE_CONFIDENCE:
        return queue.choice, "human_first"

    priority = "today" if urgent else "normal"
    return queue.choice, priority

Read what that function is doing. The model supplied six judgments, and the policy decided that one of them, mentions_money crossed with a frustrated customer, outranks the queue Jev picked. That override is a business decision, it belongs in code, and you can change it on a Friday afternoon without re-testing a model.

has_reproduction never gets used. I left it in deliberately, because that's what the fan-out pattern looks like in practice: you ask for more than the current policy consumes, log all of it, and when someone asks whether bug_triage tickets without repro steps take longer to close, you already have six weeks of the answer.

The thresholds above are just examples. Figuring the right ones out is a matter of calibration, and there's a section at the end on how to set them with data instead of vibes.

Running the script

You can access the full script from this accompanying GitHub repo. When I ran the Python script with our scenario, the judgement was to route the ticket to the incident response team today.

python routing.py
('incident_response', 'today')

Where Jev Breaks: Reading the Jaggedness List

TypeSafe publishes a jaggedness page per model version, listing the failure modes it knows about. I wish more labs did this. Read it before you build anything, and read it again when you upgrade, because the list is versioned and the edges move.

Here are the five that would have cost me the most time.

Noul and Choice don't agree

You can't port a threshold across question types. TypeSafe's own example asks "Is the customer asking for a refund?" both ways on the same ticket: the Noul returns 0.22, the yes/no Choice returns 0.01 for yes, with 0.97 confidence. Same question, two numbers, two orders of magnitude apart.

Negations don't cooperate either. A Noul and its opposite came back at 0.72 and 0.47, which sums to 1.19.

The reason is that the two types ask different things. A Choice is relative and settles which option wins, while each Noul is absolute and can be low for all of them. Tune thresholds per question, in the form you'll ship, and never assume P(yes) and 1 - P(no) are the same number.

A Score is a rank, not a measurement

Score levels are ordered, not spaced. A 1.6 tells you the model sits between your second and third level, leaning to the third, and that's all it tells you.

What you can't do is interpolate a real quantity out of it. If your levels are "under an hour", "a few hours", and "a day", a 1.5 does not mean five hours. Use the score to test a threshold, then keep every actual number in code.

State can argue for its own answer

Jev treats the state as data, but it isn't hardened against state-written code to steer it. An injected instruction, a misleading framing, or text arguing for its own classification can move the answer, and TypeSafe says it expects to improve here. If the attack surface is new to you, we cover the general case in our prompt injection guide.

That matters most where the state is user-submitted, which for a ticket router is always. Write criteria precise enough that the ticket's own claims about itself don't decide the outcome, and test with hostile inputs before you route anything automatically.

Jev can't count, do date or number math

Counting is unreliable and gets worse as the thing being counted grows, because the model recognizes the shape of an answer rather than tallying. Dates are read as text, so ordering, distance, and windows all fail. Numeric encodings underperform their semantic equivalents, so ask about "red" rather than #FF0000.

The fix is the same in all three cases: split the work

  • Extraction is a judgment, so give it to Jev as a Choice over enumerated options. 
  • Keep the arithmetic only in code.

If you need a count, iterate in code and ask one Noul per item:

count = sum(
    result.nouls[f"item_{i}"].noul > 0.5
    for i in range(len(items))
)

Literal reading, indirection, and padded state

Three smaller ones that share a cause. Jev answers the question you wrote, not the one you meant, so scoping words and negations are read at face value. Double negatives and questions about a property of a property cost accuracy. And a large state padded with irrelevant detail costs both accuracy and money, since unrelated material acts as a distractor.

The tell for the first one: when you look at a wrong answer and catch yourself explaining what you really meant, that explanation is the missing half of your instruction.

What to Do Before You Put Jev in Production

Based on what we’ve learned already, here are a few best practices to make the most out of Jev.

Writing questions Jev answers well

Write the condition instead of the intent. If you're explaining what you meant when reviewing a wrong answer, that explanation belongs in the instructions. What this means:

  • Limit yourself to one judgment per question.
  • Choose criteria that cover the boundary cases.
  • Pick a wording where a high value means yes. 
  • Send only the state the question needs.

Pinning a version and logging what Jev answered

jev-latest moves. Pin the version once a threshold depends on model behavior:

client = TypeSafeClient(model="jev-1.13.0")

Log response.model alongside the full answers on every call, not just the value you acted on. When a threshold starts misbehaving, that log is the only way to tell a model change from a drift in your tickets.

Testing thresholds before you trust them

Finally, thresholds are not a given but the result of experimentation.

  • Collect 20 or more real tickets with the answers you'd have wanted. Run Jev beside your existing routing without changing behaviour, and compare.
  • Fix the questions first, thresholds second. Then automate the cheapest path to get wrong and leave the rest to a human.
  • Version questions, criteria, and thresholds together. Replay the set whenever any of the three changes.

Final Thoughts

Jev is a narrow tool, and that's actually the point of it. It answers questions whose answers you can enumerate, cheap enough that you stop rationing them, and hands the decision back to your code.

What I'd push back on is the "cannot hallucinate" framing. It's true in a narrow sense that the model can't answer off-schema, but it says nothing about whether the answer is right. A Choice always returns a valid queue. It can still be the wrong queue at 0.9 confidence, and typed output makes that failure quieter.

To test it for your own work. I suggest finding one decision your code makes with a brittle rule or a slow LLM call, and try to write down the valid answers. If you can, it's Jev-shaped. If you can't, no amount of question engineering will change that.

If you want to get started with building systems that use AI, I highly recommend enrolling in our Associate AI Engineer for Developers career track. It teaches you how to work with the OpenAI API, MCP, LangChain, and much more.

FAQs

Which Jev question type should I use?

Choice when you can enumerate the options, Score when the answers form ordered levels, Noul for a single yes/no. The rule of thumb: if the answers have an order, use Score, because a Choice throws that order away. If you find yourself writing a Choice with options like "low", "medium", "high", you want a Score.

Can I ask Jev multiple questions in one API call?

Yes, and you should. Questions in a single request are evaluated in one parallel pass, so a sixth question costs the tokens it's written in and almost no extra latency. You pay for the state once instead of once per question, which makes it cheaper to ask everything you might want and ignore the answers you don't use.

Does a Noul return a confidence score?

No. Choice and Score answers include a confidence field, but a Noul returns only the probability, because with two outcomes, that single number already carries both. How far the value sits from 0.5 is your decisiveness signal, so 0.97 is a firm yes, and 0.52 means the model has no idea.

Can Jev count or do arithmetic?

No. Counting is unreliable and degrades as the count grows, dates are read as text rather than ordered values, and numeric encodings underperform their plain-language equivalents. Split the work: let Jev make the judgment, and keep the arithmetic in your own code.

Should I pin the Jev model version?

Yes, once any threshold in your code depends on model behaviour. jev-latest moves when TypeSafe ships a new version, and TypeSafe publishes a separate jaggedness list per version, so the failure modes change too. Pass an explicit version to the client and log response.model on every call.


Tom Farnschläder's photo
Author
Tom Farnschläder
LinkedIn

Tom is a data scientist and technical educator. He writes and manages DataCamp's data science tutorials and blog posts. Previously, Tom worked in data science at Deutsche Telekom.

Topics
Artificial Intelligence

Learn AI Engineering With DataCamp!

Track

Associate AI Engineer for Developers

29 hr
Learn how to integrate AI into software applications using APIs and open-source libraries. Start your journey to becoming an AI Engineer today!
See DetailsRight Arrow
Start Course
See MoreRight Arrow
Related

blog

Jev: TypeSafe's System One Model That Never Hallucinates

TypeSafe's Jev is a new class of AI model, a System One Model, that returns typed decisions with calibrated probabilities instead of text, running 40-200x faster than frontier LLMs.
Matt Crabtree's photo

Matt Crabtree

10 min

blog

Top 7 Open-Source TypeSafe Jev Alternatives

Explore a new wave of open-source projects offering practical Jev alternatives for fast, local, and cost-efficient AI decision-making.
Abid Ali Awan's photo

Abid Ali Awan

14 min

blog

TypeSafe Jev vs GPT-6 Astra: Decision Model or Frontier Agent?

Jev decides in milliseconds and cannot write a word; GPT-6 Astra finishes whole tasks unattended. Here is which pipeline steps belong to each model, and what they cost.
Tom Farnschläder's photo

Tom Farnschläder

12 min

Tutorial

Jan-V1: A Guide With Demo Project

Learn how to build a Deep Research Assistant using Jan-v1's agentic reasoning capabilities, including local deployment, Streamlit app development, and more.
Aashi Dutt's photo

Aashi Dutt

12 min

Tutorial

OpenAI Agents SDK Tutorial: Building AI Systems That Take Action

Learn how to build intelligent AI applications with OpenAI's Agents SDK. This comprehensive guide covers creating agents, implementing tools, structured outputs, and coordinating multiple agents.
Bexruz (Bex) Tuychiev's photo

Bexruz (Bex) Tuychiev

12 min

Tutorial

OpenAI Realtime API: A Guide With Examples

Learn how to build real-time AI applications with OpenAI's Realtime API. This tutorial covers WebSockets, Node.js setup, text/audio messaging, function calling, and deploying a React voice assistant demo.
François Aubry's photo

François Aubry

15 min

See MoreSee More