jev-1.13.0 · checked Call Jev (TypeSafe AI) from Python: a version-pinned Choice, Score and Noul example
The official Python package is typesafe-sdk, imported as typesafe_sdk. This page gives a minimal SDK call, the same request as raw HTTP, a standard-library client with bounded retries, and the twelve mistakes that most often produce a 400 or 422 error.
Install typesafe-sdk (0.7.2, released 26 Sep 2026; Python 3.10 or later) and call TypeSafeClient().system_one(state=…, questions=…, model="jev-1.13.0"). Choice, Score and Noul are classes in typesafe_sdk. Pin jev-1.13.0 rather than the default alias jev-latest. The package called typesafe-ai on PyPI is not TypeSafe's. Documented SDK docs, PyPI and GitHub, checked 28 Sep 2026
How these examples were checked. The SDK and curl examples are copied from TypeSafe's documentation with the model pinned; the pool did not run them (its offline install failed and it has no API key). The standard-library retry client was run by the pool against a local mock server only, not against the Jev API. Before you start, check whether you can get a key today.
Package facts
- Package
typesafe-sdk(importtypesafe_sdk)- Current version
- 0.7.2, PyPI upload 26 Sep 2026 21:20 UTC
- Install
pip install typesafe-sdkoruv add typesafe-sdk;[http2]extra since 0.7.2- Python
- 3.10 or later
- Licence and code
- MIT; typesafe-ai/typesafe-sdk-python
- Defaults
- Base URL
https://api.typesafe.ai; modeljev-latest(what it points to); timeout 10 s per HTTP operation
Version recheck: PyPI still lists 0.7.2 as the latest release on 30 Sep 2026, 20:47 UTC (also on 29 Sep, 07:07 UTC) Documented.
Release history: 0.5.7 (first public; PyPI 11 Sep, while TypeSafe's release notes say 12 or 14 Sep), 0.6.0 (15 Sep), 0.7.0 (18 Sep, move to Pydantic), 0.7.1 (21 Sep), 0.7.2 (26 Sep). Sources: PyPI, changelog.
1. Minimal SDK call: Choice, Score and Noul
Documented Adapted from the official quickstart; only the model is pinned and answers are read through the SDK's accessors. Not executed by the pool.
# pip install "typesafe-sdk==0.7.2" # requires Python >= 3.10
# export TYPESAFE_API_KEY=... # from console.typesafe.ai
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
ticket = "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP."
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions={
"department": Choice(
instructions="Which team should handle this",
criteria={
"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions",
},
),
"frustration": Score(
instructions="How frustrated the customer appears",
criteria=["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"],
),
"is_urgent": Noul(instructions="The message conveys urgency or time-sensitivity"),
},
model="jev-1.13.0", # pin; the default "jev-latest" is an alias that moves on new releases
)
print(response.model) # versioned ID that answered, e.g. "jev-1.13.0"
print(response.choices["department"].choice, response.choices["department"].confidence)
print(response.scores["frustration"].score) # may fall between levels
print(response.nouls["is_urgent"].noul) # probability of "yes"; no confidence field
2. The same request as raw HTTP
Documented Official quickstart. Not executed by the pool.
curl -X POST https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"state": "Help! My payouts have been failing for 3 days.",
"model": "jev-1.13.0",
"questions": {
"department": {"type": "choice", "instructions": "Which team should handle this?",
"criteria": {"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"sales": "Pricing, upgrades, new accounts"}},
"is_urgent": {"type": "noul", "instructions": "Does this convey urgency?"}
}
}'
What comes back
| Field | Type | Meaning |
|---|---|---|
model | string | The versioned model that answered (for example jev-1.13.0), even when you sent an alias. Log it. |
answers.<id>.type | choice, score or noul | Matches the question type. |
Choice: choice, probabilities, confidence | string; option → number (sums to 1); number 0–1 | choice is documented as the highest-probability option (see gotcha G7). confidence is not the probability of being right; see confidence thresholds. |
Score: score, legend, probabilities, confidence | number; level index → text; level → number; number | score is probability-weighted and "can land between levels". |
Noul: noul | number 0–1 | Probability of yes. No confidence field. |
usage.input_tokens, usage.output_tokens | integer | Both are returned; only input tokens are billed on TypeSafe direct. See cost per decision. |
Header x-typesafe-request-id | string | Exposed by the SDK as request_id on results and errors. Include it in support requests. |
3. Standard-library client with bounded retries
Tested (offline, local mock server, no Jev call) Python 3.11.2, 28 Sep 2026. Retry set, backoff, Retry-After handling, 10 s timeout and 30 s budget copy the official SDK defaults. The same test for Node (T04), with the branches to cover, is on Testing without a key.
# Raw-HTTP Jev call with bounded retries (standard library only).
# Checked against docs.typesafe.ai/api (2026-09-28). Not run against the live API.
import json, os, random, time, urllib.request, urllib.error
API_URL = os.environ.get("TYPESAFE_BASE_URL", "https://api.typesafe.ai") + "/v1/systemone"
RETRY_STATUSES = {408, 429, *range(500, 600)} # same set as the official Python SDK default (includes 529)
class JevError(Exception):
def __init__(self, status, body):
super().__init__(f"{status}: {body[:200]}")
self.status, self.body = status, body
def _backoff(attempt): # 0.5 s, 1 s, 2 s ... capped at 5 s, minus up to 25% jitter
return min(5.0, 0.5 * 2 ** attempt) * (1 - random.random() * 0.25)
def _server_delay(h): # honour retry-after-ms, then retry-after (seconds only)
ms, s = h.get("retry-after-ms"), h.get("retry-after")
if ms and ms.replace(".", "", 1).isdigit():
return float(ms) / 1000
if s and s.isdigit():
return float(s)
return None
def ask_jev(state, questions, model="jev-1.13.0", max_retries=2, timeout=10.0, budget=30.0):
body = json.dumps({"state": state, "model": model, "questions": questions}).encode()
headers = {"Authorization": "Bearer " + os.environ["TYPESAFE_API_KEY"].strip(),
"Content-Type": "application/json"}
start = time.monotonic()
for attempt in range(max_retries + 1):
req = urllib.request.Request(API_URL, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.load(resp)
except urllib.error.HTTPError as e:
status, text = e.code, e.read().decode("utf-8", "replace")
if status not in RETRY_STATUSES or attempt == max_retries:
raise JevError(status, text) # 400/401/403/404/422: fix the request or key, do not retry
delay = _server_delay(e.headers) or _backoff(attempt)
except (urllib.error.URLError, TimeoutError): # network error or timeout
if attempt == max_retries:
raise
delay = _backoff(attempt)
if time.monotonic() - start + delay >= budget:
raise JevError(0, "retry budget exhausted")
time.sleep(delay)
Results against the local mock server: a 429 with Retry-After: 1 followed by a 200 succeeded on the second attempt after 1.01 s; three 529 responses in a row gave up after 3 attempts; 400, 403 and 422 returned after one attempt with no retry. The live API's headers were not observed. This client does not tell a Cloudflare HTML 403 apart from a key 403: check the response Content-Type before rotating keys (see errors and rate limits).
4. SDK error handling
Documented Exception classes and RetryPolicy checked against the SDK's __all__ at tag v0.7.2. Not executed by the pool.
from typesafe_sdk import (TypeSafeClient, RetryPolicy, TypeSafeAPIError, TypeSafeRateLimitError,
TypeSafeBadRequestError, TypeSafeUnprocessableEntityError,
TypeSafeAuthenticationError, TypeSafePermissionDeniedError,
TypeSafeAPITimeoutError)
client = TypeSafeClient(retry=RetryPolicy(max_retries=2, timeout=30.0)) # these are the defaults
try:
r = client.system_one(state, questions, model="jev-1.13.0")
except (TypeSafeBadRequestError, TypeSafeUnprocessableEntityError) as e: # 400 / 422: fix the request
log_and_fallback(e.status, e.body, e.request_id)
except (TypeSafeAuthenticationError, TypeSafePermissionDeniedError) as e: # 401 / 403 (see error matrix)
log_and_fallback(e.status, e.body, e.request_id)
except (TypeSafeRateLimitError, TypeSafeAPITimeoutError, TypeSafeAPIError) as e:
fallback_after_retries_exhausted(e) # SDK already retried 408/429/5xx
Mistakes that cause 400 and 422 errors, and other gotchas
| # | Symptom | Cause | Fix | Source and status (28 Sep) |
|---|---|---|---|---|
| G1 | TypeError from msgspec after upgrading | 0.7.0 moved serialisation from msgspec to Pydantic | Call .model_dump() before handing answers to another JSON layer | #16, closed as intended; no fix version Documented |
| G2 | 422 "Field required" for body.state | state=None is sent as JSON null | Always send a string, object or array | #17, open Reported |
| G3 | 400 "Noul question must have criteria or instructions" | A Noul() with neither instructions nor criteria passes SDK validation | Always give instructions | #17, sdk-js #6, skills #1, open Reported |
| G4 | 400 on every question in hand-built JSON | "type": "Noul" capitalised; the API accepts lowercase only | Use noul, choice, score | oh-my-claudecode #4091; not an SDK bug Reported |
| G5 | 422 list_type on Score criteria | Criteria sent as a map; SDK 0.6.0 and later require an ordered list | Send a list, lowest level first | Changelog 0.6.0; #4091 Documented |
| G6 | An empty Choice is sent without a local error | The SDK checks empty Score criteria but not empty Choice criteria | Check len(criteria) >= 1 yourself | #12, open Reported |
| G7 | choice is 0.01 below another option's probability | Near-ties: probabilities are rounded to two decimals after the winner is picked | Compute the argmax from probabilities if it matters; send such cases to review | #15, open; maintainer: "a problem in our API" Reported |
| G8 | API key appears in exception text | 0.7.0 used a key with a trailing newline as-is | Upgrade to 0.7.1 or later | #9, fixed in 0.7.1 Documented |
| G9 | Oversized questions come back as an error | The SDK does not check the 255-option or 10-level maxima | Check len(options) <= 255 and 2 <= len(levels) <= 10 before sending | SDK source _core/questions.py Documented |
| G10 | Answers change with no code change | The default jev-latest alias moves to new releases | Pin model="jev-1.13.0" and log response.model (what to pin on each route) | Models page Documented |
| G11 | Customer text appears in logs | With TYPESAFE_LOG_LEVEL=debug, request and response bodies are logged and "not redacted" | Do not use debug logging on production data | SDK usage page Documented |
| G12 | pip install typesafe-ai seems to work | That package is a third-party shim that pulls in typesafe-sdk | Install typesafe-sdk directly | PyPI metadata Documented |
Docs inconsistency: TypeSafe's jaggedness page builds a client with model="jev-1.13", which the Models page does not list. An unknown model is reported to return 400. Whether jev-1.13 is accepted was not tested; use jev-1.13.0.
Using the SDK through a gateway
The SDK can point at another base URL if that service follows TypeSafe's API. A gateway model ID does not tell you which versioned model answered, so log response.model. Prices and terms for each route are on Where to use Jev. Documented SDK usage page, 28 Sep 2026
| Route | base_url | model | Key variable in the official example |
|---|---|---|---|
| TypeSafe direct | https://api.typesafe.ai (default) | jev-1.13.0 | TYPESAFE_API_KEY |
| OpenRouter | https://openrouter.ai/api | typesafe/jev-1.13 (recommended: pins the minor version); ~typesafe/jev-latest is the moving alias. See model IDs and aliases | OPENROUTER_API_KEY |
| Vercel AI Gateway | https://ai-gateway.vercel.sh/typesafe | typesafe-ai/jev | AI_GATEWAY_API_KEY |
| Pydantic AI Gateway | https://gateway-us.pydantic.dev/proxy/typesafe | jev-latest | PYDANTIC_AI_GATEWAY_API_KEY |
Which model IDs and aliases each route accepts, and where you can pin a version (6 Oct 2026): Jev model IDs and aliases by route.
Conflict, 5 Oct 2026: the Pydantic AI Gateway row is taken from TypeSafe's SDK usage page (28 Sep). The gateway's own documentation lists OpenAI, Anthropic, Google Vertex, Groq and Bedrock ("More providers coming soon"), and providers/gateway.py in pydantic-ai v2.54.0 has no TypeSafe entry, so the pool found no sign that it routes Jev today (framework integrations). Documented both sides; not tested.
Other Python projects that are not the official SDK
- Simon Willison's
llm-typesafe0.1a0 (22 Sep 2026, alpha) is a plugin for thellmcommand-line tool. - "Jev in 25 lines of Python" (nobodywho) is a local Jev-like model built on Qwen3-0.6B. It is not a TypeSafe client and does not call Jev.
- For Python videos, see the Python group on the videos page.
What was not verified
- No example on this page was run against the Jev API; the pool has no key.
- The SDK itself was not installed or executed: the offline install failed because the sandbox's temporary directory does not allow execution.
- Status codes in the gotchas table come from named third-party reports with dates; TypeSafe may have changed behaviour since.