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

Python SDK internals: headers, key validation, gateways and base URLs, logging, forward compatibility, dependencies, migration notes

[ reference ][ updated 2026-09-22 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.7.1 ]#python · sdk · internals · logging · gateways · dependencies

TL;DR The detail behind the builder contract on Python SDK: install, clients, system_one(): full constructor signatures and config resolution, the 0.7.1 API-key validation, the headers and wire body the SDK sends, the models resource, TYPESAFE_* variables, pointing base_url at OpenRouter or Vercel AI Gateway, logging and redaction, forward-compatibility escape hatches, runtime dependencies, the export list, response_model mechanics, and 0.6.0 → 0.7.x migration notes. Documents typesafe-sdk 0.7.1. If you just need to make a call, read Python SDK: install, clients, system_one() instead.

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 in full

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 signatures

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

api_key, model, retry, timeout and base_url are tabled on Python SDK: install, clients, system_one(). The remaining three:

Parameter Type Required Default Description
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.

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

Attributes and lifecycle

Member Kind Type Notes
models cached_property Models / AsyncModels Built once per client instance.
system_one(...) method / async method SystemOneResponse | ResponseT 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.

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. Step-by-step table in Python SDK retries, exceptions, constants.

system_one() internals

The signature and parameter table are on Python SDK: install, clients, system_one(). state is JSONContent (str | Mapping[str, JSONValue | None] | Sequence[JSONValue | None]). 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." 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.

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. More in Python SDK responses, answers, usage, models.

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

Breaking change in 0.7.0: msgspecpydantic, system_one(..., response_model=) added, str subclasses serialized correctly; Score.criteria stays an ordered sequence. The code-level before/after table is in Python SDK changelog (v0.7.0, "What the breaking change means in code"); pin 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},
    )

More complete examples

The sync quickstart, with rate-limit and API-error handling added, is the example on Python SDK: install, clients, system_one().

Async, verbatim from the upstream usage guide:

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. The client-level model="jev" is the usage-guide value discussed under models resource (not a listed model id); here the per-call model="jev-latest" overrides it:

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)

Version notes

Sources