Sonny Labs Docs
SDK ReferencePython

Problem envelope

The RFC 9457 application/problem+json shape the SDK consumes, plus how it threads onto every SonnyLabsError as the raw problem attribute.

The Sonny Labs API surfaces every error as an RFC 9457 application/problem+json envelope. The Python SDK parses the envelope and raises a typed SonnyLabsError subclass, but it also preserves the raw envelope on the problem attribute for forward compatibility with future fields.

Pydantic model

The wire shape is described by the generated Problem Pydantic v2 model, re-exported from the top-level package:

from sonnylabs import Problem

The fields the SDK reads off the envelope are:

FieldTypeDescription
typestr (URI)Problem type URI; for Sonny Labs errors this resolves under https://docs.sonnylabs.ai/errors/<code>.
titlestrShort, human-readable summary. Surfaced in the SDK exception message.
statusintHTTP status code. Echoes the response status; preserved on the typed exception's status attribute.
codestrMachine-readable, dot-namespaced identifier (e.g. auth.api_key.expired). Branch on this — never on title or detail.
detailstr | NonePer-occurrence detail, often references a specific request id or field. Free-form; do not match against it.
request_idstr | NoneEchoed X-Request-Id for correlation with server logs.
trace_idstr | NoneDistributed-trace id when tracing is enabled; absent in the SaaS default.
errorslist[ProblemFieldError]?Per-field validation errors when code is in the validation.* namespace. Empty otherwise.

How it threads onto exceptions

Every SonnyLabsError carries the parsed envelope on the problem attribute:

from sonnylabs import SonnyLabsError, ValidationError

try:
    client.create_scan(
        surface="user_message",
        content={"type": "text", "text": user_prompt},
    )
except ValidationError as e:
    # The named attributes cover the common case.
    print(e.code, e.status, e.title)

    # The raw envelope is preserved for forward compatibility — read
    # any future field off here without waiting for an SDK release.
    print(e.problem.get("type"))
    for field in e.errors:
        # Each entry is a {path, code, detail} dict from the envelope.
        print(field.get("path"), field.get("code"))
except SonnyLabsError as e:
    # Catch-all — `e.problem` is still populated.
    print(e.problem)

Synthesised envelopes for non-JSON failures

When the server returns a non-JSON body — typically a load-balancer error page slipping through with a 5xx — the client synthesises a minimal envelope so the caller still gets a typed exception:

{
    "type": "about:blank",
    "title": "<reason phrase or 'HTTP error'>",
    "status": <status>,
    "code": "http.<status>",
    "detail": "<first 500 chars of the body>",
}

Network-level failures (connection refused, DNS error) raise a SonnyLabsError with code="transport.error" and status=0; the underlying httpx exception is preserved as the cause via raise … from exc.

On this page