Skip to content

Handling errors

When a call can't give you a valid answer, it raises an exception that tells you why. Each of these exceptions still carries the call's record, so you can see the tokens you paid for and what the model actually said.

The exceptions

StructuredOutputError          base class; every one has .record
├── RefusalError               the model refused to answer
├── ContentFilterError         Azure's content filter blocked the prompt or the response
├── TruncatedError             the answer hit the token limit before the JSON was complete
└── ValidationFailedError      the answer still failed validation after every retry
Exception record.status What to do
RefusalError refusal Rephrase the request, or treat it as "can't answer".
ContentFilterError content_filter Check the input. Azure blocked the prompt (HTTP 400) or the output.
TruncatedError truncated Raise max_completion_tokens, or ask for less output. Reasoning models need extra room.
ValidationFailedError invalid Raise validation_retries, loosen the validator, or make the schema or instructions clearer.

All of them are recorded before they're raised: they count in az.tracker and track() scopes, appear in the log, and are passed to hooks.

Catching them

import azure_openai_utils as az

try:
    res = az.extract(text, Invoice)
except az.TruncatedError:
    res = az.extract(text, Invoice, max_completion_tokens=4000)  # try again with more room
except az.StructuredOutputError as err:
    rec = err.record
    print(f"failed: {rec.status} after {rec.attempts} attempt(s)")
    print("errors:", rec.errors)
    print("model said:", rec.raw_output)
    print("tokens spent:", rec.usage.total_tokens, "cost:", rec.cost_usd)
    raise

What gets retried automatically

Problem Retried? By
The answer fails Pydantic validation (or isn't valid JSON) Yes, validation_retries times (default 1), with the errors shown to the model this module
Rate limit (429), timeouts, connection errors, 5xx Yes, up to max_retries times (default 3), with backoff the OpenAI SDK
Refusal, content filter, truncation No, raised immediately n/a

Errors from the OpenAI SDK

Network and API errors (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError, openai.AuthenticationError, openai.NotFoundError, ...) are raised unchanged, after the SDK has used up its own retries. They're still recorded with status="error":

import openai

try:
    res = az.structured(prompt, Summary)
except openai.RateLimitError:
    ...  # back off at a higher level
except openai.NotFoundError:
    ...  # usually a wrong deployment name

The one exception to "unchanged" is Azure's content filter. Azure rejects a filtered prompt with an HTTP 400 whose code is content_filter, and that's converted to ContentFilterError, so both kinds of filtering can be caught the same way. The original SDK error is kept as __cause__.

Cancelled calls

A call that is cancelled (an asyncio.wait_for timeout, a cancelled task) or interrupted (Ctrl+C) is recorded with status="cancelled", and then the CancelledError or KeyboardInterrupt carries on unchanged. Tokens from attempts that finished before the cancellation, such as a failed validation attempt, still count in the tracker and the log. A request cut off mid-flight has no usage to report, so it adds no tokens.

Pressing Ctrl+C during structured_many stops it from sending the prompts that haven't started. The ones already running finish and are recorded, then the KeyboardInterrupt is raised.

Errors in batches

In structured_many and astructured_many, the first error is raised and the rest of the batch is cancelled. astructured_many records the calls it cancels before it raises. In structured_many, calls that are already running finish (a thread can't be interrupted), and calls that haven't started are never sent. To collect failures instead, use return_exceptions=True:

results = az.structured_many(texts, Invoice, return_exceptions=True)

for text, res in zip(texts, results):
    if isinstance(res, az.StructuredOutputError):
        print("model problem:", res.record.status, text[:60])
    elif isinstance(res, Exception):
        print("API problem:", type(res).__name__, text[:60])
    else:
        save(res.parsed)

Setup mistakes

These are raised before any request is sent. They aren't recorded, because no call happened:

Error Cause
ValueError: No deployment Neither deployment= nor AZURE_OPENAI_DEPLOYMENT is set.
ValueError: No Azure endpoint Neither endpoint= nor AZURE_OPENAI_ENDPOINT is set.
RuntimeError: No API key ... No API key, and azure-identity isn't installed for Entra ID.
TypeError: ... sets model itself You passed model=, messages=, response_format= or stream=. Use deployment= instead of model=.
ValueError: mode must be 'strict' or 'json' Typo in mode=.

See Troubleshooting for more.