keelwave
SDKsPython

Errors

Exception types raised by the Python SDK and how to handle them.

Every SDK failure raises a subclass of KeelwaveError. Catch the base class to handle all of them, or a specific subclass when the response differs.

from keelwave import Keelwave, KeelwaveError

client = Keelwave(api_key=os.environ["KEELWAVE_API_KEY"])

try:
    with client.run("research-agent", input=task) as run:
        run.step("think", content="deciding which tool to call")
except KeelwaveError as exc:
    logger.warning("keelwave tracing failed: %s", exc)

All seven exception types are exported from the package root:

from keelwave import (
    KeelwaveError,
    KeelwaveAuthError,
    KeelwaveValidationError,
    KeelwaveRateLimited,
    KeelwaveBufferFull,
    KeelwaveServerError,
    KeelwaveTransportError,
)

Hierarchy

Exception
└── KeelwaveError
    ├── KeelwaveAuthError          401
    ├── KeelwaveValidationError    400
    ├── KeelwaveRateLimited        429   (.retry_after)
    ├── KeelwaveBufferFull         503   (.retry_after)
    ├── KeelwaveServerError        5xx
    └── KeelwaveTransportError     network failure

Reference

KeelwaveError

Base class for every SDK error. Catch this to handle all keelwave failures in one place. It is also raised directly for any non-2xx status the SDK does not map to a more specific type, with the message formatted as "{status}: {body}".

KeelwaveAuthError

Raised on HTTP 401. The API key is missing, malformed, or revoked.

Not retryable. Check that api_key holds a real kw_… key for the project you are pointing at, and that endpoint is the right server.

from keelwave import KeelwaveAuthError

try:
    client.health()
except KeelwaveAuthError:
    raise SystemExit("KEELWAVE_API_KEY is invalid or revoked")

KeelwaveValidationError

Raised on HTTP 400. The server's validator rejected the payload.

Not retryable — the same payload will fail again. In practice this means a field outside its allowed set, such as a status the server does not recognise, or a required field left empty. Fix the call site.

from keelwave import KeelwaveValidationError

try:
    client.ingest_ai(model="gpt-4o", status="not-a-valid-status")
except KeelwaveValidationError as exc:
    logger.error("rejected by server validator: %s", exc)

KeelwaveRateLimited

Raised on HTTP 429. The per-IP or per-key bucket is exhausted.

Retryable after a wait. The exception carries retry_after, parsed from the Retry-After response header; it is None when the header is absent or not an integer.

import time
from keelwave import KeelwaveRateLimited

try:
    run.step("think", content="deciding which tool to call")
except KeelwaveRateLimited as exc:
    time.sleep(exc.retry_after or 5)

KeelwaveBufferFull

Raised on HTTP 503. The server's ingest buffer is full — writes are arriving faster than they can be flushed.

Retryable after a wait, and it also carries retry_after from the Retry-After header. If you see this steadily rather than in bursts, the server is undersized for your write volume rather than your client misbehaving.

import time
from keelwave import KeelwaveBufferFull

try:
    run.tool_call("web_search", input={"q": query}, output=results)
except KeelwaveBufferFull as exc:
    time.sleep(exc.retry_after or 1)

KeelwaveServerError

Raised on any 5xx other than 503. A server bug or a database outage. The message is formatted as "{status}: {body}".

Retryable with backoff, but if it persists the problem is server-side — check the keelwave server's own logs.

KeelwaveTransportError

Raised on a network failure before any HTTP response. DNS failure, connection refused, TLS error, or a timeout against the client's fixed timeout profile (5s connect, 30s read, 30s write, 5s pool). The message is the string form of the underlying httpx.RequestError, which is preserved as the __cause__.

Most often it means the endpoint is wrong or the server is not running.

from keelwave import KeelwaveTransportError

try:
    client.health()
except KeelwaveTransportError as exc:
    raise SystemExit(f"cannot reach keelwave at {client.endpoint}: {exc}")

Where errors surface

CallCan raise
client.health()all types
client.ingest_*(...)all types
Entering with client.run(...)all types (the start call)
run.step() / run.tool_call()all types (the step call)
Exiting the with blockall types (the finish call)
run.set_output() / run.mark_loop() / run.check_fingerprint()none — local state only
@client.observe / client.span()none — emit failures become warnings

@client.agent is the exception worth calling out: it wraps a real run, so a keelwave failure raised while opening or closing that run propagates out of your decorated function.

Keeping tracing non-fatal

Observability should not take down the thing it observes. @client.observe and client.span() already swallow emit failures into warnings.warn. For manual runs, wrap the block yourself:

from contextlib import contextmanager
from keelwave import KeelwaveError


@contextmanager
def traced(client, name, **kwargs):
    """Yield a Run, or None if keelwave is unavailable. Never raises
    KeelwaveError out of the with-block."""
    try:
        run = client.run(name, **kwargs)
        run.__enter__()
    except KeelwaveError as exc:
        logger.warning("keelwave run failed to open: %s", exc)
        yield None
        return

    try:
        yield run
    finally:
        try:
            run.__exit__(None, None, None)
        except KeelwaveError as exc:
            logger.warning("keelwave run failed to close: %s", exc)

This swallows keelwave failures but also loses the failed-run status, because it always closes the run as completed. Use it when tracing must never break the caller; use a plain with client.run(...) when accurate run status matters more.

Retry only KeelwaveRateLimited, KeelwaveBufferFull, KeelwaveServerError, and KeelwaveTransportError. Retrying KeelwaveAuthError or KeelwaveValidationError just repeats the same failure.

import time
from keelwave import (
    KeelwaveBufferFull,
    KeelwaveRateLimited,
    KeelwaveServerError,
    KeelwaveTransportError,
)

RETRYABLE = (
    KeelwaveRateLimited,
    KeelwaveBufferFull,
    KeelwaveServerError,
    KeelwaveTransportError,
)


def with_retry(fn, attempts=3):
    for attempt in range(attempts):
        try:
            return fn()
        except RETRYABLE as exc:
            if attempt == attempts - 1:
                raise
            time.sleep(getattr(exc, "retry_after", None) or 2**attempt)

On this page