Python SDK responses, answers, usage, models
TL;DR
system_one()returns a frozenSystemOneResponsewith.model,.usage,.answersplus three cached views — the attribute names are exactly.nouls,.choices,.scores(plural, lowercase). Readresponse.nouls[name].noul(float 0–1),response.choices[name].choice/.confidence/.probabilities,response.scores[name].score/.confidence/.legend/.probabilities..request_idand.raw_http_responseexpose the HTTP layer.client.models.list()returns aListModelsResponseofModelMetadata.
SystemOneResponse
Immutable (frozen=True), keyword-only msgspec struct; subclasses the internal Response base, which also gives it request_id and raw_http_response.
| Member | Kind | Type | Default | Description |
|---|---|---|---|---|
model |
instance attribute | str |
— | The model used to answer the request. |
usage |
instance attribute | Usage |
— | Token usage for the request. |
answers |
class/instance attribute | dict[str, Answer] |
field(default_factory=dict) |
All answer objects keyed by question name. |
nouls |
cached_property |
dict[str, NoulAnswer] |
— | Yes/no answers keyed by question name. |
choices |
cached_property |
dict[str, ChoiceAnswer] |
— | Choice answers keyed by question name. |
scores |
cached_property |
dict[str, ScoreAnswer] |
— | Score answers keyed by question name. |
request_id |
cached_property |
str |
— | The x-typesafe-request-id response header. |
raw_http_response |
property |
httpx2.Response |
— | The underlying httpx2.Response (status, headers, body). |
Attribute names verified against both raw/docs/sdk__python__api__types__responses.md and _core/response_types.py: the three views are nouls, choices, scores — plural, and they are properties on the response, not on answers.
The three views are computed by filtering answers with isinstance, so answers remains the complete map and the views are mutually exclusive subsets. They are cached: the dicts are rebuilt only once per response object.
request_id and raw_http_response are runtime metadata stored in the instance __dict__ rather than schema fields; both raise TypeSafeError if the response was not built from a real HTTP response ("The response did not include a request ID." / "The response was not created from a raw HTTP response."). Responses are copyable and picklable — __copy__ and __reduce__ carry that metadata (a 0.6.0 bug fix).
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.model, response.request_id)
print(response.nouls["billing"].noul) # 0.0 – 1.0
print(response.choices["tone"].choice) # "calm" | "frustrated" | "angry"
print(response.choices["tone"].probabilities) # {"calm": 0.1, ...}
print(response.scores["urgency"].score) # e.g. 1.7
print(response.scores["urgency"].legend) # {0: "can wait", 1: "this week", 2: "today"}
print(response.usage.input_tokens, response.usage.output_tokens)
Iterating everything, type-agnostically:
from typesafe_sdk import ChoiceAnswer, NoulAnswer, ScoreAnswer
for name, answer in response.answers.items():
if isinstance(answer, NoulAnswer):
print(name, "noul", answer.noul)
elif isinstance(answer, ChoiceAnswer):
print(name, "choice", answer.choice, answer.confidence)
elif isinstance(answer, ScoreAnswer):
print(name, "score", answer.score, answer.confidence)
Answers
Answer: TypeAlias = NoulAnswer | ChoiceAnswer | ScoreAnswer
Each answer type is frozen and keyword-only, and is discriminated on the wire by its type tag ("noul", "choice", "score").
NoulAnswer
| Attribute | Type | Description |
|---|---|---|
noul |
float |
Probability of a yes answer, from zero to one. |
NoulAnswer has no confidence and no probabilities: the single float is the calibrated probability. See Noul (yes/no) questions and Confidence vs probability.
ChoiceAnswer
| Attribute | Type | Description |
|---|---|---|
choice |
str |
The selected label (one of your Choice.criteria keys). |
confidence |
float |
Reported confidence in the selected label. |
probabilities |
dict[str, float] |
Probabilities keyed by label. |
ScoreAnswer
| Attribute | Type | Description |
|---|---|---|
score |
float |
Expected score, which may fall between the integer rubric levels. |
confidence |
float |
Reported confidence in the score. |
legend |
dict[int, str | dict[str, Any] | list[Any]] |
Rubric descriptions keyed by integer score. |
probabilities |
dict[int, float] |
Probabilities keyed by integer score. |
Key typing detail: JSON object keys are strings on the wire, and the public ScoreAnswer declares dict[int, ...] so msgspec coerces them to integers at decode time. So answer.probabilities[2] (int key), not answer.probabilities["2"]. This is the inverse of Score.criteria, which since 0.6.0 is an ordered sequence — see Python SDK question types (Noul, Choice, Score).
score = response.scores["urgency"]
top_level = max(score.probabilities, key=score.probabilities.get)
print(top_level, score.legend[top_level], score.probabilities[top_level])
Confidence-gated routing, the canonical use of confidence:
tone = response.choices["tone"]
if tone.confidence >= 0.85:
auto_route(tone.choice)
else:
send_to_human(tone.probabilities)
Usage
| Attribute | Type | Default | Description |
|---|---|---|---|
input_tokens |
int | None |
None |
Input tokens used, or None when the API did not report it. |
output_tokens |
int | None |
None |
Output tokens used, or None when the API did not report it. |
Doc-vs-schema discrepancy: the generated wire struct typesafe_sdk._schemas.models.Usage has a required billing_units: int alongside optional input_tokens/output_tokens, matching the OpenAPI schema. The public typesafe_sdk.Usage used for decoding has only input_tokens and output_tokens, both defaulting to None. A comment in _core/response_types.py states the reason: "The OpenAPI Usage schema still requires billing_units, which the API does not return." So do not expect response.usage.billing_units in Python — it does not exist on the public type; see OpenAPI component schemas and Models, aliases, pricing, rate limits, context for the billing-unit contract.
Always treat both counts as optional:
usage = response.usage
if usage.input_tokens is not None:
meter(usage.input_tokens, usage.output_tokens or 0)
Raw HTTP access and forward compatibility
Decoding happens in two passes (_core/response_types.py):
- Fast path — one msgspec call decodes the whole tagged body into the public types.
- Dispatch path — used when the fast path raises a validation error. Each answer is decoded individually so that (a) unknown answer
typetags are skipped with alogger.warning("Ignoring answer %r with unrecognized type %r", ...), and (b) a genuinely malformed field raisesTypeSafeAPIResponseValidationErrorwith a precise dottedfield_pathsuch asanswers.tone.confidence.
Unknown extra fields on recognized objects are ignored (forbid_unknown_fields=False), so a newer server never breaks an older client.
To read answer kinds this SDK version does not model, go to the raw body:
raw_answers = response.raw_http_response.json()["answers"]
raw_http_response also gives you status, headers, and elapsed information:
http = response.raw_http_response
print(http.status_code, http.headers.get("x-typesafe-request-id"))
| Where | request_id behavior |
|---|---|
SystemOneResponse.request_id / ListModelsResponse.request_id |
str; raises TypeSafeError if the header was absent |
TypeSafeAPIError.request_id |
str | None; returns None if the header was absent |
Listing models
client.models.list() (sync) / await client.models.list() (async) issues GET /v1/models and returns:
ListModelsResponse
| Member | Kind | Type | Description |
|---|---|---|---|
models |
instance attribute | tuple[ModelMetadata, ...] |
The models available to the account. |
request_id |
cached_property |
str |
The x-typesafe-request-id response header. |
raw_http_response |
property |
httpx2.Response |
The underlying HTTP response. |
Note it is a tuple, not a list, and the response is frozen.
ModelMetadata
| Attribute | Type | Description |
|---|---|---|
name |
str |
Model name, e.g. an alias like jev-latest or a pinned id. |
description |
str |
Human-readable description. |
release_date |
str |
Release date as a string (no date parsing in the SDK). |
ModelMetadata is the class the wire schema calls ModelMetadata and the API groups under ModelMetadataList; there is no ModelCard symbol in the Python SDK. It is re-exported from _schemas/models.py and has no docstring upstream (the docs page lists only the three fields).
list() parameters (identical on Models and AsyncModels, all keyword-only):
| 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. |
Raises TypeSafeAPIError (unsuccessful HTTP after retries) and TypeSafeAPIConnectionError (cannot connect or timed out after retries).
from typesafe_sdk import TypeSafeClient
with TypeSafeClient() as client:
models = client.models.list()
for card in models.models:
print(f"{card.name}\t{card.release_date}\t{card.description}")
import asyncio
from typesafe_sdk import AsyncTypeSafeClient
async def main() -> None:
async with AsyncTypeSafeClient() as client:
models = await client.models.list()
print([card.name for card in models.models])
asyncio.run(main())
Error cases
| Situation | Result |
|---|---|
| Non-2xx status | The matching TypeSafeAPIError subclass is raised by Response.from_http_response; no response object is returned. |
| 2xx with a missing/invalid required field | TypeSafeAPIResponseValidationError with .field_path (e.g. answers.tone.confidence). |
2xx with an unknown answer type |
That answer is dropped from .answers and logged at WARNING; the rest decode normally. |
| 2xx with unknown extra fields | Ignored. |
Response header x-typesafe-request-id absent |
.request_id raises TypeSafeError. |
Full exception hierarchy: Python SDK retries, exceptions, constants.
Related
- Python SDK: install, clients, system_one() — clients and
system_one() - Python SDK question types (Noul, Choice, Score) — the questions that produce these answers
- Python SDK retries, exceptions, constants — exceptions raised instead of a response
- HTTP API: POST /v1/systemone and GET /v1/models — the JSON body these types decode
- OpenAPI component schemas — the generated wire schemas, including
billing_units - Models, aliases, pricing, rate limits, context — model names, aliases, pricing
- Confidence vs probability — what
confidencemeans versusprobabilities - Score questions · Choice questions · Noul (yes/no) questions
- Confidence-gated routing — routing on
confidence
Sources
- raw/docs/sdk__python__api__types__responses.md (https://docs.typesafe.ai/sdk/python/api/types/responses.md)
- raw/docs/sdk__python__api__clients__sync__models.md (https://docs.typesafe.ai/sdk/python/api/clients/sync/models.md)
- raw/docs/sdk__python__api__clients__async__models.md (https://docs.typesafe.ai/sdk/python/api/clients/async/models.md)
- raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/response_types.py, _core/schemas/base.py, _core/client/sync/models.py, _schemas/models.py (https://github.com/typesafe-ai/typesafe-sdk-python @ 420ef4ffb612d5a539a1e0f0fe883ff6770340af)