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.