Creuto is now an OpenAI Select Partner Read More

AI & Machine Learning

Jev Python SDK: install, ask questions, handle errors

A working guide to the Jev Python SDK: install, sync and async clients, questions as objects or dicts, confidence, token usage and the retry policy.

Jev Python SDK: install, ask questions, handle errors

The Jev Python SDK never hands you a string to parse. A call to client.system_one() comes back as a probability between 0 and 1, a label drawn from a set you defined, or a score on your own rubric — each one a typed Python object with its probabilities attached. There is no JSON mode, no schema retry loop, and no prompt to tune.

This is a working guide to that SDK as of 23 September 2026, running against jev-1.13: how to install it, when to reach for the synchronous or the asynchronous client, how to define questions as objects or as plain dictionaries, how to read answers, confidence and token usage off the response, and how the retry policy and exception classes behave when the API pushes back. Every signature below comes from TypeSafe's own SDK reference.

Install the Jev Python SDK and make one call

The package is typesafe-sdk and it needs Python 3.10 or newer. Install it with pip or uv:

pip install typesafe-sdk
# or
uv add typesafe-sdk

The client reads TYPESAFE_API_KEY from the environment and calls jev-latest by default. Three other environment variables matter, and all of them are published as SDK constants: TYPESAFE_BASE_URL (default https://api.typesafe.ai), TYPESAFE_DEFAULT_MODEL (default jev-latest) and TYPESAFE_LOG_LEVEL. The default per-operation timeout is 10 seconds.

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

client = TypeSafeClient()
state = "I was charged twice. Please help ASAP."
questions = {
    "billing": Noul(instructions="Is this about billing?"),
    "tone": Choice(
        instructions="What is the tone?", criteria={"calm": None, "angry": None}
    ),
    "urgency": Score(
        instructions="How urgent is this?", criteria=["low", "medium", "high"]
    ),
}
result = client.system_one(state, questions)
print(
    result.nouls["billing"].noul,
    result.choices["tone"].choice,
    result.scores["urgency"].score,
)

One behaviour worth knowing before you ship: a malformed API key raises TypeSafeError during client construction, before any request leaves the process and before any retry. Keys are stripped of leading and trailing whitespace — useful when the key comes from a file with a trailing newline — but internal whitespace, control characters and non-ASCII characters are rejected outright. If your container boots and the client constructor throws, the key is wrong, not the network.

Sync or async: which client belongs in your service

TypeSafeClient and AsyncTypeSafeClient take the same constructor arguments — api_key, model, retry, timeout, headers, transport, http_client, base_url — and expose the same system_one signature. The async client is awaited and closed with aclose(); the sync one with close(). Both work as context managers, which is the form we use in production because it closes the underlying HTTP client for you.

import asyncio

from typesafe_sdk import AsyncTypeSafeClient, Noul

async def main() -> None:
    async with AsyncTypeSafeClient() as client:
        result = await client.system_one(
            "I was charged twice. Please help ASAP.",
            {"billing": Noul(instructions="Is this about billing?")},
        )
        print(result.nouls["billing"].noul)

asyncio.run(main())

The instinct from LLM work is to reach for the async client and fan out one call per question. Resist it. Jev ingests the state once and evaluates every question against it in parallel inside a single request, and the context budget is shared: 64k tokens per request for state plus all questions, with 32k covering the state plus the single longest question. TypeSafe's own parallel-questions cookbook runs 13 questions over the GDPR Wikipedia article and reports that packing them into one call was 12.2x cheaper and 10.0x faster than one call per question, with no change in the answers. Use the async client because your framework is async, not to parallelise questions the API already parallelises.

Questions as objects, or as plain dictionaries

system_one(state, questions) takes the state first — text, a JSON object or an array, and never None — and then a non-empty mapping of your own names to questions. The names are yours: they come back as the keys of the answer dictionary, so pick names your branching code will read well.

There are three question types, and the shape of criteria differs for each:

QuestioncriteriaYou get back
NoulOptional descriptions of the true and false outcomesnoul: probability of yes, 0 to 1
ChoiceA mapping of labels to descriptions, or None for an undescribed labelchoice, confidence, probabilities per label
ScoreA non-empty ordered sequence, one entry per score starting at zeroscore, confidence, legend, probabilities

Every question object also takes optional instructions, and instructions and criteria both accept a JSON object or array rather than a plain string when the distinction matters.

You do not have to use the classes. A question dictionary carrying a type key of "noul", "choice" or "score" is accepted anywhere a question object is, and you can mix the two forms in the same request. That matters for two reasons. Dictionaries survive being loaded from YAML or a database, which is how we prefer to keep question text reviewable rather than buried in code. And they are the documented forward-compatibility escape hatch: a raw dictionary can carry a request field the installed SDK version does not know about yet, as can extra_body on the call itself.

client.system_one(
    "I was charged twice.",
    {"billing": {"type": "noul", "instructions": "About billing?", "weight": 2}},
)

TypeSafe's own advice on that escape hatch is to prefer upgrading the SDK, and we agree. A dictionary that type-checkers complain about is a note to your future self that you are one release behind.

Reading answers, confidence and token usage

The response is a frozen Pydantic model. result.answers holds every answer keyed by your question name; result.nouls, result.choices and result.scores are cached properties that narrow the same data to one answer type, which is what makes the attribute access below type-safe rather than a cast.

The single most common mistake we see in code written against this API is treating confidence as universal. It is not. A NoulAnswer carries exactly one field, noul: the probability that the statement is true, where values near 0.5 are the uncertain ones. There is no confidence attribute on it at all. ChoiceAnswer and ScoreAnswer do carry confidence, alongside a full probability distribution — over labels for a choice, over integer rubric levels for a score. A score is an expected value, the probability-weighted average of the levels, so it legitimately lands between two integers.

Token usage rides on the same object. result.usage.input_tokens and result.usage.output_tokens are each int | None, and the None is real: the SDK models them as absent when the API does not report them, so a naive sum over a batch will raise on you eventually. result.request_id returns the x-typesafe-request-id header, which is the value to log next to result.model when you need TypeSafe to look a call up. Log both: jev-latest is an alias, and the model field reports the versioned ID that actually answered.

Input tokens are the only ones you pay for — TypeSafe prices Jev at $0.042 per million input tokens with output free — so usage logging here is mostly a capacity signal rather than a cost alarm. If you want the response typed even more tightly, pass response_model: a Pydantic model whose fields are your question names, and attribute access replaces dictionary lookups entirely.

Retries, rate limits and the exception classes

The SDK retries by default. RetryPolicy ships with max_retries=2, an initial backoff of 0.5 seconds doubling to a maximum of 5.0, jitter of 0.25 subtracted from each delay, and a retryable status set of 408, 429 and everything from 500 to 599. It honours Retry-After and retry-after-ms headers, and it retries connection and timeout errors. Sitting over all of that is timeout=30.0, a total budget per SDK call covering the initial attempt and every delay; the policy stops before a retry whose delay would reach the budget and re-raises the last error.

from typesafe_sdk import RetryPolicy, TypeSafeClient

client = TypeSafeClient(
    retry=RetryPolicy(
        max_retries=3, timeout=10.0, http_statuses={429, 500, 502, 503, 504}
    )
)

The same retry argument works per call, which is the right place for it when one endpoint in your service is interactive and another is a nightly job. Pass RetryPolicy(max_retries=0) to turn retries off — do that when your own queue already owns redelivery, because two retry layers stacked on one another turn a brief 429 into a stampede.

Rate limits are where this policy earns its keep. TypeSafe publishes limits of 250,000 tokens per second and 1,200 requests per minute for Jev 1.13, warns that they are adjusting dynamically while demand is high, and returns 429 Too Many Requests when you cross either. TypeSafeRateLimitError exposes retry_after_ms, the server's requested wait, or None when the header is absent. That is the number to surface in your own backpressure, and the pattern is the same one we described for reading a 429 properly rather than guessing at a backoff.

The exception tree is shallow and worth catching precisely:

  • TypeSafeError — the base class, also raised for a bad key or an invalid timeout at construction time, and for empty questions or an empty score rubric.
  • TypeSafeAPIError — any unsuccessful HTTP response, carrying status, body, headers, endpoint and request_id. Its subclasses map to status codes: bad request (400), authentication (401), permission denied (403), not found (404), unprocessable entity (422), rate limit (429) and internal server error (5xx).
  • TypeSafeAPIConnectionError — a request that failed without an HTTP response. TypeSafeAPITimeoutError subclasses it and exposes the timeout that was in force.
  • TypeSafeAPIResponseValidationError — a 2xx whose body was missing or structurally invalid, with field_path pointing at the offending field, such as answers.tone.confidence.

One operational warning that is easy to miss in the docs. Setting TYPESAFE_LOG_LEVEL=debug logs request and response headers and bodies. Secret headers are redacted; bodies are not. Your state is the body, and in most of the systems we build the state is a customer record, a support ticket or a document. Keep debug logging to a local environment with synthetic data, or you have just written personal data into your log pipeline.

Where this SDK is the wrong tool

The strongest argument against reaching for Jev here is that it answers a narrow class of question and will quietly underperform outside it, and TypeSafe documents that rather than hiding it. The published jaggedness page for jev-1.13 is explicit: it does not count reliably, it reads dates as text rather than as ordered quantities, it is not a calculator, accuracy falls as the state fills with detail unrelated to the question, and it is not trained to generate text. Arithmetic, ordering and counting belong in your code; extraction over a closed set of options belongs in a Choice.

So the honest rule is narrow. Reach for this SDK when your Python code needs a decision it can branch on — a route, a rating, a yes/no with a probability attached — and keep everything computable in Python. If what you actually need is a paragraph of text, a code change or a multi-step plan, you want a generative model, and the trade-off between the two is the subject of our earlier piece on when a decision model beats an LLM inside a product.

The next decision is a small one: pick the single noisiest if statement in your codebase — the regex chain, the keyword list, the LLM call wrapped in a JSON parser and two retries — and write it as one Noul with a threshold. Put the question text and the threshold in one module where a reviewer can find them. That is the shape all our AI engineering work takes when a model is making a decision rather than writing prose, and it is the same discipline we apply to API integrations generally: the boundary is typed, and the types are checked at the edge.

Frequently asked questions

Install typesafe-sdk with pip or uv on Python 3.10 or newer, set TYPESAFE_API_KEY in your environment, then create a TypeSafeClient and call system_one with your state and a dictionary of named questions. The response holds one typed answer per question name, keyed exactly as you named them.

Yes. AsyncTypeSafeClient takes the same constructor arguments and the same system_one signature as the synchronous TypeSafeClient. You await the call, use the client as an async context manager, and close it with aclose() rather than close() if you manage its lifetime yourself.

Catch TypeSafeAPIError for any unsuccessful HTTP response and read its status and request_id. TypeSafeRateLimitError covers 429 and exposes retry_after_ms, the server's requested wait. The Jev Python SDK already retries 408, 429 and 5xx status codes twice by default, with exponential backoff and jitter, honouring Retry-After headers.

Yes. A dictionary with a type key of noul, choice or score is accepted anywhere a Noul, Choice or Score object is, and both forms can appear in the same request. Dictionaries are also the documented way to send request fields your installed SDK version does not know yet.

No. A noul answer carries only the noul field, a probability from 0 to 1 where values near 0.5 mean uncertainty. Choice and score answers carry both confidence and a full probability distribution. Code that reads confidence off a noul answer will fail.

TypeSafe charges for input tokens only, at $0.042 per million for Jev 1.13, with output tokens free. The SDK reports usage.input_tokens and usage.output_tokens on every response, though both are typed as optional because the API does not always report them.

Written by

Akash Mohapatra

Akash Mohapatra

Co Founder & Director

22 Sep 2026

·

10 min read

Share

LET'S CONNECT

Connect with Creuto!

Ready to take the first step towards unlocking opportunities, realizing goals, and embracing innovation? We're here and eager to connect.

We don't just aim to fit in – we strive to stand out. Experience the perfect blend of innovation, excellence, and trust that makes us truly unforgettable. Discover the difference with Creuto.

© 2026 Creuto All Rights Reserved