# Python SDK

The complete Python surface — every exported name, its signature, and where the behaviour is documented in full.

Source: https://tokentriage.com/docs/sdks/python

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import Details from '../../../components/Details.astro';

The complete public surface of `tokentriage` for Python, in one place. Behaviour is documented on
the topic pages; this is the list of what exists and what each call takes.

```python
import tokentriage as tt
```

Requires Python 3.10 or newer. Local unreleased version `1.0.0rc2`, contract version 1, zero runtime dependencies.

## Configured provider client

The unreleased `tokentriage.client.TokenTriage` accepts `base_url`, `api_key`
and `mode="managed" | "self_hosted"`. It reads `TOKENTRIAGE_BASE_URL` and
`TOKENTRIAGE_API_KEY` when arguments are omitted.

Use `openai()` or `async_openai()` to construct official provider clients.
`openai_options()` and `async_openai_options()` support your own constructor
and borrowed HTTP transport. `metadata(response)` and `error_details(error)`
read information tied to that response or error.

Those four, plus `native()` and `async_native()`, take the saved-mode binding:
`gateway_id` (required), `environment_id`, `base_url`, `credential_source` and
`provider_api_key`. Supplying `gateway_id` selects saved mode, where `api_key`
becomes the `Authorization` bearer and the gateway and environment selector
headers are stamped for you; send the literal `"gateway"` as the model.
`provider_api_key` needs `credential_source="request_supplied"`, and a provider
key passed without it raises `ConfigurationError` naming
`provider_key_not_allowed` before a request is built.

Start with [a routed request](/docs/sdks/first-request) and
[async and streaming examples](/docs/sdks/adapters).

## Guardrail refusals

A gateway that refuses a request on a guardrail answers `403` for a policy block and `503` for a
guard whose detector could not run, with the typed refusal at the top level of the body and
`x-tt-guardrail: blocked | unavailable` on the response. The SDK-owned transport raises it as its
own exception rather than handing back a response the provider client would read as an empty error.

**`GuardrailRefusal`** (from `tokentriage.client`), raised by `openai()`, `async_openai()` and
`native()` clients:

```python
from tokentriage.client import GuardrailRefusal

try:
    client.chat.completions.create(model="gateway", messages=messages)
except GuardrailRefusal as refusal:
    print(refusal.type, refusal.binding, refusal.category, refusal.phase, refusal)
```

Every failure that is not a guardrail refusal keeps its own provider class and propagates, so a
broken route cannot be mistaken for refused content.

| Attribute | What it says |
|---|---|
| `type` | `"guardrail_blocked"` or `"guardrail_unavailable"`; `None` mid-stream |
| `binding` | the guardrail binding that acted |
| `category` | `pii`, `secrets`, `injection`, … when the refusal reached one |
| `phase` | `input`, `output` or `stream` |
| `rule_ref` | the policy rule that fired — a **block** only |
| `reason` | which detector was suspended — an **unavailable** refusal only |
| `status` | `403` or `503`; `None` mid-stream |
| `disclosure` | the `x-tt-guardrail` value, when the response carried it |
| `mid_stream` | the refusal was read from stream bytes, not from a response |
| `response` | the raw response, for the safe routing headers; `None` mid-stream |

`rule_ref` and `reason` are never both set, and neither is ever filled in for the other: a block
means "do not retry this content", an unavailable guard means "retry". `refusal.blocked` and
`refusal.unavailable` are the two predicates, and `str(refusal)` is a sentence naming what acted.
It carries no credential, no header value and no request content.

**`guardrail_refusal(error)`** narrows any exception to a `GuardrailRefusal` or `None`, including
one raised by a borrowed transport this SDK did not wrap. **`stream_guardrail_refusal(data)`** reads
a **mid-stream** refusal out of relayed SSE bytes: that case is not an error at all, just a terminal
delta carrying `finish_reason: "content_filter"` (exported as `STREAM_FINISH_REASON`) and no
explanation, because a refusal that said why would be an oracle for the detector that fired. The
full disclosure is on the request's row, never on the wire.

TypeScript cannot raise its own class here and narrows instead; see
[parity](/docs/sdks/parity#a-guardrail-refusal-raised-in-python-narrowed-in-typescript).

## Configuration

**`init(base_url=None, disabled=None, log=None, emit_traceparent=False, tier_header=None,
admin_url=None, exporters=(), *, agent=None, user=None, tenant=None, feature=None, tags=None)`**

Installs the process configuration. Resolution is argument, then environment, then default. Invalid instrumentation configuration raises `TokenTriageConfigError`. The
five keyword-only dimension arguments form the lowest precedence layer.

**`check(admin_url=None, *, probe=True)` → `CheckReport`**

The first-run doctor. Lints configuration and probes the admin plane only, not the data plane.
Prints its report and returns it. See [install and configure](/docs/sdks/install#verify-before-you-spend).

## Scopes

**`trace(agent=None, user=None, tenant=None, feature=None, tags=None, trace_id=None)`**

**`step(agent=None, step_class=None, user=None, tenant=None, feature=None, tags=None,
parent=None, step_id=None)`**

Both are context managers **and** decorators. `step_class` must be `user_turn`, `tool_loop` or
`provider_state`. `parent` accepts a step id or a `Step`, and is the only identity override.

**`observe(agent=None, step_class=None, user=None, tenant=None, feature=None, tags=None,
parent=None)`**

Decorator sugar over `step`. No TypeScript equivalent.

**`current_step()` → `Step | None`**

The innermost open **explicit** step, not an adapter's implicit one.

Behaviour: [traces, steps, and context](/docs/sdks/context).

## Wire

**`headers(**overrides)` → `dict[str, str]`**

The resolved `x-tt-*` map. Override keys are `agent`, `tier`, `cache`, `user`, `tenant`,
`feature` and `tags`; `tier` and `cache` are accepted here only. Overrides are ephemeral. Returns
`{}` when disabled.

**`receipt(source, status=None)` → `Receipt`**

Parses a response, an error response, or a plain header mapping. Never raises. Returns an
all-absent receipt rather than `None` on any failure.

**`last_receipt()` → `Receipt | None`**

The most recent receipt on the nearest open scope, step before trace. Outside any scope, `None`
plus `TT-SDK-013`.

Behaviour: [from your call to the wire](/docs/sdks/emission) and
[reading the receipt](/docs/sdks/receipts).

## Adapters

| Call | Wraps |
|---|---|
| `httpx_client(*, origins=None, base_url=None, **kwargs)` | a new `httpx.Client` |
| `async_httpx_client(...)` | a new `httpx.AsyncClient` |
| `instrument_httpx(client, ...)` | an existing httpx client, in place |
| `instrument_requests(session, ...)` | a `requests` session |
| `from tokentriage.adapters.langchain import TTCallbackHandler` | LangChain / LangGraph runs |
| `from tokentriage.adapters.crewai import instrument_crew` | a CrewAI crew |
| `from tokentriage.adapters import otel` → `otel.bridge()` | OpenTelemetry span ids |

Each needs its extra installed. Importing one patches nothing.

Behaviour: [adapters](/docs/sdks/adapters).

## Ledger

```python
from tokentriage import ledger
```

All synchronous. All use the credential-containment protocol and read the token from
`TOKENTRIAGE_ADMIN_TOKEN` only.

**`ledger.fetch(receipt_or_req_id, admin_url=None, wait_s=0.0)`** → `RowView`

**`ledger.fetch_trace(receipt_or_trace_id, admin_url=None, wait_s=0.0)`** → `TraceView`

**`ledger.feedback(receipt_or_req_id, ok, note=None, admin_url=None)`** → `FeedbackAck`

**`ledger.outcomes(receipt_or_req_id, metrics, source, evaluator, eval_ver, labels=None,
admin_url=None, basis=...)`** → `OutcomeAck`

`metrics` is a non-empty sequence of `{"metric": str, "value": float, "pass": bool}` mappings,
where `value` is required and `pass` is optional. `basis` is accepted and never sent.

Errors: `PlaneCollision`, `AdminAuthRequired`, `NotFound`, `LedgerError`. `feedback` is the one
call that is not shielded and raises on failure.

Behaviour: [fetching a costed row](/docs/sdks/ledger) and
[outcomes and feedback](/docs/sdks/outcomes).

## Types

`Receipt`, `Cost`, `Rewrite`, `CacheInfo`, `Refusal`, `Trace`, `Step`, `StepRecord`, `Exporter`,
`TokenTriageConfigError`, plus `CheckReport` and `CheckFinding`.

All receipt types are immutable. `Cost` has no public constructor.

`Receipt` carries `request_id`, `mode`, `decision`, `model_rewrite`, `cache`, `refusal`, `cost`,
`extra` and `status_code`, plus the `cost_display` property and `dashboard_url(admin_url=None)`.

## Exporter

**`register_exporter(exporter)`** installs an object with `on_step_end(record)`. Survives a later
`init()`.

**`DebugExporter()`** prints one line per closed step, locally, with no network:

```text
[tt] step trace=b2728443b6c6bfb8b6029a4bcb24debe step=66fcd77192c62295 parent=None agent=planner class=user_turn status=ok dur_us=5.0 receipts=0
```

An exporter that raises cannot break a scope. The default registry is empty.

## Testing

```python
from tokentriage.testing import RecordingTransport, assert_tt_headers
```

Needs `httpx`. See [testing instrumented code](/docs/sdks/testing).

## Management

```python
from tokentriage.management import Client
```

A separate, generated client for the admin API, on its own contract version. Not re-exported from
the package root. See [the management client](/docs/sdks/management).

<Details summary="What is imported when">

Importing `tokentriage` binds the scopes, the wire calls, the types and the exporter. Everything
else resolves on first use: `ledger`, `check`, `testing` and the adapters.

That is why `import tokentriage` opens no socket. The module that can open one is not imported
until you call something that needs it, which also means an uninstalled extra surfaces its error
at first use with a message naming what to install, rather than at import.

</Details>

<NextSteps items={[
  { title: 'TypeScript SDK', href: '/docs/sdks/typescript', description: 'The same surface, idiomatic to TypeScript.' },
  { title: 'Python ↔ TypeScript parity', href: '/docs/sdks/parity', description: 'Where the two differ, and why each difference was kept.' },
  { title: 'Instrument your first request', href: '/docs/sdks/first-request', description: 'These calls in the order you would actually use them.' },
  { title: 'Attribute cost per agent step', href: '/docs/recipes/attribute-cost-per-agent-step', description: 'A worked multi-step agent loop using this surface.' }
]} />
