Skip to content

Failing well

The error category is not paperwork. KAI-C routes on it: a transport_error is never retried, a model_error counts against the adapter, an overloaded makes KAI-C back off and come back. Getting it wrong turns one bad frame into a retry storm, or a busy adapter into one the operator is told is broken.

What the facade classifies for you

You raise Becomes
ValueError, KeyError 400 transport_error, not retried
Overloaded(retry_after_ms=…) 503 overloaded, retried after the hint
anything else, TypeError included 500 model_error, logged with a traceback
ServiceError(...) passed through exactly as you wrote it

TypeError is deliberately on the model_error side. It almost always means the handler itself is wrong — a None where a number was expected, a bad call signature — and blaming the caller for that hid real bugs behind a 400 with no traceback anywhere.

So the common cases need no error handling at all:

@adapter.on_image()
def detect(call):
    if not call.image:
        raise ValueError("a frame is required")     # → 400
    if _queue_depth() > 32:
        raise Overloaded(retry_after_ms=250)        # → 503
    return _run(call.model, call.image)             # anything else → 500

When to be explicit

Two categories the facade cannot infer, because only you know them:

    if call.param("match_faces") and not _face_matching_permitted():
        # The operator's policy, not a model failure. PERMISSION_DENIED
        # tells KAI-C this will not succeed on retry either, and the
        # message reaches the operator's audit log.
        raise ServiceError(
            ErrorCategory.PERMISSION_DENIED,
            code="face_matching_refused",
            message="Site policy does not permit face matching on this camera.",
            transient=False,
            http_status=403,
        )

provider_error is the other: an upstream your adapter fronts is down. The adapter is fine, its dependency is not, and it is transient — so KAI-C should retry.

Backpressure is honest

Silently queueing turns a slow model into growing latency nobody can see. Overloaded says so, with a number the caller can act on.

Full example: 04_errors_and_backpressure.py.