> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitfab.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK Reference

> Pure API reference for the bitfab-py PyPI package.

Package: `bitfab-py` (imported as `bitfab`). Python ≥ 3.10.

## Module Exports

```python theme={null}
from bitfab import (
    BITFAB_PROGRESS_PREFIX,
    AllowedEnvVars,
    Bitfab,
    BitfabFunction,
    BitfabTracingProcessor,   # only if openai-agents is installed
    CapturedSpan,
    CurrentSpan,
    CurrentTrace,
    DetachedTrace,
    ReplayItem,
    ReplayResult,
    SpanType,
    SpanOccurrence,
    finalizers,               # finalizers.openai_chunks, finalizers.anthropic_events
    flush_traces,
    get_current_span,
    get_current_trace,
    report_replay_progress,
)
```

`BitfabTracingProcessor` is only exported when `openai-agents` is installed. `from bitfab import BitfabTracingProcessor` will raise `ImportError` otherwise.

## Type Aliases

```python theme={null}
SpanType = Literal["llm", "agent", "function", "guardrail", "handoff", "custom"]
```

## `class Bitfab`

### `__init__`

```python theme={null}
Bitfab(
    api_key: str,
    service_url: Optional[str] = None,
    env_vars: Optional[AllowedEnvVars] = None,
    enabled: bool = True,
    baml_client: Any = None,
)
```

| Param         | Type             | Default               | Description                                                         |
| ------------- | ---------------- | --------------------- | ------------------------------------------------------------------- |
| `api_key`     | `str`            | required              | Empty/whitespace disables tracing with a warning                    |
| `service_url` | `Optional[str]`  | `"https://bitfab.ai"` | Base URL                                                            |
| `env_vars`    | `AllowedEnvVars` | `{}`                  | LLM provider keys for `call()`                                      |
| `enabled`     | `bool`           | `True`                | When `False`, `@span` decorator returns the function unwrapped      |
| `baml_client` | `Any`            | `None`                | Generated BAML client for `wrap_baml()` without explicit client arg |

### `span`

```python theme={null}
def span(
    trace_function_key: str,
    *,
    name: Optional[str] = None,
    type: SpanType = "custom",
    test_run_id: Optional[str] = None,
    mock_on_replay: bool = False,
    finalize: Optional[Callable[[Any], Any]] = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]
```

Decorator. Wraps the decorated function (sync or async) with a span.

**Returns:** a decorator that returns a function with the same signature.

**Semantics:**

* When `enabled=False`, returns the function unchanged
* `name` defaults to the function's `__name__`
* Nested spans propagate via `contextvars.ContextVar` -- safe across `asyncio.gather`, threads, and sync/async boundaries
* Exceptions are recorded on the span and re-raised
* `test_run_id` is rarely set directly; `replay()` injects it via a replay context
* `finalize` records a serializable view of a streaming result as the span output (the raw result is always returned to the caller, unchanged). On an **async generator** it receives the list of yielded chunks, assembled after iteration so the caller has already streamed every chunk (non-blocking, non-destructive). On a plain sync/async function it receives the return value and is applied inline before the span is recorded, so a live single-consumer stream is blocked on and consumed. Prefer an async generator for streaming. May be `async` on async/async-generator spans, must be sync on sync spans. A throwing `finalize` records an error instead of crashing. Pair with `finalizers.openai_chunks` / `finalizers.anthropic_events`. See [Tracing streaming functions](/python-sdk#tracing-streaming-functions)

### `get_function`

```python theme={null}
def get_function(trace_function_key: str) -> BitfabFunction
```

### `wrap_baml`

Framework integration → see [BAML framework guide](/frameworks/baml) for examples.

```python theme={null}
# Form 1 -- uses baml_client from constructor
def wrap_baml(
    method: Callable[..., Any],
    *,
    on_collector: Callable[[Any], None] | None = None,
) -> Callable[..., Any]

# Form 2 -- explicit client
def wrap_baml(
    baml_client: Any,
    method: Callable[..., Any],
    *,
    on_collector: Callable[[Any], None] | None = None,
) -> Callable[..., Any]
```

**Returns:** an `async` wrapper with the same signature as `method`, plus a `.collector` attribute holding the most recent call's BAML `Collector` (`None` before the first call or if `baml-py` is not installed). Pass `on_collector` to receive the `Collector` after each call.

**Raises:**

* `ValueError` if form 1 is used without `baml_client` in constructor
* `ValueError` if the method has no `__name__`

**Semantics:**

* If `baml-py` is not installed, the method is called directly without instrumentation
* Otherwise calls `get_current_span().set_prompt(...)` and `get_current_span().add_context({...})` with extracted metadata
* Must be invoked inside a `@span`-decorated function for the prompt/context to attach to anything

### `get_trace`

```python theme={null}
def get_trace(trace_id: str) -> DetachedTrace
```

Returns a `DetachedTrace` handle for annotating a trace after its root span has closed, from any process or thread.

**Raises:** `ValueError` if `trace_id` is not a canonical Bitfab trace ID.

**Semantics:**

* All methods on the returned handle are fire-and-forget (return `Optional[threading.Thread]`)
* When `enabled=False`, methods return `None` immediately
* Pending requests are tracked so `flush_traces()` waits for them
* Server returns 404 if no trace exists with that ID; failure is logged as a warning

### `get_trace_span`

```python theme={null}
def get_trace_span(
    trace_id: str,
    *,
    id: Optional[str] = None,
    name: Optional[str] = None,
    occurrence: Literal["first", "last"] | int = "last",
) -> Optional[CapturedSpan]
```

Fetches one persisted span without loading its trace. `trace_id` is the canonical Bitfab trace ID. Exactly one of the span's Bitfab `id` or `name` is required. Numeric occurrences are zero-based in start-time order. Returns `None` when no trace or span matches.

### `replay`

```python theme={null}
def replay(
    fn_or_key: Callable | str,
    fn: Optional[Callable] = None,  # required when fn_or_key is a key string
    *,
    limit: Optional[int] = None,  # default 5; max 5,000; ignored with trace_ids/dataset_id
    trace_ids: Optional[list[str]] = None,  # max 100
    name: str | None = None,  # display name for the resulting experiment/test run
    max_concurrency: Optional[int] = 10,
    experiment_group_id: str | None = None,
    environment: ReplayEnvironment | None = None,  # per-trace DB branch
    dataset_id: str | None = None,  # durably attributes the experiment to a dataset
    grader_ids: list[str] | None = None,  # graders attached to this run, unioned with the dataset's at grading (max 100)
    on_progress: Callable[[ReplayProgress], None] | None = None,  # per-item running totals
) -> ReplayResult
```

`on_progress` fires once per item as it settles with running totals (`completed`, `total`, `succeeded`, `errored`), `test_run_id`, and `item`, the single trace that just settled. `item.trace_id` is `None` during the run (the server replay id isn't known until completion), while `item.original_trace_id` is the original historical trace (`source_trace_id` is a deprecated alias); the item also carries `input`, `result`, `original_output`, `error`, `duration_ms`, `tokens`, `model`, and `db_snapshot_ref`. Replay doesn't know pass/fail yet (verdicts are assigned later), so the totals only split ran-ok vs errored. A raising callback never crashes the run. The SDK exports a ready-made reporter, `report_replay_progress`, that you can pass straight in (`on_progress=report_replay_progress`): it writes the running totals plus settled item payload to stderr, which the Bitfab plugin polls to report live progress and write per-item result files while the replay runs in the background, so scripts never hand-format the protocol.

Two call forms. `replay(decorated_fn)`: the trace function key is read from the `@span` decorator (raises if undecorated). `replay("key", fn)`: explicit key with any plain callable; the SDK wraps it in a span under the key internally. The explicit-key form is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent SDK, OpenAI Agents) replay, since they have no decorated root in the app. When replay auto-wraps a plain callable, a recorded dict root input (e.g. a LangGraph state) is passed as a single positional argument (matching TypeScript); decorated functions always get the keyword-args splat, with or without a redundant matching key. An explicit key that contradicts the decorator's key raises. `max_concurrency=None` means unlimited; `1` means sequential. When `trace_ids` is passed, `limit` is ignored with a warning (an explicit ID list already determines how many traces replay). When `environment` is passed, the server resolves a per-trace database branch and exposes it inside `fn` via `environment.database_url` (read `environment.active` first; it is `False` when no branch was resolved).

A trace replays only when its root span has serializable inputs, or it was instrumented through a [framework handler](/python-sdk#replaying-handler-instrumented-functions) (whose recorded root input is serializable). If the original inputs were stubbed as non-serializable at capture time, the trace cannot be replayed.

### `call`

```python theme={null}
def call(method_name: str, **kwargs: Any) -> Any
```

Executes a server-configured BAML function locally using `env_vars`.

**Raises:** `ValueError` on lookup failure or execution error.

### Framework Integrations

Handlers returned by these methods plug into each framework's callback/processor/hook surface and emit Bitfab spans automatically. For usage examples and semantics, see the per-framework guides.

#### `get_langgraph_callback_handler`

```python theme={null}
def get_langgraph_callback_handler(trace_function_key: str) -> BitfabLangGraphCallbackHandler
```

Returns a LangChain/LangGraph `BaseCallbackHandler`. Pass via `config={"callbacks": [handler]}` when invoking. The handler-created root is replayable from the framework input, so a separate `@span` root is only needed for meaningful surrounding application work. See [LangGraph framework guide](/frameworks/langgraph).

Aliased as `get_langchain_callback_handler(trace_function_key)` for plain LangChain projects; the returned handler and behavior are identical. The handler class is also exported as `BitfabLangChainCallbackHandler`.

#### `get_openai_tracing_processor`

```python theme={null}
def get_openai_tracing_processor() -> BitfabOpenAITracingProcessor
```

**Raises:** `ImportError` if `openai-agents` is not installed. Captures agent internals; pair it with `get_openai_agent_handler` for a replayable root. See [OpenAI Agents framework guide](/frameworks/openai-agents).

#### `get_openai_agent_handler`

```python theme={null}
def get_openai_agent_handler(trace_function_key: str) -> BitfabOpenAIAgentHandler
```

Returns a handler whose `wrap_run(agent, input, **run_kwargs)` is a drop-in for `Runner.run` that records a keyed, replayable root span carrying the run input (the tracing processor's spans nest underneath). For streamed runs, `wrap_run_streamed(agent, input, **run_kwargs)` is an async-generator drop-in for `Runner.run_streamed`: iterate it to consume the same stream events while the run is traced. (`bitfab.replay()` re-runs the non-streaming `wrap_run`, not this async generator.) See [OpenAI Agents framework guide](/frameworks/openai-agents).

#### `get_claude_agent_handler`

```python theme={null}
def get_claude_agent_handler(trace_function_key: str) -> BitfabClaudeAgentHandler
```

Returns a handler exposing `instrument_options(options)`, `wrap_response(stream, input=...)`, and `wrap_query(stream, input=...)` for the Claude Agent SDK. Pass `input=prompt` to the wrap call to record a replayable root span. See [Claude Agent SDK framework guide](/frameworks/claude-agent-sdk).

#### `wrap_baml`

See the [BAML framework guide](/frameworks/baml) for examples; full signature under [`wrap_baml`](#wrap-baml) above.

## `class BitfabFunction`

Returned from `client.get_function(key)`.

```python theme={null}
def span(
    *,
    name: Optional[str] = None,
    type: SpanType = "custom",
) -> Callable[[Callable[P, T]], Callable[P, T]]

def get_claude_agent_handler() -> BitfabClaudeAgentHandler

def get_langgraph_callback_handler() -> BitfabLangGraphCallbackHandler

def get_langchain_callback_handler() -> BitfabLangGraphCallbackHandler

def wrap_baml(
    method_or_client: Any,
    method: Optional[Callable] = None,
) -> Callable[..., Any]
```

Delegates to the parent `Bitfab` instance with the bound `trace_function_key`. The `get_*_handler` methods reuse the bound key so a `span` root and the handler share it (the documented same-key nesting pattern), with no repeated string. `wrap_baml` is the exception: it opens no span and uses no key, enriching the *current* span instead, so call it inside a function wrapped by this handle's `span`.

## Module Functions

### `get_current_span()`

```python theme={null}
def get_current_span() -> CurrentSpan | _NoOpCurrentSpan
```

Returns a no-op object when called outside a span context. The no-op's methods do nothing and never raise.

### `get_current_trace()`

```python theme={null}
def get_current_trace() -> CurrentTrace | _NoOpCurrentTrace
```

Returns a no-op object when called outside a span context.

### `flush_traces(timeout: float = 30.0)`

```python theme={null}
def flush_traces(timeout: float = 30.0) -> None
```

Waits for pending trace threads to join, up to `timeout` seconds. Use before process exit in short-lived scripts.

## Classes (Context Handles)

### `CurrentSpan`

```python theme={null}
class CurrentSpan:
    @property
    def id(self) -> str
    @property
    def trace_id(self) -> str
    def add_context(self, context: dict[str, Any]) -> None
    def set_prompt(self, prompt: str) -> None
```

| Method        | Semantics                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------- |
| `id`          | Canonical Bitfab span ID                                                                    |
| `trace_id`    | UUID string                                                                                 |
| `add_context` | Appends the dict as one entry to `span_data.contexts`. Non-dict input ignored. Never raises |
| `set_prompt`  | Overwrites `span_data.prompt`. Non-string input ignored. Never raises                       |

### `CurrentTrace`

```python theme={null}
class CurrentTrace:
    def set_session_id(self, session_id: str) -> None
    def set_metadata(self, metadata: dict[str, Any]) -> None
    def add_context(self, context: dict[str, Any]) -> None
    def drop(self) -> None
```

`set_metadata` shallow-merges with later keys winning. `add_context` accumulates entries. `drop` flags the trace to be dropped: once flagged, spans that complete afterward are not uploaded at all, and the flag rides out on the completion payload, so at completion the server scrubs any payloads that already raced out (the trace, its external trace, and sibling spans), deletes the archived S3 objects, and marks it `dropped` instead of `completed`, keeping only a skeleton audit row. It is a no-op outside a span and never raises.

### `DetachedTrace`

```python theme={null}
class DetachedTrace:
    trace_id: str   # read-only property
    def add_context(self, context: dict[str, Any]) -> Optional[threading.Thread]
    def set_metadata(self, metadata: dict[str, Any]) -> Optional[threading.Thread]
    def set_session_id(self, session_id: str) -> Optional[threading.Thread]
```

Returned by `client.get_trace(trace_id)`, where `trace_id` is the canonical Bitfab trace ID. Methods have the same semantics as `CurrentTrace` but send to the server immediately (fire-and-forget). Returns a `threading.Thread` you can `.join()` for confirmation, or `None` if the client is disabled or input validation fails.

## TypedDicts & Dataclasses

### `AllowedEnvVars`

```python theme={null}
class AllowedEnvVars(TypedDict, total=False):
    OPENAI_API_KEY: str
```

### `ReplayItem`

```python theme={null}
@dataclass
class ReplayItem:
    input: list[Any]
    result: Any
    original_output: Any
    error: str | None
    duration_ms: int | None  # the original trace's duration
    tokens: TokenUsage | None  # the replayed run's token usage (compare vs the original)
    model: str | None  # the original trace's model
    trace_id: str | None
    db_snapshot_ref: dict | None  # the source trace's snapshot pin, if any
```

### `ReplayResult`

```python theme={null}
@dataclass
class ReplayResult:
    items: list[ReplayItem]
    test_run_id: str
    test_run_url: str
```

## Error Behavior Summary

| Situation                                                   | Behavior                                                                 |
| ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| Empty `api_key`                                             | Warning logged, `enabled` forced to `False`                              |
| `@span` transport failure                                   | Swallowed. User's return value / exception passes through                |
| Decorated function raises                                   | Span records `error` and `error_source: "code"`, exception re-raised     |
| `add_context` / `set_prompt` with invalid input             | Silently ignored                                                         |
| `call()` -- function not found                              | `ValueError` with URL to `/functions`                                    |
| `call()` -- no prompt                                       | `ValueError` with URL to `/functions/{id}`                               |
| `wrap_baml` -- missing client                               | `ValueError`                                                             |
| `replay("key", fn)` -- plain callable (no `@span`)          | Auto-wrapped under the key, so spans link to the test run (not an error) |
| `replay(fn)` -- `fn` undecorated and no explicit key        | `ValueError` (decorate with `@span`, or pass an explicit key)            |
| `replay("key", fn)` -- `fn` decorated under a different key | `ValueError` (key mismatch)                                              |
| Import `BitfabTracingProcessor` without `openai-agents`     | `ImportError`                                                            |
