feat: expose the API's declared error verdict as typed attributes

CY
31s ago
+236-4

What

HTTP errors from the client now carry the verdict the API declared, as typed attributes. Callers no longer have to re-parse err.body.

# Response: 503 {"detail": "OCR timed out", "retryable": false,
#                "error_type": "timeout", "error_reason": "ocr_timeout", "audience": "platform"}

# Before
err.retryable                              # AttributeError
json.loads(err.body).get("retryable")      # False, but every caller re-implements the parsing

# After
err.retryable     # False
err.error_type    # "timeout"
err.error_reason  # "ocr_timeout"
err.audience      # "platform"
Error bodyretryable / error_type / error_reason / audience
Declares all four fieldsthe declared values
{"detail": "Internal server error"} (declares nothing)all None
A field with the wrong type, e.g. "retryable": "false"that field is None
Not JSON, or not a JSON objectall None

The attributes are on UnstructuredClientError, so they work for every HTTP error class at any status (SDKError for 4xx, ServerError, HTTPValidationError). That includes the response a failed split-PDF chunk returns.

Why

The API publishes retryable, error_type, error_reason and audience next to detail on errors it raises deliberately. The client's error models only expose detail, so any caller that wants to act on the verdict has to parse the raw body itself. Each caller then re-implements the same type checks and tends to get the edge cases slightly different.

The retry loop already reads retryable to stop retrying a declared non-retryable error (0.46.4). The loop and the error attributes now share one parser, so they cannot disagree about what a body declares. Both decode the response bytes, so a UTF-8 BOM or a UTF-16 body is read the same way response.json() reads it.

Notes for consumers

  • Additive only. No existing attribute, message or retry decision changes.
  • A ResponseValidationError raised on a 2xx response always reads None, and its (possibly large) body is not parsed again.
  • An unread streamed response is never consumed to read the verdict.

Validation

  • Unit suite: 403 passed, 1 xfailed. make lint is clean (pylint 10/10, mypy).
  • New tests cover the parser's type rules, each error class with UTF-8, UTF-8-BOM and UTF-16 bodies, the unread-stream fallback, 2xx validation errors, and public partition / partition_async (400 and 503) through a mocked transport.
  • Four rounds of Codex review. Rounds 2 and 3 found that decoding text instead of bytes lost JSON encoding detection, in the retry check and then in the error attributes. Both are fixed with tests.