Generative models are very good at writing answers.

But sometimes our program does not need a paragraph.

It needs this:

masculine
feminine
ambiguous

And, even better, it needs to know how certain that decision is.

That kind of problem fits Jev well, TypeSafe AI’s first System One Model. Instead of generating free-form text, Jev receives state, one or more typed questions, and returns structured decisions with probabilities.

We are going to build a small but complete example:

receive a name and classify its common usage as masculine, feminine, or ambiguous.

That wording matters. We are not trying to infer a person’s gender identity. We are classifying the linguistic usage of a name, which can vary by country, language, and culture.

The goal

We want to end up with a function like this:

result = classify_name("Alex")

print(result["choice"])
print(result["confidence"])
print(result["probabilities"])

And receive a structure equivalent to:

choice: ambiguous
confidence: ...
probabilities:
  masculine: ...
  feminine: ...
  ambiguous: ...

The numbers depend on the model’s actual response. The important point is that our program receives a typed decision, not an explanation we have to parse afterward.

Install the dependencies

We need requests and python-dotenv:

pip install requests python-dotenv

Create a .env file:

TYPESAFE_API_KEY=your_api_key

And keep it out of Git:

.env

The minimal request

The current endpoint is:

POST https://api.typesafe.ai/v1/systemone

The request has three main pieces:

{
    "model": "jev-latest",
    "state": {...},
    "questions": {...}
}

state describes what Jev should reason about.

questions defines the decisions we want back.

For a classification with several options, we use a choice question.

The detail that causes many 422 errors

A choice question does not use a list called choices.

The current API expects an object called criteria.

This is wrong:

"choices": [
    "masculine",
    "feminine",
    "ambiguous",
]

This is correct:

"criteria": {
    "masculine": "The name is predominantly used as a masculine name.",
    "feminine": "The name is predominantly used as a feminine name.",
    "ambiguous": "The name is meaningfully used across more than one gender or its usage depends strongly on cultural context.",
}

In TypeSafe’s OpenAPI schema, criteria is required for choice. If it is missing or the request structure does not match the schema, the server returns HTTP 422 Unprocessable Entity.

That error does not mean Jev “reasoned incorrectly.”

It means our JSON failed input validation.

Complete script

This example reads a name from the console, calls Jev, and prints the decision, confidence, and all probabilities.

import os
import sys

import requests
from dotenv import load_dotenv


API_URL = "https://api.typesafe.ai/v1/systemone"


def classify_name(name: str, locale: str | None = None) -> dict:
    load_dotenv()

    api_key = os.getenv("TYPESAFE_API_KEY")
    if not api_key:
        raise RuntimeError(
            "TYPESAFE_API_KEY is missing. Add it to the environment or a .env file."
        )

    state = {
        "name": name,
    }

    if locale:
        state["locale"] = locale

    payload = {
        "model": "jev-latest",
        "state": state,
        "questions": {
            "name_usage": {
                "type": "choice",
                "instructions": (
                    "Classify the common usage of the supplied name. "
                    "Do not infer a person's gender identity. "
                    "Use cultural or linguistic context when provided."
                ),
                "criteria": {
                    "masculine": (
                        "The name is predominantly used as a masculine name "
                        "in the supplied context."
                    ),
                    "feminine": (
                        "The name is predominantly used as a feminine name "
                        "in the supplied context."
                    ),
                    "ambiguous": (
                        "The name is meaningfully used across more than one gender, "
                        "is unisex, or changes substantially across cultures or languages."
                    ),
                },
            }
        },
    }

    response = requests.post(
        API_URL,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json=payload,
        timeout=30,
    )

    if response.status_code == 422:
        print("Jev rejected the request schema:", file=sys.stderr)

        try:
            error = response.json()
        except ValueError:
            print(response.text, file=sys.stderr)
        else:
            print(error, file=sys.stderr)

        response.raise_for_status()

    response.raise_for_status()

    data = response.json()
    answer = data["answers"]["name_usage"]

    return {
        "choice": answer["choice"],
        "confidence": answer["confidence"],
        "probabilities": answer["probabilities"],
        "model": data["model"],
        "usage": data["usage"],
    }


def main() -> None:
    name = input("Name: ").strip()

    if not name:
        raise SystemExit("You must enter a name.")

    locale = input(
        "Optional cultural context (for example es, en, it; Enter to skip): "
    ).strip()

    result = classify_name(name, locale or None)

    print()
    print(f"Name: {name}")
    print(f"Classification: {result['choice']}")
    print(f"Confidence: {result['confidence']:.2%}")
    print("Probabilities:")

    for label, probability in result["probabilities"].items():
        print(f"  {label}: {probability:.2%}")

    print(f"Model: {result['model']}")
    print(f"Usage: {result['usage']}")


if __name__ == "__main__":
    main()

What Jev actually returns

According to the current API schema, a choice answer contains:

  • choice: the option with the highest probability;
  • confidence: confidence in the selected choice, from 0 to 1;
  • probabilities: probability assigned to every alternative;
  • type: choice.

Conceptually:

{
  "type": "choice",
  "choice": "ambiguous",
  "confidence": 0.78,
  "probabilities": {
    "masculine": 0.16,
    "feminine": 0.12,
    "ambiguous": 0.72
  }
}

That JSON is illustrative; it is not the output of a specific run.

The difference from asking an LLM to “reply masculine or feminine” is important.

With text output, we could get:

Probably masculine, although it depends on the country.

Now our code has to interpret that sentence.

With Jev, the possible outputs were defined before the model ran.

Uncertainty is also useful output

Suppose our system does not want to act automatically when Jev is not confident enough.

We can add a gate:

result = classify_name("Robin", "en")

if result["confidence"] < 0.80:
    print("Manual review")
else:
    print(result["choice"])

The architecture becomes:

name
  │
  ▼
Jev
  │
  ├── choice
  ├── confidence
  └── probabilities
         │
         ▼
    business rule
         │
    ┌────┴─────┐
    │          │
 high         low
 confidence   confidence
    │          │
 act         review

The model produces uncertainty.

The code decides what to do with it.

That is one of the interesting properties of System One Models: a probabilistic decision can become part of an ordinary software workflow.

A name can change with context

This example also shows why state should not always be just a string.

Consider Andrea.

In many Spanish-speaking contexts it is interpreted mainly as a feminine name.

In Italy, it is also a very common masculine name.

So we can send:

state = {
    "name": "Andrea",
    "locale": "es"
}

or:

state = {
    "name": "Andrea",
    "locale": "it"
}

The question did not change.

The state on which the decision is made changed.

This looks much more like a function:

decision = f(state)

than a traditional chatbot.

Good names to test

To see how the model behaves on less obvious cases, try:

Alex
Andrea
Ariel
Charlie
Chris
Dominique
Jamie
Jordan
Leslie
Morgan
Robin
Sam
Sasha
Taylor

Not all of these are equally ambiguous in every culture.

That is exactly what makes the experiment interesting.

We can run the same name with different contexts and compare how the probability distribution moves.

Batch testing

We can automate the experiment:

names = [
    "Frank",
    "Maria",
    "Alex",
    "Andrea",
    "Robin",
    "Sasha",
]

for name in names:
    result = classify_name(name)

    probs = result["probabilities"]

    print(
        f"{name:10} "
        f"{result['choice']:10} "
        f"conf={result['confidence']:.2%} "
        f"M={probs.get('masculine', 0):.2%} "
        f"F={probs.get('feminine', 0):.2%} "
        f"A={probs.get('ambiguous', 0):.2%}"
    )

This turns a console toy into the beginning of a small benchmark.

We can ask:

  • which names create the most uncertainty?;
  • does the result change when cultural context is supplied?;
  • how stable is the probability distribution across runs?;
  • which cases cross our manual-review threshold?;
  • which criteria need a more precise definition?

How to diagnose a 422 correctly

During development it is tempting to write only:

response.raise_for_status()

But then all we see is:

requests.exceptions.HTTPError:
422 Client Error: Unprocessable Entity

and we lose the useful part.

While developing, print the response:

if not response.ok:
    print("HTTP:", response.status_code)
    print(response.text)

response.raise_for_status()

TypeSafe’s error schema includes a detail list with fields such as:

loc
msg
type

loc tells us where the invalid value is.

For example, if the problem points to:

body → questions → name_usage → criteria

we know exactly which part of the payload to inspect.

Reading a 422 properly can save a lot of debugging time.

Do not hardcode the model name forever

The API also exposes:

GET /v1/models

to discover the models and aliases available to the authenticated account.

We can query it like this:

response = requests.get(
    "https://api.typesafe.ai/v1/models",
    headers={
        "Authorization": f"Bearer {api_key}",
    },
    timeout=30,
)

response.raise_for_status()

for model in response.json()["models"]:
    print(model["name"], model["release_date"])

For demos, using jev-latest is convenient.

In production, it is worth deciding explicitly how aliases and new versions should be handled.

The real example is not “guessing gender”

The example looks like it is about names.

But the pattern is much broader.

We can replace the criteria with:

fraud / legitimate / review
bug / feature / question
urgent / normal / low priority
allow / block / escalate
sales / support / billing
approved / rejected / manual review

The structure stays the same:

state
  +
typed question
  +
criteria defined by the program
  ↓
probabilistic decision
  ↓
normal code

That is the important point.

Jev does not need to write a beautiful answer.

It needs to produce a signal our software can use.

One detail we should not lose

TypeSafe describes Jev as a model designed for fast, typed, calibrated decisions. The company also argues that removing free-form generation prevents output type errors and makes answers easier to integrate directly into workflows.

That does not mean every semantic classification is correct.

An output can match the schema perfectly and still assign the wrong probability.

The advantage is different: the output contract is constrained and uncertainty is exposed to the program.

That lets us write systems like:

if confidence >= 0.90:
    automate()
elif confidence >= 0.70:
    ask_for_review()
else:
    fallback()

In a chatbot, uncertainty is often hidden inside language.

In a decision model, it can become a variable in our program.

Sources