keelwave
SDKsPython

Client

The Keelwave and AsyncKeelwave clients — configuration, transport, and lifecycle.

Keelwave is the entry point for the Python SDK. It holds the API key, the server endpoint, and an httpx connection pool, and it exposes everything else in the SDK: runs, spans, decorators, and provider adapters.

import os
from keelwave import Keelwave

client = Keelwave(
    api_key=os.environ["KEELWAVE_API_KEY"],
    endpoint=os.environ.get("KEELWAVE_ENDPOINT", "http://localhost:8080"),
)

print(client.health())
client.close()

Install

pip install keelwave

Requires Python 3.10+. The SDK needs a running keelwave server to send traces to — see self-hosting.

Constructor

Both clients take the same two parameters.

Keelwave(api_key: str, endpoint: str = "http://localhost:8080")
AsyncKeelwave(api_key: str, endpoint: str = "http://localhost:8080")
ParameterTypeDefaultNotes
api_keystrrequiredSent as Authorization: Bearer <api_key>. Keys are kw_…
endpointstr"http://localhost:8080"Base URL of your keelwave server

The client does not read environment variables on its own. api_key is a required argument — read it from the environment yourself, as in the examples above. KEELWAVE_API_KEY and KEELWAVE_ENDPOINT are the names used throughout the SDK's own examples and tests, not names the constructor looks up.

Transport

Each client builds an httpx.Client (or httpx.AsyncClient) with a fixed timeout profile:

httpx.Timeout(connect=5.0, read=30.0, write=30.0, pool=5.0)

Non-2xx responses are translated into typed exceptions, and connection-level failures are wrapped in KeelwaveTransportError. See errors.

Lifecycle

Both clients are context managers, which is the simplest way to guarantee the connection pool is released.

with Keelwave(api_key=os.environ["KEELWAVE_API_KEY"]) as client:
    with client.run("research-agent", input="what is keelwave") as run:
        run.step("think", content="deciding which tool to call")

__exit__ calls client.close().

A long-lived process can also keep one module-level client and never close it — that is what the SDK examples do.

Health check

client.health()          # sync
await client.health()    # async

Calls GET /v1/health and returns the data field of the response body. Raises KeelwaveTransportError if the server is unreachable.

Instrumentation methods

These are the methods you will use day to day. Each has its own page.

MethodReturnsDocs
run(agent_name, *, input, metadata)Run / AsyncRunRuns and steps
span(step_type="span", *, name)span context managerDecorators
observe(fn=None, *, name, step_type="tool_call")decoratorDecorators
agent(fn=None, *, name)decoratorDecorators
wrap_anthropic(client, *, provider="anthropic")proxy clientAdapters
wrap_openai(client, *, provider="openai")proxy clientAdapters

Low-level ingest

The run() context manager and the decorators are built on four ingest methods. Call them directly only if you are building your own abstraction on top of the SDK — normally you should not need them.

client.ingest_ai(
    *,
    model: str,
    status: str,
    provider: str | None = None,
    input_tokens: int | None = None,
    output_tokens: int | None = None,
    total_tokens: int | None = None,
    cost_usd: float | None = None,
    latency_ms: int | None = None,
    error_message: str | None = None,
    request_id: str | None = None,
    agent_run_id: str | None = None,
    metadata: dict | None = None,
)

Posts to /v1/ingest/ai. If total_tokens is omitted but both input_tokens and output_tokens are given, the total is computed for you.

client.ingest_agent_run_start(
    *,
    agent_name: str,
    input: str | None = None,
    metadata: dict | None = None,
)

Posts to /v1/ingest/agent/runs and returns the created run's data, which carries id and timestamp.

client.ingest_agent_step(
    *,
    agent_run_id: str,
    step_index: int,
    step_type: str,
    content: str | None = None,
    tool_name: str | None = None,
    tool_input: dict | None = None,
    tool_output: dict | None = None,
    tool_success: bool | None = None,
    tool_latency_ms: int | None = None,
    tokens: int | None = None,
    cost_usd: float | None = None,
    metadata: dict | None = None,
)

Posts to /v1/ingest/agent/steps.

client.ingest_agent_run_finish(
    run_id: str,
    *,
    timestamp: str,
    status: str,
    termination_reason: str | None = None,
    total_steps: int = 0,
    total_tokens: int = 0,
    total_cost_usd: float | None = None,
    duration_ms: int | None = None,
    loop_detected: bool = False,
    loop_step_index: int | None = None,
    output: str | None = None,
)

Posts to /v1/ingest/agent/runs/{run_id}/finish and returns None. The timestamp must be the one returned by ingest_agent_run_start — keelwave stores runs in a time-partitioned table, so the finish call needs the original timestamp to locate the row.

On AsyncKeelwave all four ingest methods are coroutines and must be awaited.

On this page