---
title: "Python SDK responses, answers, usage, models"
type: reference
tags: [python, sdk, responses, answers, usage, models]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/sdk__python__api__types__responses.md
  - raw/docs/sdk__python__api__clients__sync__models.md
  - raw/docs/sdk__python__api__clients__async__models.md
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/response_types.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_core/schemas/base.py
  - raw/github/typesafe-sdk-python/src/typesafe_sdk/_schemas/models.py
jev_version: "jev-1.13.0"
sdk_python: "0.6.0"
summary: "SystemOneResponse fields and the .nouls/.choices/.scores views, every answer attribute, Usage, request_id and raw_http_response, plus ListModelsResponse and ModelMetadata."
---

# Python SDK responses, answers, usage, models

> **TL;DR** `system_one()` returns a frozen `SystemOneResponse` with `.model`, `.usage`, `.answers` plus three cached views — the attribute names are exactly **`.nouls`**, **`.choices`**, **`.scores`** (plural, lowercase). Read `response.nouls[name].noul` (float 0–1), `response.choices[name].choice` / `.confidence` / `.probabilities`, `response.scores[name].score` / `.confidence` / `.legend` / `.probabilities`. `.request_id` and `.raw_http_response` expose the HTTP layer. `client.models.list()` returns a `ListModelsResponse` of `ModelMetadata`.

## `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).

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

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

```python
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 [[concepts/noul]] and [[concepts/confidence]].

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

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

```python
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 [[reference/openapi-schemas]] and [[reference/models-and-pricing]] for the billing-unit contract.

Always treat both counts as optional:

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

1. **Fast path** — one msgspec call decodes the whole tagged body into the public types.
2. **Dispatch path** — used when the fast path raises a validation error. Each answer is decoded individually so that (a) unknown answer `type` tags are *skipped* with a `logger.warning("Ignoring answer %r with unrecognized type %r", ...)`, and (b) a genuinely malformed field raises `TypeSafeAPIResponseValidationError` with a precise dotted `field_path` such as `answers.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:

```python
raw_answers = response.raw_http_response.json()["answers"]
```

`raw_http_response` also gives you status, headers, and elapsed information:

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

```python
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}")
```

```python
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: [[reference/python-sdk-retries-errors]].

## Related

- [[reference/python-sdk]] — clients and `system_one()`
- [[reference/python-sdk-questions]] — the questions that produce these answers
- [[reference/python-sdk-retries-errors]] — exceptions raised instead of a response
- [[reference/http-api]] — the JSON body these types decode
- [[reference/openapi-schemas]] — the generated wire schemas, including `billing_units`
- [[reference/models-and-pricing]] — model names, aliases, pricing
- [[concepts/confidence]] — what `confidence` means versus `probabilities`
- [[concepts/score]] · [[concepts/choice]] · [[concepts/noul]]
- [[patterns/confidence-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)
