---
title: "Python SDK: install, clients, system_one()"
type: reference
tags: [python, sdk, client, install, system-one]
created: 2026-09-17
updated: 2026-09-21
confidence: high
sources:
  - raw/docs/sdk.md
  - raw/docs/sdk__python.md
  - raw/docs/sdk__python__usage.md
  - raw/docs/sdk__python__api.md
  - raw/docs/sdk__python__api__clients__sync.md
  - raw/docs/sdk__python__api__clients__async.md
  - raw/docs/sdk__python__api__constants.md
  - raw/docs/sdk__python__changelog.md
  - raw/github/typesafe-sdk-python/README.md
  - raw/github/typesafe-sdk-python/pyproject.toml
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/__init__.py
jev_version: "jev-1.13.0"
sdk_python: "0.7.1"
summary: "typesafe-sdk 0.7.1: install, TypeSafeClient/AsyncTypeSafeClient constructor params, system_one() kwargs including response_model, models resource, env vars, logging, and the full export list."
---

# Python SDK: install, clients, 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 [[reference/python-sdk-changelog]]).

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

Package-name traps:

- **`typesafe-ai`** on PyPI (version 0.1.0) is a *redirect shim* that simply depends on `typesafe-sdk`. Installing it works, but you still `import typesafe_sdk`. Prefer `typesafe-sdk` directly.
- **`typesafe`** 0.9.1 on PyPI is an unrelated third-party package and is **not** TypeSafe AI.
- The cookbook pages use a `pypi.typesafe.ai` index for a `cooksafe` helper; that host returned 404 publicly as of 2026-09-17.

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

```sh
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.

```python
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,
)
```

```python
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_url` is `rstrip("/")`-ed.
- `timeout` is validated by `resolve_timeout`: a non-`httpx2.Timeout` value must be finite and `> 0`.
- The resolved `api_key` is stored on a dataclass field with `repr=False`, as are the default headers, so it does not leak through `repr()`.

### 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`:

```python
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 `@overload`s 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 [[reference/python-sdk-responses]].

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`:

```python
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`":

```python
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:

```json
{"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 [[reference/http-api]] 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 [[reference/environment-variables]] and [[reference/python-sdk-retries-errors]] for the constants module in full.

## `models` resource

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

```python
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 [[reference/python-sdk-responses]] and [[reference/models-and-pricing]].

```python
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:

```python
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 [[reference/models-and-pricing]] (`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](https://openrouter.ai/~typesafe/jev-latest/)":

```python
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](https://vercel.com/docs/ai-gateway/sdks-and-apis/typesafe) can be used with the SDK":

```python
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](https://api.typesafe.ai/docs/)." 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 [[reference/models-and-pricing]].

## 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 `Struct`s, 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 `TypedDict`s | `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 `Struct`s | `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 `Struct`s | generated `pydantic.BaseModel`s |

**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 with `response_model`](#typed-responses-with-response_model).
- "`str` subclasses are now correctly serialized as strings instead of lists of characters" (0.7.0 bug fix; `_core/json.py:_fallback` now returns `str(value)` for any `str` subclass before the generic `Sequence` branch).
- 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.criteria` is 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.criteria` is still a mapping, and `ScoreAnswer.legend` / `.probabilities` are still keyed by integer score.
- The wire contract: `POST /v1/systemone`, the same body and headers, the same status codes ([[reference/http-api]]).
- `RetryPolicy`, the exception hierarchy, the constants module, and all four environment variables.

Upgrade command:

```sh
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.

```python
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 `TypedDict`s 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 [[reference/python-sdk-questions]].

```python
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:

```python
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:

```python
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:

```python
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 `TypeSafeClient` for scripts, sync web frameworks, and notebooks; use `AsyncTypeSafeClient` inside asyncio services and when fanning out many calls concurrently (see [[patterns/fan-out]]).
- Do not create a client per request: construction resolves config and builds an `httpx2` client. Build one per process and reuse it.
- If you only need one call in one language-agnostic place, the raw [[reference/http-api]] is equivalent; the SDK adds typed questions/answers and the default retry policy.

## Version notes

- Version documented here: `typesafe-sdk` **0.7.1** (repo captured at commit `0ffd094c72ed9445223060b24ffd7a56aa781fb4`, 2026-09-21; `pyproject.toml` declares `version = "0.7.1"`). See [[reference/python-sdk-changelog]].
- 0.7.0 replaced `msgspec` with `pydantic` and added `response_model` — see [Migrating from 0.6.0](#migrating-from-060).
- 0.6.0 changed `Score.criteria` from an int-keyed dict to an ordered sequence, and that is still the current form — see [[reference/python-sdk-questions]].
- `Usage.billing_units` is gone from the regenerated wire schema; the public `Usage` makes both token counts optional. Details in [[reference/python-sdk-responses]].
- The `sync/client`, `sync/models`, `async/client` and `async/models` doc pages were merged upstream into one page per client (`/sdk/python/api/clients/sync` and `/sdk/python/api/clients/async`); the old URLs redirect.

## Related

- [[reference/python-sdk-questions]] — `Noul`, `Choice`, `Score` and their dict forms
- [[reference/python-sdk-responses]] — `SystemOneResponse`, answers, usage, models
- [[reference/python-sdk-retries-errors]] — `RetryPolicy`, exceptions, constants
- [[reference/python-sdk-changelog]] — release history
- [[reference/http-api]] — the wire contract the SDK speaks
- [[reference/environment-variables]] — `TYPESAFE_*` across SDKs
- [[reference/javascript-sdk]] — the JS/TS equivalent
- [[reference/system-one-adapter]] — LLM-backed drop-in for `TypeSafeClient`
- [[guides/quickstart]] — first call in HTTP, Python, JS
- [[concepts/state]] — what `state` may contain
- [[concepts/primitives]] — 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-ai` shim, unrelated `typesafe`) collected 2026-09-17 and recorded in CLAUDE.md
