Shaduf.Research preview
BuildSDK 0.7.2 · model 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.

Short answer

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 (import typesafe_sdk)
Current version
0.7.2, PyPI upload 26 Sep 2026 21:20 UTC
Install
pip install typesafe-sdk or uv 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; model jev-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

FieldTypeMeaning
modelstringThe versioned model that answered (for example jev-1.13.0), even when you sent an alias. Log it.
answers.<id>.typechoice, score or noulMatches the question type.
Choice: choice, probabilities, confidencestring; option → number (sums to 1); number 0–1choice 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, confidencenumber; level index → text; level → number; numberscore is probability-weighted and "can land between levels".
Noul: noulnumber 0–1Probability of yes. No confidence field.
usage.input_tokens, usage.output_tokensintegerBoth are returned; only input tokens are billed on TypeSafe direct. See cost per decision.
Header x-typesafe-request-idstringExposed 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

#SymptomCauseFixSource and status (28 Sep)
G1TypeError from msgspec after upgrading0.7.0 moved serialisation from msgspec to PydanticCall .model_dump() before handing answers to another JSON layer#16, closed as intended; no fix version Documented
G2422 "Field required" for body.statestate=None is sent as JSON nullAlways send a string, object or array#17, open Reported
G3400 "Noul question must have criteria or instructions"A Noul() with neither instructions nor criteria passes SDK validationAlways give instructions#17, sdk-js #6, skills #1, open Reported
G4400 on every question in hand-built JSON"type": "Noul" capitalised; the API accepts lowercase onlyUse noul, choice, scoreoh-my-claudecode #4091; not an SDK bug Reported
G5422 list_type on Score criteriaCriteria sent as a map; SDK 0.6.0 and later require an ordered listSend a list, lowest level firstChangelog 0.6.0; #4091 Documented
G6An empty Choice is sent without a local errorThe SDK checks empty Score criteria but not empty Choice criteriaCheck len(criteria) >= 1 yourself#12, open Reported
G7choice is 0.01 below another option's probabilityNear-ties: probabilities are rounded to two decimals after the winner is pickedCompute the argmax from probabilities if it matters; send such cases to review#15, open; maintainer: "a problem in our API" Reported
G8API key appears in exception text0.7.0 used a key with a trailing newline as-isUpgrade to 0.7.1 or later#9, fixed in 0.7.1 Documented
G9Oversized questions come back as an errorThe SDK does not check the 255-option or 10-level maximaCheck len(options) <= 255 and 2 <= len(levels) <= 10 before sendingSDK source _core/questions.py Documented
G10Answers change with no code changeThe default jev-latest alias moves to new releasesPin model="jev-1.13.0" and log response.model (what to pin on each route)Models page Documented
G11Customer text appears in logsWith TYPESAFE_LOG_LEVEL=debug, request and response bodies are logged and "not redacted"Do not use debug logging on production dataSDK usage page Documented
G12pip install typesafe-ai seems to workThat package is a third-party shim that pulls in typesafe-sdkInstall typesafe-sdk directlyPyPI 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

Routebase_urlmodelKey variable in the official example
TypeSafe directhttps://api.typesafe.ai (default)jev-1.13.0TYPESAFE_API_KEY
OpenRouterhttps://openrouter.ai/apitypesafe/jev-1.13 (recommended: pins the minor version); ~typesafe/jev-latest is the moving alias. See model IDs and aliasesOPENROUTER_API_KEY
Vercel AI Gatewayhttps://ai-gateway.vercel.sh/typesafetypesafe-ai/jevAI_GATEWAY_API_KEY
Pydantic AI Gatewayhttps://gateway-us.pydantic.dev/proxy/typesafejev-latestPYDANTIC_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-typesafe 0.1a0 (22 Sep 2026, alpha) is a plugin for the llm command-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.

Search published pools, pages, reports, and evidence.