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

Python SDK retries, exceptions, constants

[ reference ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.6.0 ]#python · sdk · retries · errors · timeouts · constants

TL;DR Every client retries by default: RetryPolicy() = 2 retries, exponential backoff 0.5s → 5.0s with 25% jitter, retryable statuses {408, 429, 500–599}, Retry-After honored, 30s total retry budget per call. HTTP timeouts are separate and default to 10.0s per operation. Catch TypeSafeError for everything, TypeSafeAPIError for HTTP failures (.status, .body, .headers, .endpoint, .request_id), and TypeSafeAPIConnectionError / TypeSafeAPITimeoutError for transport failures. Disable retries with RetryPolicy(max_retries=0).

RetryPolicy

A frozen dataclass, importable as from typesafe_sdk import RetryPolicy. It may be passed on the client (retry=) or per call (system_one(..., retry=...) / models.list(retry=...)); a per-call policy fully replaces the client-level one for that call.

RetryPolicy(
    max_retries: int = 2,
    backoff_initial: float = 0.5,
    backoff_max: float = 5.0,
    backoff_jitter: float = 0.25,
    http_statuses: set[int] = {408, 429, *range(500, 600)},
    respect_retry_after: bool = True,
    api_connection_error: bool = True,
    api_timeout_error: bool = True,
    exceptions: set[type[BaseException]] = set(),
    predicate: Callable[[BaseException], bool] | None = None,
    timeout: float | None = 30.0,
)
Field Type Default Description
max_retries int 2 Maximum retries after the initial attempt; 0 disables retries. So the default is up to 3 attempts total.
backoff_initial float 0.5 First backoff delay in seconds, doubled each attempt up to backoff_max; zero disables backoff.
backoff_max float 5.0 Maximum backoff delay in seconds; zero disables backoff.
backoff_jitter float 0.25 Fraction of each backoff delay randomly subtracted, between 0 and 1.
http_statuses set[int] {408, 429, 500, 501, …, 599} (via default_factory) HTTP status codes that are retried.
respect_retry_after bool True Honor Retry-After and retry-after-ms response headers.
api_connection_error bool True Retry TypeSafeAPIConnectionError (cannot reach or read from the server).
api_timeout_error bool True Retry TypeSafeAPITimeoutError (request exceeded its timeout).
exceptions set[type[BaseException]] set() (via default_factory) Additional exception types that trigger a retry, on top of the built-in rules.
predicate Callable[[BaseException], bool] | None None Called with the raised exception; returning True triggers a retry in addition to the other rules.
timeout float | None 30.0 Total retry budget in seconds per SDK call, including the initial attempt and delays; None disables the limit. Stops before a retry whose delay would reach or exceed the budget, re-raising the last error.

Validation (__post_init__)

Condition Exception and message
max_retries not an int, or negative TypeSafeError("max_retries must be a non-negative integer.")
backoff_initial / backoff_max non-finite or negative TypeSafeError("<name> must be a non-negative, finite number of seconds.")
backoff_jitter outside [0, 1] TypeSafeError("backoff_jitter must be between zero and one.")
timeout not None and not a positive finite number (or httpx2.Timeout) TypeSafeError("timeout must be a positive, finite number of seconds.") (via resolve_timeout)

This validation is the 0.6.0 bug fix "handle invalid values in RetryPolicy".

Which failures are retried

RetryPolicy._retryable(error) returns True when any of these hold:

  1. isinstance(error, TypeSafeAPITimeoutError) and api_timeout_error is True; else
  2. isinstance(error, TypeSafeAPIConnectionError) and api_connection_error is True; else
  3. isinstance(error, TypeSafeAPIError) and error.status in http_statuses; else not retryable by the built-in rules — then
  4. isinstance(error, tuple(self.exceptions)), or
  5. predicate is not None and predicate(error).

The three built-in branches are mutually exclusive (TypeSafeAPITimeoutError is checked first because it subclasses TypeSafeAPIConnectionError). Note that TypeSafeAPIResponseValidationError subclasses TypeSafeAPIError and carries the successful status code, so it is retried only if that status happens to be in http_statuses — with the defaults, a 200 is not.

Default retryable statuses: 408 (Request Timeout), 429 (Too Many Requests), and every 5xx from 500 through 599. Non-retryable by default: 400, 401, 403, 404, 422. See HTTP status codes, rate limits, retry semantics.

Backoff and Retry-After

For attempt n (1-based, n = 1 is the wait before the first retry), _backoff computes:

With defaults that is ~0.5s then ~1.0s (each minus up to 25%).

When respect_retry_after is True and the error is a TypeSafeAPIError carrying a retry header, that header overrides the computed backoff entirely. parse_retry_after checks headers in this order:

Header Unit Multiplier to ms
retry-after-ms milliseconds ×1
retry-after seconds, or an HTTP-date ×1000

Rules from _core/errors.py:parse_retry_after:

The total budget is enforced with tenacity's stop_after_attempt(max_retries + 1) | stop_before_delay(timeout) and reraise=True, so the original exception (not a RetryError) surfaces.

Each retried request carries X-TypeSafe-Retry-Count: <attempt> and logs "<METHOD> <url> retry <n>" at INFO.

Examples

from typesafe_sdk import RetryPolicy, TypeSafeClient

client = TypeSafeClient(
    retry=RetryPolicy(max_retries=3, timeout=10.0, http_statuses={429, 500, 502, 503, 504})
)
from typesafe_sdk import Noul, RetryPolicy, TypeSafeClient

with TypeSafeClient() as client:
    # client-level
    client_policy = RetryPolicy(max_retries=3, backoff_max=0.2, timeout=1.0)

    # per-call override
    result = client.system_one(
        "I was charged twice.",
        {"billing": Noul(instructions="Is this about billing?")},
        retry=RetryPolicy(max_retries=0),  # no retries for this call
    )
    print(result.nouls["billing"].noul)

Retry on an extra exception type, or with a custom predicate:

import httpx2

from typesafe_sdk import RetryPolicy, TypeSafeAPIError, TypeSafeClient

policy = RetryPolicy(
    exceptions={httpx2.RemoteProtocolError},
    predicate=lambda error: isinstance(error, TypeSafeAPIError) and error.status == 409,
)
client = TypeSafeClient(retry=policy)

Timeouts

Two independent budgets:

Setting Default Scope Where
timeout on the client / per call 10.0 s (constants.DEFAULT_TIMEOUT) each HTTP operation (connect/read/write/pool, per httpx2.Timeout semantics) TypeSafeClient(timeout=...), system_one(..., timeout=...), models.list(timeout=...)
RetryPolicy.timeout 30.0 s the whole SDK call: initial attempt + all retries + delays RetryPolicy(timeout=...)

Precedence for the HTTP timeout: per-call timeout → client timeouthttp_client.timeout (when you supplied an http_client and passed no timeout) → DEFAULT_TIMEOUT (10.0).

Pass an httpx2.Timeout for fine-grained control:

import httpx2

from typesafe_sdk import TypeSafeClient

client = TypeSafeClient(timeout=httpx2.Timeout(connect=2.0, read=20.0, write=10.0, pool=5.0))

An expired HTTP timeout raises TypeSafeAPITimeoutError, which is retryable by default; when the retry budget is exhausted it propagates.

Exception hierarchy

Exception
└── TypeSafeError
    ├── TypeSafeAPIError
    │   ├── TypeSafeBadRequestError            (400)
    │   ├── TypeSafeAuthenticationError        (401)
    │   ├── TypeSafePermissionDeniedError      (403)
    │   ├── TypeSafeNotFoundError              (404)
    │   ├── TypeSafeUnprocessableEntityError   (422)
    │   ├── TypeSafeRateLimitError             (429)
    │   ├── TypeSafeInternalServerError        (5xx)
    │   └── TypeSafeAPIResponseValidationError (2xx with an unusable body)
    └── TypeSafeAPIConnectionError   (also subclasses ConnectionError)
        └── TypeSafeAPITimeoutError  (also subclasses TimeoutError)
Class Bases Raised when Extra attributes
TypeSafeError Exception Base for all SDK failures; also raised directly for config/validation problems (missing API key, invalid timeout, empty questions, invalid RetryPolicy, unencodable body, missing request id).
TypeSafeAPIError TypeSafeError Any unsuccessful HTTP response after retries. status: int, body: Any, headers: httpx2.Headers, endpoint: str | None, request_id: str | None (property)
TypeSafeBadRequestError TypeSafeAPIError 400 — the request was invalid. inherited
TypeSafeAuthenticationError TypeSafeAPIError 401 — authentication failed. inherited
TypeSafePermissionDeniedError TypeSafeAPIError 403 — access was denied. inherited
TypeSafeNotFoundError TypeSafeAPIError 404 — resource not found. inherited
TypeSafeUnprocessableEntityError TypeSafeAPIError 422 — failed server validation. inherited
TypeSafeRateLimitError TypeSafeAPIError 429 — rate limit exceeded. retry_after_ms: float | None plus inherited
TypeSafeInternalServerError TypeSafeAPIError any status ≥ 500. inherited
TypeSafeAPIResponseValidationError TypeSafeAPIError A successful HTTP response whose body was missing or structurally invalid required data. field_path: str (e.g. answers.tone.confidence); args == (status, body, headers, field_path, endpoint)
TypeSafeAPIConnectionError TypeSafeError, ConnectionError The request failed without an HTTP response.
TypeSafeAPITimeoutError TypeSafeAPIConnectionError, TimeoutError The request exceeded its configured timeout. timeout: float | httpx2.Timeout

Status→class mapping (STATUS_ERROR_TYPES in _core/errors.py): 400, 401, 403, 404, 422, 429 map to the classes above; any other status ≥ 500 becomes TypeSafeInternalServerError; anything else becomes a plain TypeSafeAPIError.

Because TypeSafeAPIConnectionError also subclasses ConnectionError and TypeSafeAPITimeoutError also subclasses TimeoutError, generic except ConnectionError / except TimeoutError handlers in surrounding code will catch them.

Error attributes in detail

__str__ renders "{endpoint}: {status} {message} (request_id={id})", omitting the parts that are unavailable. The message is derived from the body by extract_message, which tries, in order: body as a string → body["error"] (string) → body["error"]["message"]body["message"]body["detail"] (string) → body["detail"]["message"] → a FastAPI-style body["detail"] list joined as "loc.path: msg; …". If nothing matches, the serialized body is truncated to MAX_ERROR_BODY_LENGTH (200) characters with an ellipsis; an empty body yields "status code (no body)". __repr__ deliberately omits the body and headers. Exceptions are picklable as of 0.6.0.

Handling examples

from typesafe_sdk import TypeSafeAPIError, TypeSafeClient

try:
    client.system_one(state, questions)
except TypeSafeAPIError as error:
    print(error.status, error.request_id)

Full triage:

import logging
import time

from typesafe_sdk import (
    Noul,
    TypeSafeAPIConnectionError,
    TypeSafeAPIError,
    TypeSafeAPIResponseValidationError,
    TypeSafeAPITimeoutError,
    TypeSafeAuthenticationError,
    TypeSafeClient,
    TypeSafeError,
    TypeSafeRateLimitError,
)

with TypeSafeClient() as client:
    try:
        result = client.system_one(
            "I was charged twice.",
            {"billing": Noul(instructions="Is this about billing?")},
        )
    except TypeSafeAuthenticationError:
        raise SystemExit("Check TYPESAFE_API_KEY")
    except TypeSafeRateLimitError as error:
        wait = (error.retry_after_ms or 1000) / 1000
        logging.warning("rate limited, waiting %.1fs (request %s)", wait, error.request_id)
        time.sleep(wait)
    except TypeSafeAPIResponseValidationError as error:
        logging.error("bad response body at %s: %s", error.field_path, error.endpoint)
    except TypeSafeAPITimeoutError as error:
        logging.error("timed out after %s", error.timeout)
    except TypeSafeAPIConnectionError as error:
        logging.error("connection failure: %s", error)
    except TypeSafeAPIError as error:
        logging.error("%s %s", error.status, error)
    except TypeSafeError as error:  # config / validation problems, no HTTP made
        logging.error("client-side error: %s", error)
    else:
        print(result.nouls["billing"].noul)

Catch order matters: TypeSafeAPITimeoutError before TypeSafeAPIConnectionError, the specific TypeSafeAPIError subclasses before TypeSafeAPIError, and TypeSafeError last.

Constants

Public — typesafe_sdk.constants

Name Value Meaning
API_KEY_ENV 'TYPESAFE_API_KEY' Environment variable for the API key.
BASE_URL_ENV 'TYPESAFE_BASE_URL' Environment variable for the API base URL.
DEFAULT_MODEL_ENV 'TYPESAFE_DEFAULT_MODEL' Environment variable for the default model.
LOG_LEVEL_ENV 'TYPESAFE_LOG_LEVEL' Environment variable for the logging level.
DEFAULT_BASE_URL 'https://api.typesafe.ai' Default API base URL.
DEFAULT_MODEL 'jev-latest' Default model name.
DEFAULT_TIMEOUT 10.0 Default timeout in seconds for each HTTP operation.
from typesafe_sdk import constants

print(constants.DEFAULT_BASE_URL, constants.DEFAULT_MODEL, constants.DEFAULT_TIMEOUT)

Internal — typesafe_sdk._core.constants

Not part of the public API (absent from the docs and from __all__), but they define the wire behavior you will see on the network:

Name Value
SYSTEM_ONE_PATH /v1/systemone
MODELS_PATH /v1/models
SDK_NAME typesafe-sdk
LOGGER_NAME typesafe_sdk
JSON_CONTENT_TYPE application/json
MAX_ERROR_BODY_LENGTH 200
AUTHORIZATION_HEADER Authorization
ACCEPT_HEADER Accept
CONTENT_TYPE_HEADER Content-Type
USER_AGENT_HEADER User-Agent
SDK_HEADER X-TypeSafe-SDK
RUNTIME_HEADER X-TypeSafe-Runtime
RETRY_COUNT_HEADER X-TypeSafe-Retry-Count
REQUEST_ID_HEADER x-typesafe-request-id
RETRY_AFTER_HEADER retry-after
RETRY_AFTER_MS_HEADER retry-after-ms
SECRET_HEADERS frozenset({authorization, proxy-authorization, x-api-key, api-key, cookie, set-cookie})

Treat these as version-specific implementation detail; depend on the public constants module instead.

Version notes

0.6.0 changed retry/error behavior in two ways recorded in the changelog: RetryPolicy now rejects invalid values (see the validation table), and errors carry HTTP details and metadata (endpoint, richer __str__). Exceptions and responses became picklable. See Python SDK changelog.

Related

Sources