Python SDK: install, clients, system_one()
TL;DR
pip install typesafe-sdk(import nametypesafe_sdk), setTYPESAFE_API_KEY, thenwith 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 aSystemOneResponse, or an instance ofresponse_modelwhen you pass one. The async twin isAsyncTypeSafeClientwithawait client.system_one(...)andaclose(). Since 0.7.0 serialization ispydantic, notmsgspec.
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:
typesafe-aion PyPI (version 0.1.0) is a redirect shim that simply depends ontypesafe-sdk. Installing it works, but you stillimport typesafe_sdk. Prefertypesafe-sdkdirectly.typesafe0.9.1 on PyPI is an unrelated third-party package and is not TypeSafe AI.- The cookbook pages use a
pypi.typesafe.aiindex for acooksafehelper; that host returned 404 publicly as of 2026-09-17.
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.models → Models |
client.models → AsyncModels |
| 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 | None → TYPESAFE_DEFAULT_MODEL → "jev-latest" |
Default model for every call from this client. |
retry |
RetryPolicy | None |
no | None → RetryPolicy() defaults |
Retry behavior. RetryPolicy(max_retries=0) disables retries. |
timeout |
float | httpx2.Timeout | None |
no | None → http_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 | None → TYPESAFE_BASE_URL → https://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):
- Explicit arguments win over environment variables.
- Empty or whitespace-only environment values are ignored and fall back to the default.
base_urlisrstrip("/")-ed.timeoutis validated byresolve_timeout: a non-httpx2.Timeoutvalue must be finite and> 0.- The resolved
api_keyis stored on a dataclass field withrepr=False, as are the default headers, so it does not leak throughrepr().
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_onelists only the empty-questions and empty-score-criteria cases plus the three HTTP/validation errors;_core/questions.pyadditionally raisesTypeSafeErrorfor a malformed question dictionary and for achoice/scoredict with no"criteria"key, and_core/transport.pyraises it when the body cannot be encoded as JSON (now catchingPydanticSerializationError,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
system_one(..., response_model=...)for a typed response — see Typed responses withresponse_model.- "
strsubclasses are now correctly serialized as strings instead of lists of characters" (0.7.0 bug fix;_core/json.py:_fallbacknow returnsstr(value)for anystrsubclass before the genericSequencebranch). - Early API-key validation and credential redaction in logged exceptions (0.7.1).
- AI-gateway usage examples (0.7.1).
What did not change
Score.criteriais still the ordered sequence introduced in 0.6.0 (index = score, starting at 0) — the 0.6.0 rule holds unchanged in 0.7.x.Choice.criteriais still a mapping, andScoreAnswer.legend/.probabilitiesare still keyed by integer score.- The wire contract:
POST /v1/systemone, the same body and headers, the same status codes (HTTP API: POST /v1/systemone and GET /v1/models). RetryPolicy, the exception hierarchy, the constants module, and all four environment variables.
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 redacted — debug 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
- Use
TypeSafeClientfor scripts, sync web frameworks, and notebooks; useAsyncTypeSafeClientinside asyncio services and when fanning out many calls concurrently (see Speculative fan-out). - Do not create a client per request: construction resolves config and builds an
httpx2client. Build one per process and reuse it. - If you only need one call in one language-agnostic place, the raw HTTP API: POST /v1/systemone and GET /v1/models is equivalent; the SDK adds typed questions/answers and the default retry policy.
Version notes
- Version documented here:
typesafe-sdk0.7.1 (repo captured at commit0ffd094c72ed9445223060b24ffd7a56aa781fb4, 2026-09-21;pyproject.tomldeclaresversion = "0.7.1"). See Python SDK changelog. - 0.7.0 replaced
msgspecwithpydanticand addedresponse_model— see Migrating from 0.6.0. - 0.6.0 changed
Score.criteriafrom an int-keyed dict to an ordered sequence, and that is still the current form — see Python SDK question types (Noul, Choice, Score). Usage.billing_unitsis gone from the regenerated wire schema; the publicUsagemakes both token counts optional. Details in Python SDK responses, answers, usage, models.- The
sync/client,sync/models,async/clientandasync/modelsdoc pages were merged upstream into one page per client (/sdk/python/api/clients/syncand/sdk/python/api/clients/async); the old URLs redirect.
Related
- Python SDK question types (Noul, Choice, Score) —
Noul,Choice,Scoreand their dict forms - Python SDK responses, answers, usage, models —
SystemOneResponse, answers, usage, models - Python SDK retries, exceptions, constants —
RetryPolicy, exceptions, constants - Python SDK changelog — release history
- HTTP API: POST /v1/systemone and GET /v1/models — the wire contract the SDK speaks
- TYPESAFE_* environment variables across SDKs —
TYPESAFE_*across SDKs - JavaScript/TypeScript SDK: install, client, choice/score/noul — the JS/TS equivalent
- system-one-adapter: LLM-backed drop-in for TypeSafeClient — LLM-backed drop-in for
TypeSafeClient - Quickstart: first call in HTTP, Python, JS — first call in HTTP, Python, JS
- State: what you send Jev — what
statemay contain - Primitives: Choice, Score, Noul — Choice, Score, Noul
Sources
- raw/docs/sdk.md (https://docs.typesafe.ai/sdk.md)
- raw/docs/sdk__python.md (https://docs.typesafe.ai/sdk/python.md)
- raw/docs/sdk__python__usage.md (https://docs.typesafe.ai/sdk/python/usage.md)
- raw/docs/sdk__python__api.md (https://docs.typesafe.ai/sdk/python/api.md)
- raw/docs/sdk__python__api__clients__sync.md (https://docs.typesafe.ai/sdk/python/api/clients/sync.md)
- raw/docs/sdk__python__api__clients__async.md (https://docs.typesafe.ai/sdk/python/api/clients/async.md)
- raw/docs/sdk__python__api__constants.md (https://docs.typesafe.ai/sdk/python/api/constants.md)
- raw/docs/sdk__python__changelog.md (https://docs.typesafe.ai/sdk/python/changelog.md)
- raw/github/typesafe-sdk-python/README.md, pyproject.toml, src/typesafe_sdk/init.py, src/typesafe_sdk/_core/{config,transport,endpoints,json,logging,constants}.py, src/typesafe_sdk/_core/schemas/base.py (https://github.com/typesafe-ai/typesafe-sdk-python @ 0ffd094c72ed9445223060b24ffd7a56aa781fb4, captured 2026-09-21)
- PyPI package facts (
typesafe-sdk,typesafe-aishim, unrelatedtypesafe) collected 2026-09-17 and recorded in CLAUDE.md