$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people
~/wiki/reference

Python SDK: install, clients, system_one()

[ reference ][ updated 2026-09-21 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.7.1 ]#python · sdk · client · install · system-one

TL;DR pip install typesafe-sdk (import name typesafe_sdk), set TYPESAFE_API_KEY, then with TypeSafeClient() as client: client.system_one(state, questions). Both clients are keyword-only in their constructors; system_one(state, questions, *, model, retry, timeout, extra_headers, extra_body, response_model) returns a SystemOneResponse, or an instance of response_model when you pass one. The async twin is AsyncTypeSafeClient with await client.system_one(...) and aclose(). Since 0.7.0 serialization is pydantic, not msgspec.

Install

Tool Command
uv uv add typesafe-sdk
pip pip install typesafe-sdk

Both commands are verbatim from raw/docs/sdk__python.md, which still installs unpinned. The current release is 0.7.1 (2026-09-21); to pin it explicitly use pip install "typesafe-sdk>=0.7.1,<0.8" / uv add "typesafe-sdk>=0.7.1,<0.8" (see Python SDK changelog).

The distribution name is typesafe-sdk; the import name is typesafe_sdk.

Package-name traps:

Then set the API key (create one at https://console.typesafe.ai/):

export TYPESAFE_API_KEY=...

Requirements

Item Value Source
requires-python >=3.10 pyproject.toml
Declared Python classifiers 3.10, 3.11, 3.12, 3.13, 3.14 pyproject.toml
License MIT (LICENSE shipped) pyproject.toml
Typing Typing :: Typed, ships py.typed pyproject.toml, src/typesafe_sdk/py.typed
Build backend uv_build>=0.12.5,<0.13 pyproject.toml
Author / maintainer TypeSafe AI <support@typesafe.ai> / Daniel Gafni <daniel@typesafe.ai> pyproject.toml

Runtime dependencies ([project].dependencies):

Dependency Constraint Used for
httpx2 >=2.0.0 HTTP transport, Timeout, Headers, Response
pydantic >=2.12.0 question/response models (BaseModel, ConfigDict, ValidationError)
pydantic-core >=2.41.1 JSON codec (to_json / from_json) in _core/json.py
tenacity >=9.0.0 retry loop (Retrying / AsyncRetrying)
typing-extensions >=4.13.0 Self, override, NotRequired, TypedDict, TypeAliasType

msgspec>=0.21.1 was a runtime dependency up to 0.6.0 and is gone as of 0.7.0; the SDK no longer imports msgspec anywhere.

Project URLs: Homepage https://typesafe.ai, Documentation https://docs.typesafe.ai/sdk/python/, Changelog https://docs.typesafe.ai/sdk/python/changelog/, Repository https://github.com/typesafe-ai/typesafe-sdk-python, Issues .../issues.

Public exports

typesafe_sdk.__all__ (38 names, verbatim from src/typesafe_sdk/__init__.py):

Group Names
Clients TypeSafeClient, AsyncTypeSafeClient, Models, AsyncModels
Questions Noul, Choice, Score, NoulCriteria, NoulModel, ChoiceModel, ScoreModel, QuestionModel, Question, Questions
Responses SystemOneResponse, Answer, NoulAnswer, ChoiceAnswer, ScoreAnswer, Usage, ListModelsResponse, ModelMetadata
JSON types JSONContent, JSONValue
Retry RetryPolicy
Errors TypeSafeError, TypeSafeAPIError, TypeSafeAPIConnectionError, TypeSafeAPITimeoutError, TypeSafeAPIResponseValidationError, TypeSafeAuthenticationError, TypeSafeBadRequestError, TypeSafeInternalServerError, TypeSafeNotFoundError, TypeSafePermissionDeniedError, TypeSafeRateLimitError, TypeSafeUnprocessableEntityError
Submodule constants

__version__ is also importable (from typesafe_sdk import __version__) although it is not listed in __all__; it is resolved at import time with importlib.metadata.version("typesafe-sdk").

Everything else lives under typesafe_sdk._core / typesafe_sdk._schemas and is private. __init__.py ends with del _core, so typesafe_sdk._core is not bound as an attribute of the package after import even though the submodule itself is importable.

Clients

Two clients, identical surface except for async/await and the transport types:

Sync Async
Class TypeSafeClient AsyncTypeSafeClient
Call client.system_one(...) await client.system_one(...)
Models resource client.modelsModels client.modelsAsyncModels
List models client.models.list() await client.models.list()
Close close() await aclose()
Context manager with ... as client async with ... as client
transport type httpx2.BaseTransport httpx2.AsyncBaseTransport
http_client type httpx2.Client httpx2.AsyncClient

Constructor

Both constructors are keyword-only (def __init__(self, *, ...)); there are no positional parameters.

TypeSafeClient(
    *,
    api_key: str | None = None,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    headers: Mapping[str, str] | None = None,
    transport: httpx2.BaseTransport | None = None,
    http_client: httpx2.Client | None = None,
    base_url: str | None = None,
)
AsyncTypeSafeClient(
    *,
    api_key: str | None = None,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    headers: Mapping[str, str] | None = None,
    transport: httpx2.AsyncBaseTransport | None = None,
    http_client: httpx2.AsyncClient | None = None,
    base_url: str | None = None,
)
Parameter Type Required Default Description
api_key str | None yes (or env) None API key. May be set via TYPESAFE_API_KEY. "Leading and trailing whitespace is stripped. Empty keys, internal whitespace, control characters, and non-ASCII characters are rejected." A missing or invalid key raises TypeSafeError at client construction, before any request (0.7.1).
model str | None no NoneTYPESAFE_DEFAULT_MODEL"jev-latest" Default model for every call from this client.
retry RetryPolicy | None no NoneRetryPolicy() defaults Retry behavior. RetryPolicy(max_retries=0) disables retries.
timeout float | httpx2.Timeout | None no Nonehttp_client.timeout if http_client given, else 10.0 (constants.DEFAULT_TIMEOUT) Timeout for each HTTP operation. Invalid values raise TypeSafeError.
headers Mapping[str, str] | None no None Additional default request headers.
transport httpx2.BaseTransport / httpx2.AsyncBaseTransport | None no None Custom transport, closed when the SDK client closes. Mutually exclusive with http_client.
http_client httpx2.Client / httpx2.AsyncClient | None no None Bring your own HTTP client. Closed when the SDK client closes. Mutually exclusive with transport.
base_url str | None no NoneTYPESAFE_BASE_URLhttps://api.typesafe.ai API root. Trailing / is stripped during resolution.

Raises:

Exception When
TypeSafeError No API key resolved, the API key is invalid, or timeout is not a positive finite number / httpx2.Timeout.
ValueError Both transport and http_client supplied ("transport and http_client are mutually exclusive.").

Resolution rules (from _core/config.py):

API key validation (0.7.1)

resolve_and_validate_api_key in _core/config.py runs during Config.create, i.e. inside the constructor:

Rule Behavior
Whitespace Stripped from both ends, "including newlines from key files".
Empty after stripping TypeSafeError("No API key was provided. Pass api_key or set the TYPESAFE_API_KEY environment variable.")
Non-ASCII, non-printable, or containing a space TypeSafeError("API key must contain only printable ASCII characters without whitespace.")
Explicitly empty api_key="" Does not fall back to the environment (the docs state: "An explicitly empty key does not fall back to the environment.").

The usage guide states the consequence plainly: "Invalid API keys raise TypeSafeError during client creation, before any request or retry."

Also new in 0.7.1: credentials are stripped from exception text. _core/logging.py:redact_exception copies a transport exception's message, chain, and notes with every secret-header value (and the credential part of Authorization / Proxy-Authorization) replaced by ***, in raw, repr-escaped, byte-repr, and JSON-escaped forms, and detaches the unredacted original from __context__ before re-raising as TypeSafeAPIConnectionError / TypeSafeAPITimeoutError.

Attributes and lifecycle

Member Kind Type Notes
models cached_property Models / AsyncModels Built once per client instance.
system_one(...) method / async method SystemOneResponse | ResponseT See below; ResponseT only when response_model is passed.
close() / aclose() method / async method None Closes the underlying HTTP client, including one you supplied via http_client.
__enter__ / __exit__ context manager Sync client only.
__aenter__ / __aexit__ async context manager Async client only.

Context-manager usage is the documented default in every upstream example. Because close()/aclose() also close a user-supplied http_client, do not share one httpx2.Client across several TypeSafeClient instances whose lifetimes differ.

system_one()

The implementation signature, verbatim from raw/docs/sdk__python__api__clients__sync.md:

system_one(
    state: JSONContent,
    questions: Mapping[str, Question],
    *,
    model: str | None = None,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    extra_headers: Mapping[str, str] | None = None,
    extra_body: Mapping[str, JSONValue | None] | None = None,
    response_model: type[ResponseT] | None = None,
) -> SystemOneResponse | ResponseT

It is published as two @overloads so the return type is exact:

Overload response_model Returns
Overload 1 None = None SystemOneResponse
Overload 2 type[ResponseT] (required, no default) ResponseT

ResponseT is a TypeVar bound to pydantic.BaseModel (_core/schemas/base.py); it is not exported from typesafe_sdk. The async version has the same signature and overloads and is async def.

Parameter Type Positional? Default Description
state JSONContent (str | Mapping[str, JSONValue | None] | Sequence[JSONValue | None]) yes (1st) Text, a JSON object, or an array to evaluate. Cannot be None; values inside an object may be None.
questions Mapping[str, Question] yes (2nd) Nonempty mapping of your names → question objects or raw dicts.
model str | None keyword-only None Per-call model override; None inherits the client default.
retry RetryPolicy | None keyword-only None Per-call retry policy, replacing the client-level one for this call.
timeout float | httpx2.Timeout | None keyword-only None Per-call HTTP timeout in seconds; None inherits the client value.
extra_headers Mapping[str, str] | None keyword-only None Extra request headers for this call.
extra_body Mapping[str, JSONValue | None] | None keyword-only None Extra top-level body fields, shallow-merged over the body after state, model, questions are set. Last write wins; object values are replaced, not deep-merged.
response_model type[ResponseT] | None keyword-only None New in 0.7.0. "Optional Pydantic BaseModel type describing the JSON response body, including any nested answer models."

Returns, verbatim: "An instance of response_model, or SystemOneResponse with answers keyed by question name and model and token usage details when no custom model is supplied." See Python SDK responses, answers, usage, models.

Raises:

Exception When
TypeSafeError questions is empty; a Score question's criteria is empty; a dict question lacks a nonempty string "type"; a "choice"/"score" dict question has no "criteria" key; or the body cannot be JSON-encoded.
TypeSafeAPIError (and subclasses) The server returned an unsuccessful HTTP status after any retries.
TypeSafeAPIConnectionError / TypeSafeAPITimeoutError The request could not connect, or timed out, after any retries.
TypeSafeAPIResponseValidationError "The response body does not match the response model."

The docs' "Raises" block for system_one lists only the empty-questions and empty-score-criteria cases plus the three HTTP/validation errors; _core/questions.py additionally raises TypeSafeError for a malformed question dictionary and for a choice/score dict with no "criteria" key, and _core/transport.py raises it when the body cannot be encoded as JSON (now catching PydanticSerializationError, TypeError, ValueError).

Typed responses with response_model

Upstream shows two shapes (raw/docs/sdk__python__usage.md). Subclass SystemOneResponse to lift named answers onto attributes while keeping .nouls / .choices / .scores, .request_id and .raw_http_response:

from typesafe_sdk import Noul, NoulAnswer, SystemOneResponse, TypeSafeClient


class BillingResponse(SystemOneResponse):
    billing: NoulAnswer


with TypeSafeClient() as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
        response_model=BillingResponse,
    )
    assert 0 <= result.billing.noul <= 1
    assert result.billing == result.nouls["billing"]
    print(result.request_id)

Or define a model from scratch: "It is also possible to define a completely new response model without inheriting from SystemOneResponse":

from pydantic import BaseModel

from typesafe_sdk import Noul, NoulAnswer, TypeSafeClient


class BillingAnswers(BaseModel):
    billing: NoulAnswer


class BillingResponse(BaseModel):
    answers: BillingAnswers


result = TypeSafeClient().system_one(
    "I was charged twice.",
    {"billing": Noul(instructions="Is this about billing?")},
    response_model=BillingResponse,
)
assert 0 <= result.answers.billing.noul <= 1

Mechanism (_core/schemas/base.py:parse_response, _core/response_types.py): a response_model that inherits the SDK's response base goes through the SDK decoder — unknown answer kinds are dropped, and any field you declared beyond SystemOneResponse's own is lifted out of answers into a top-level key before validation. A plain BaseModel is validated directly with model_validate_json(response.content), gets no request_id / raw_http_response, and raises TypeSafeAPIResponseValidationError (with a dotted field_path) when the body does not match.

Wire request built

_core/endpoints.py:prepare_system_one sends POST {base_url}/v1/systemone with the body:

{"state": ..., "model": "...", "questions": {...}}

model is always present (client default when the per-call override is None), then extra_body is applied with body.update(extra_body). Since 0.7.0 prepare_system_one also takes the response_type to decode into (SystemOneResponse when response_model is None). See HTTP API: POST /v1/systemone and GET /v1/models for the wire contract.

Headers the SDK sets

Set on every request from _core/transport.py:prepare (user headers/extra_headers are merged first, then these overwrite them — so authentication, Accept, and SDK identification cannot be overridden):

Header Value
Authorization Bearer {api_key}
Accept application/json
User-Agent typesafe-sdk/{__version__}
X-TypeSafe-SDK typesafe-sdk/{__version__}
X-TypeSafe-Runtime python/{platform.python_version()} ({sys.platform}; {platform.machine()})
Content-Type application/json (only when a body is sent)
X-TypeSafe-Retry-Count attempt number, added only on retries; any caller-supplied value is dropped first

The response header x-typesafe-request-id is surfaced as response.request_id and error.request_id.

Environment variables

Variable Configures Default Constant
TYPESAFE_API_KEY API key (required) constants.API_KEY_ENV
TYPESAFE_BASE_URL API root URL https://api.typesafe.ai constants.BASE_URL_ENV / DEFAULT_BASE_URL
TYPESAFE_DEFAULT_MODEL Default model jev-latest constants.DEFAULT_MODEL_ENV / DEFAULT_MODEL
TYPESAFE_LOG_LEVEL typesafe_sdk logger level, applied once at import unset constants.LOG_LEVEL_ENV

See TYPESAFE_* environment variables across SDKs and Python SDK retries, exceptions, constants for the constants module in full.

models resource

client.models is a cached property returning Models (sync) or AsyncModels (async). It has one method:

list(
    *,
    retry: RetryPolicy | None = None,
    timeout: float | httpx2.Timeout | None = None,
    extra_headers: Mapping[str, str] | None = None,
) -> ListModelsResponse
Parameter Type Default Description
retry RetryPolicy | None None Per-call retry override.
timeout float | httpx2.Timeout | None None Per-operation timeout override; None inherits the client setting.
extra_headers Mapping[str, str] | None None Extra headers; authentication, SDK identification, and Accept remain protected.

Issues GET {base_url}/v1/models. Returns ListModelsResponse whose .models is a tuple[ModelMetadata, ...] of name, description, release_date — see Python SDK responses, answers, usage, models and Models, aliases, pricing, rate limits, context.

from typesafe_sdk import TypeSafeClient

with TypeSafeClient() as client:
    for card in client.models.list().models:
        print(card.name, card.release_date, card.description)

Select a model when constructing a client:

client = TypeSafeClient(model="jev")

Note: "jev" is the upstream usage-guide sample verbatim (raw/docs/sdk__python__usage.md), but it is not among the names listed on Models, aliases, pricing, rate limits, context (jev-latest, jev-preview, jev-1.13.0). Prefer model="jev-latest" or an explicit versioned id, and confirm with client.models.list().

Pointing the client at another base URL (AI gateways)

"In order to use the SDK with a different API url, set base_url on the client or the TYPESAFE_BASE_URL environment variable." 0.7.1 added two worked gateway examples to the usage guide; both are reproduced verbatim. Upstream marks each block skip: next (they are not executed in the docs test suite).

OpenRouter — "Use an OpenRouter API key and an OpenRouter model ID":

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)

Vercel AI Gateway — "Vercel's TypeSafe-compatible API can be used with the SDK":

import os

from typesafe_sdk import Noul, TypeSafeClient

with TypeSafeClient(
    api_key=os.environ["AI_GATEWAY_API_KEY"],
    base_url="https://ai-gateway.vercel.sh/typesafe",
    model="typesafe-ai/jev",
) as client:
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
    )
    print(result.nouls["billing"].noul)

Upstream's one caveat: "This requires the alternative API to follow the TypeSafe OpenAPI spec." Note the gateway key still passes the 0.7.1 API-key validation (printable ASCII, no whitespace), and that neither gateway's pricing or rate limits are covered by Models, aliases, pricing, rate limits, context.

Migrating from 0.6.0

0.7.0 (2026-09-18) is the breaking release: "ser/de library has been changed from msgspec to pydantic". 0.7.1 (2026-09-21) is additive. What this means in practice:

What breaks

Area 0.6.0 0.7.x
Dependency msgspec>=0.21.1 pydantic>=2.12.0, pydantic-core>=2.41.1; msgspec removed
Serializing a response msgspec.json.encode(response) / msgspec.to_builtins(response) response.model_dump_json() / response.model_dump()
Catching decode failures from SDK types msgspec.ValidationError / msgspec.DecodeError pydantic.ValidationError (the SDK still wraps its own decoding in TypeSafeAPIResponseValidationError)
Question objects msgspec Structs, extra constructor kwargs tolerated by the wire types pydantic.BaseModel with model_config = ConfigDict(extra="forbid") — an unknown kwarg to Noul/Choice/Score raises pydantic.ValidationError
Question TypedDicts extra_items=JSONValue | None (extra keys type-check) closed=True (extra keys are a type error, still sent at runtime)
Answer/response types frozen msgspec Structs BaseModel with ConfigDict(extra="ignore", frozen=True, strict=True); model_dump, model_validate_json etc. are available, msgspec.structs.replace is not
JSONValue / JSONContent TypeAlias typing_extensions.TypeAliasType (same members; a plain recursive TypeAlias broke pydantic's core-schema build)
Anything importing typesafe_sdk._schemas.models msgspec Structs generated pydantic.BaseModels

Import paths that did change (verified in the 0.7.1 tree): none of the public ones. typesafe_sdk.__all__ is the same 38 names, and constants is unchanged. Internally ModelMetadata moved from _schemas/models.py to _core/response_types.py and ResponseT moved from _core/transport.py to _core/schemas/base.py, but both are private moves — from typesafe_sdk import ModelMetadata still works.

What's new

What did not change

Upgrade command:

uv add "typesafe-sdk>=0.7.1,<0.8"

Logging

The SDK logs to the typesafe_sdk logger (logging.getLogger("typesafe_sdk")) and attaches a NullHandler plus a SensitiveHeadersFilter. It never configures handlers for you.

import logging

logging.getLogger("typesafe_sdk").setLevel(logging.DEBUG)

Or set TYPESAFE_LOG_LEVEL before importing the SDK; it is applied once at import.

Level string Effect
debug logging.DEBUG — also logs request and response headers and bodies
info logging.INFO — one summary line per request (METHOD url <- status in Nms (request <id>)) plus a line per retry
warn logging.WARNING (accepted by the source; not listed in the docs page)
warning logging.WARNING
error logging.ERROR
off logging.CRITICAL + 1

Redaction: header names in {authorization, proxy-authorization, x-api-key, api-key, cookie, set-cookie}, plus any header name containing token or secret (case-insensitive), are replaced with ***. Request and response bodies are not redacteddebug will print your state and the model's answers.

The SDK also logs Ignoring answer %r with unrecognized type %r at WARNING when the API returns an answer kind this version does not model.

Forward compatibility

Need Mechanism
Send a request field newer than the SDK extra_body={"beam_width": 4}
Send a question field newer than the SDK Pass the question as a plain dict: {"type": "noul", "instructions": "...", "weight": 2}
Read an answer kind newer than the SDK The SDK logs a warning, skips it, and you read result.raw_http_response.json()["answers"]
Unknown extra fields on known responses Silently ignored (model_config = ConfigDict(extra="ignore", ...))

One 0.7.0 wrinkle on the second row: the question TypedDicts are now declared closed=True (they were extra_items=JSONValue | None in 0.6.0), so a dict question carrying an unmodelled key no longer type-checks. It is still sent, and the docs keep the escape hatch with the note: "Unknown fields are a forward-compatibility escape hatch. Ignore their type-checking errors and prefer upgrading the SDK instead." See Python SDK question types (Noul, Choice, Score).

from typesafe_sdk import Noul, TypeSafeClient

with TypeSafeClient() as client:
    client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="About billing?")},
        extra_body={"beam_width": 4},
    )

Complete examples

Sync, verbatim style from the upstream quickstart:

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        state={"document": "I was charged twice. Please fix this ASAP."},
        questions={
            "billing": Noul(instructions="Is this ticket about billing?"),
            "tone": Choice(
                instructions="What is the customer's tone?",
                criteria={"calm": None, "frustrated": None, "angry": None},
            ),
            "urgency": Score(
                instructions="How urgent is this ticket?",
                criteria=["can wait", "this week", "today"],
            ),
        },
    )

print(response.nouls["billing"].noul)
print(response.choices["tone"].choice)
print(response.scores["urgency"].score)

Async:

import asyncio

from typesafe_sdk import AsyncTypeSafeClient, Choice, Noul, Score


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?"),
                "tone": Choice(
                    instructions="What is the tone?",
                    criteria={"calm": None, "angry": None},
                ),
                "urgency": Score(
                    instructions="How urgent is this?",
                    criteria=["low", "medium", "high"],
                ),
            },
        )
        print(
            result.nouls["billing"].noul,
            result.choices["tone"].choice,
            result.scores["urgency"].score,
        )


asyncio.run(main())

Per-call overrides plus error handling:

from typesafe_sdk import Noul, RetryPolicy, TypeSafeAPIError, TypeSafeClient

with TypeSafeClient(model="jev") as client:
    try:
        result = client.system_one(
            "I was charged twice.",
            {"billing": Noul(instructions="Is this about billing?")},
            model="jev-latest",
            retry=RetryPolicy(max_retries=3, backoff_max=0.2, timeout=1.0),
            timeout=5.0,
            extra_headers={"X-Request-Source": "support-bot"},
        )
    except TypeSafeAPIError as error:
        print(error.status, error.request_id)
    else:
        print(result.model, result.usage.input_tokens, result.request_id)

When to use / when not to use

Version notes

Related

Sources