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 ProblemThe fields the SDK reads off the envelope are:
| Field | Type | Description |
|---|---|---|
type | str (URI) | Problem type URI; for Sonny Labs errors this resolves under https://docs.sonnylabs.ai/errors/<code>. |
title | str | Short, human-readable summary. Surfaced in the SDK exception message. |
status | int | HTTP status code. Echoes the response status; preserved on the typed exception's status attribute. |
code | str | Machine-readable, dot-namespaced identifier (e.g. auth.api_key.expired). Branch on this — never on title or detail. |
detail | str | None | Per-occurrence detail, often references a specific request id or field. Free-form; do not match against it. |
request_id | str | None | Echoed X-Request-Id for correlation with server logs. |
trace_id | str | None | Distributed-trace id when tracing is enabled; absent in the SaaS default. |
errors | list[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.
Exception classes
The typed exception hierarchy raised by SonnyLabsClient — SonnyLabsError plus per-namespace subclasses for auth, scope, validation, idempotency, rate-limit, not-found, and server errors.
verify_webhook
Constant-time HMAC-SHA256 verification for inbound Sonny Labs webhook deliveries, plus the tolerance and clock-injection knobs.