Jev Python SDK: Install, Examples and Error Handling
Last checked · Independent guide, not affiliated with TypeSafe AI
Install the official SDK with pip install typesafe-sdk (Python 3.10 or newer), set TYPESAFE_API_KEY, and call client.system_one(state=..., questions={...}) with Noul, Choice and Score objects. The SDK retries rate limits and server errors automatically and raises typed exceptions such as TypeSafeAuthenticationError.
The official Python client is typesafe-sdk (imported as typesafe_sdk). It talks to TypeSafe’s API by default, and since version 0.7.1 its documentation also shows how to point it at OpenRouter or Vercel AI Gateway (see below). Everything below was first run with version 0.7.0 on Python 3.10 on September 19, 2026; we re-ran the main example on 0.7.1 on September 25, 2026 with the same answers.
Install
Section titled “Install”pip install typesafe-sdk # or: uv add typesafe-sdkIt requires Python 3.10 or newer.
Mirror gotcha. If pip says No matching distribution found for typesafe-sdk==0.7.1 and lists only older versions, your pip is probably using a package mirror that has not synced yet. That happened to us with a mirror that only had 0.6.0. Install from PyPI directly:
pip install -i https://pypi.org/simple typesafe-sdkVersion 0.7.0 switched the SDK’s serialization library to pydantic; if you pinned 0.6.x, read the changelog before upgrading. Version 0.7.1 (September 21, 2026) is a small fix on top: it checks the key early and keeps the key’s value out of logged exceptions.
Authenticate
Section titled “Authenticate”The client reads TYPESAFE_API_KEY from the environment. You can also pass api_key= to the constructor. With no key at all, the SDK raises TypeSafeError: No API key was provided... before sending anything.
Ask questions
Section titled “Ask questions”from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
client = TypeSafeClient(model="jev-1.13.0")
message = "I was charged twice for my annual plan this morning. Please refund one of the charges today."
response = client.system_one( state=message, questions={ "wants_refund": Noul(instructions="Is the customer asking for money back?"), "queue": Choice( instructions="Which team should handle this message?", criteria={ "billing": "Charges, invoices, refunds", "technical": "Bugs, outages, integrations", "sales": "New plans, upgrades, quotes", }, ), "urgency": Score( instructions="How urgent is this message?", criteria=["Can wait a week", "Should be handled this week", "Needs a reply today"], ), },)
print(response.answers["wants_refund"].noul) # 0.99print(response.answers["queue"].choice) # billingprint(response.answers["urgency"].score) # 2.0print(response.usage) # input_tokens=418 output_tokens=72import asynciofrom typesafe_sdk import AsyncTypeSafeClient, Choice, Noul
async def main(): async with AsyncTypeSafeClient(model="jev-1.13.0") as client: result = await client.system_one( state="My invoice lists two seats, but only one of us can sign in.", questions={ "is_bug": Noul(instructions="Does the customer describe something not working?"), "queue": Choice( instructions="Which team should handle this message?", criteria={"billing": None, "technical": None, "other": None}, ), }, ) print(result.nouls["is_bug"].noul, result.choices["queue"].choice)
asyncio.run(main())The sync example took 557 ms end to end in our run. Besides response.answers[...], the response has typed shortcuts: response.nouls, response.choices and response.scores.
Typed responses with response_model
Section titled “Typed responses with response_model”Since 0.7.0 you can pass a pydantic model to get attribute access and type checking:
from typesafe_sdk import ChoiceAnswer, Noul, Choice, NoulAnswer, SystemOneResponse, TypeSafeClient
class Triage(SystemOneResponse): is_bug: NoulAnswer queue: ChoiceAnswer
with TypeSafeClient(model="jev-1.13.0") as client: r = client.system_one( "My invoice lists two seats, but only one of us can sign in.", { "is_bug": Noul(instructions="Does the customer describe something not working?"), "queue": Choice(instructions="Which team should handle this message?", criteria={"billing": None, "technical": None, "other": None}), }, response_model=Triage, ) print(r.is_bug.noul, r.queue.choice, r.request_id)Choosing a model
Section titled “Choosing a model”Pass model= to the client; the default is jev-latest. Use the full versioned ID jev-1.13.0. The shortened jev-1.13, which appears in one example in TypeSafe’s docs, fails with TypeSafeBadRequestError: ... 400 Unknown model: jev-1.13. To see what your account can use:
print([m.name for m in client.models.list().models]) # ['jev-latest', 'jev-preview']Use the SDK with OpenRouter or Vercel
Section titled “Use the SDK with OpenRouter or Vercel”The client takes a base_url argument, or reads the TYPESAFE_BASE_URL environment variable. OpenRouter and Vercel AI Gateway both serve TypeSafe’s request format, so the same code runs against them with their key and model name:
| Route | base_url |
Key | model |
|---|---|---|---|
| TypeSafe (default) | https://api.typesafe.ai |
TYPESAFE_API_KEY |
jev-1.13.0 or jev-latest |
| OpenRouter | https://openrouter.ai/api |
OPENROUTER_API_KEY |
~typesafe/jev-latest |
| Vercel AI Gateway | https://ai-gateway.vercel.sh/typesafe |
AI_GATEWAY_API_KEY |
typesafe-ai/jev |
import os
from typesafe_sdk import Noul, TypeSafeClient
with TypeSafeClient( api_key=os.environ["OPENROUTER_API_KEY"], base_url="https://openrouter.ai/api", model="~typesafe/jev-latest",) as client: result = client.system_one( "I was charged twice.", {"billing": Noul(instructions="Is this about billing?")}, ) print(result.nouls["billing"].noul)This is the example from TypeSafe’s SDK documentation. We ran the same code against TypeSafe’s own API with TYPESAFE_BASE_URL set on September 25, 2026; we have not run it through OpenRouter or Vercel ourselves. The model-listing call, client.models.list(), does not work on OpenRouter. Channel details: OpenRouter, Vercel AI Gateway.
Errors, retries and timeouts
Section titled “Errors, retries and timeouts”| Exception | When |
|---|---|
TypeSafeError |
No API key configured (raised locally) |
TypeSafeAuthenticationError |
401: invalid key |
TypeSafeBadRequestError |
400: unknown model, unknown question type, max_tokens_exceeded |
TypeSafeUnprocessableEntityError |
422: missing required field |
TypeSafeRateLimitError |
429 after retries are exhausted |
TypeSafeInternalServerError |
5xx (including 529) after retries |
TypeSafeAPITimeoutError, TypeSafeAPIConnectionError |
Network problems after retries |
Messages include the method, URL, status and a request_id, for example POST https://api.typesafe.ai/v1/systemone: 401 Cannot authenticate with the server... (request_id=req_...).
By default the client retries 408, 429 and all 5xx responses, plus timeouts and connection errors, twice after the first attempt, with backoff starting at 0.5 seconds and capped at 5 seconds, and it honors Retry-After. To change this:
from typesafe_sdk import RetryPolicy, TypeSafeClient
client = TypeSafeClient( model="jev-1.13.0", retry=RetryPolicy(max_retries=4, backoff_max=10.0),)- Create one client and reuse it; use it as a context manager (
with/async with) or close it when done. - Put all the questions for one input into one
system_onecall. It costs about the same and takes the same time as a single question. - Keep question text and thresholds in one module so they are easy to review and re-test.
See also: Your first Jev call, Jev API reference and Jev API errors.