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 keelwaveRequires 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")| Parameter | Type | Default | Notes |
|---|---|---|---|
api_key | str | required | Sent as Authorization: Bearer <api_key>. Keys are kw_… |
endpoint | str | "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() # asyncCalls 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.
| Method | Returns | Docs |
|---|---|---|
run(agent_name, *, input, metadata) | Run / AsyncRun | Runs and steps |
span(step_type="span", *, name) | span context manager | Decorators |
observe(fn=None, *, name, step_type="tool_call") | decorator | Decorators |
agent(fn=None, *, name) | decorator | Decorators |
wrap_anthropic(client, *, provider="anthropic") | proxy client | Adapters |
wrap_openai(client, *, provider="openai") | proxy client | Adapters |
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.