# TypeScript SDK

The complete TypeScript surface — every export, its signature, the two builds, and where each behaviour is documented.

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

---
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 TypeScript, in one place. Behaviour is documented
on the topic pages; this is the list of what exists and what each call takes.

```ts
import * as tt from "tokentriage";
```

Requires Node 18 or newer. Local unreleased version `1.0.0-rc.2`, contract version 1, zero runtime dependencies,
dual ESM and CommonJS.

## Two builds, and which one you get

The package ships an edge-safe default build and a Node build, selected by export condition.

| Condition | Build | Async context |
|---|---|---|
| `node` | `dist/node/` | `AsyncLocalStorage`; scopes propagate |
| anything else | `dist/` | a no-op carrier; scopes run but do not propagate |

On edge runtimes, use the callback's explicit handle:

```ts
await trace({}, async scope => {
  const requestHeaders = scope.headers();
});
```

The handle supports nested `step()`, `headers()` and `recordReceipt()`.
It preserves identity when passed through your application without ambient storage.
Existing callbacks accepting no argument remain valid.

## Configured provider client

The unreleased `TokenTriage` from `tokentriage/client` accepts `baseUrl`, `apiKey`
and `mode`. `await connection.openai()` creates an official OpenAI client;
`connection.openaiOptions()` returns options for your own constructor and fetch.
`metadata(response)` and `errorDetails(error)` read response-bound information.

`openai()`, `openaiOptions()` and `native()` also take the saved-mode binding:
`gatewayId` (required), `environmentId`, `baseUrl`, `credentialSource` and
`providerApiKey`. Supplying `gatewayId` selects saved mode, where `apiKey` is sent
as the `Authorization` bearer and the gateway and environment selector headers are
stamped for you. Send the exported `gatewayModel` as the model. `providerApiKey`
needs `credentialSource: "request_supplied"`; without it, and for any `apiKey`
passed to a saved-mode call, the SDK throws `ConfigurationError` naming
`provider_key_not_allowed` before building a request.

See [the first request](/docs/sdks/first-request) 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. `tokentriage/client` rewrites that body
into the shape the official client can read, so the thrown `PermissionDeniedError` or
`InternalServerError` already carries the written reason in its message.

**`guardrailRefusal(error: unknown): GuardrailRefusal | null`**

Reads the typed fields back off a thrown provider error. `null` means the failure was not a
guardrail refusal (a wrong key, a spent budget, a broken route) and is yours to handle as before.

```ts
try {
  await client.chat.completions.create({ model: gatewayModel, messages });
} catch (thrown) {
  const refusal = guardrailRefusal(thrown);
  if (refusal === null) throw thrown;
  console.log(refusal.type, refusal.binding, refusal.category, refusal.phase, refusal.message);
}
```

<Callout type="note">
**TypeScript narrows; it does not throw its own class.** openai-node converts anything thrown from
a custom `fetch` into `APIConnectionError`, so a distinct exception class here would reach you as
"connection error", worse than the status error it replaced. The Python binding raises
`GuardrailRefusal` instead, because openai-python re-raises non-httpx exceptions unwrapped. See
[parity](/docs/sdks/parity#a-guardrail-refusal-raised-in-python-narrowed-in-typescript).
</Callout>

`GuardrailRefusal` fields, every one nullable because the wire makes it optional:

| Field | What it says |
|---|---|
| `type` | `"guardrail_blocked"` or `"guardrail_unavailable"`; `null` mid-stream |
| `binding` | the guardrail binding that acted |
| `category` | `pii`, `secrets`, `injection`, … when the refusal reached one |
| `phase` | `input`, `output` or `stream` |
| `ruleRef` | the policy rule that fired — a **block** only |
| `reason` | which detector was suspended — an **unavailable** refusal only |
| `status` | `403` or `503`; `null` mid-stream |
| `disclosure` | the `x-tt-guardrail` value, when the response carried it |
| `midStream` | the refusal was read from stream bytes, not from a response |
| `message` | the written sentence; it names what acted and never the matched content |
| `requestId` | `x-tt-request-id`, to quote |

`ruleRef` 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". `isBlocked(refusal)` and
`isUnavailable(refusal)` are the two predicates.

**`streamGuardrailRefusal(data: string | Uint8Array): GuardrailRefusal | null`**

A refusal that arrives **mid-stream** is not an error at all: the stream ends with 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. Pass
relayed SSE bytes, or any prefix of them, to read it; a reader that already parses events can
compare the finish reason itself. The full disclosure is on the request's row, never on the wire.

## Configuration

**`init(options?: InitOptions): void`**

Installs the process configuration. Resolution is argument, then environment, then default. The
one call that throws, with `TokenTriageConfigError` naming the field.

`InitOptions` carries `baseUrl`, `adminUrl`, `log`, `disabled`, `emitTraceparent`, `tierHeader`,
`exporters`, and the default dimensions `agent`, `user`, `tenant`, `feature` and `tags`.

**`check(options?: CheckOptions): Promise<CheckReport>`**

The first-run doctor. Admin plane only, not the data plane.

## Scopes

**`trace<T>(options: TraceOptions, fn: (scope: Trace) => T): T`** and **`trace<T>(fn: (scope: Trace) => T): T`**

**`step<T>(options: StepOptions, fn: (scope: Step) => T): T`** and **`step<T>(fn: (scope: Step) => T): T`**

Callback-scoped; the callback's return value, including a promise, passes through. There is no
decorator form and no `observe` — those are Python-only conveniences.

**`currentStep(): Step | null`**

The innermost open explicit step. `currentTrace` is not exported.

`Trace` exposes `traceId`, `headers()` and `lastReceipt`. `Step` adds `stepId`, `parentId` and
`receipts`.

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

## Wire

**`headers(overrides?: CallOverrides): Record<string, string>`**

**`receipt(source: ReceiptSource, status?: number): Receipt`**

**`lastReceipt(): Receipt | null`**

`receipt` accepts a `Response`, a `Headers`, or a plain record. It never throws, and returns an
all-absent receipt rather than `null`.

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

## Transport

**`fetch`** is a drop-in for `globalThis.fetch` that injects headers, opens an implicit
per-request step, and pairs the receipt.

**`wrapFetch(base?: FetchFn, options?: WrapFetchOptions): FetchFn`** builds an origin-scoped
wrapper around a `fetch` you supply. `options.origins` takes an explicit allowlist instead of
deriving one from `baseUrl`.

**`bridge()`** from `tokentriage/adapters/otel` adopts the active OpenTelemetry span's ids. The
OpenTelemetry API is an optional peer.

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

## Ledger

```ts
import { ledger } from "tokentriage";
```

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

**`ledger.fetch(receiptOrReqId, options?): Promise<RowView>`** — options `{ adminUrl?, waitS? }`

**`ledger.fetchTrace(receiptOrTraceId, options?): Promise<TraceView>`**

**`ledger.feedback(receiptOrReqId, { ok, note?, adminUrl? }): Promise<FeedbackAck>`**

**`ledger.outcomes(receiptOrReqId, options: OutcomeOptions): Promise<OutcomeAck>`** — options
carry `metrics`, `source`, `evaluator`, `evalVer`, and optionally `labels` and `adminUrl`.

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

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

## Types and helpers

The whole type surface is exported from the package root: `Mode`, `StepClass`, `CacheStatus`,
`CacheControl`, `RefusalType`, `Cost`, `Rewrite`, `CacheInfo`, `RefusalKind`, `Refusal`,
`Receipt`, `ReceiptSource`, `InitOptions`, `Dims`, `TraceOptions`, `StepOptions`, `CallOverrides`,
`Trace`, `Step`, `WrapFetchOptions`, `StepRecord`, `StepStatus`, `Exporter`, `CheckFinding`,
`CheckReport` and `CheckOptions`.

**`renderCost(cost): string`** produces the pinned money string for any cost state.

**`version: string`** is the package version.

`Cost` is a discriminated union: `usd` is a decimal **string**, reachable only after narrowing on
`state === "priced"`. Several closed vocabularies are literal unions here that are plain strings
in Python; see [parity](/docs/sdks/parity#closed-vocabularies-are-stricter-in-typescript).

## Exporter

**`registerExporter(exporter)`** and **`DebugExporter`**, matching Python:

```text
[tt] step trace=af1ee2db0e477e5b3511fefb3b291231 step=20bdde57899eb179 parent=null agent=null class=null status=error dur_us=1119.5 receipts=0
```

## Testing and management

```ts
const { recordingFetch, assertTtHeaders } = tt.testing;
import { Client } from "tokentriage/management";
```

`testing` is built in, needing no extra. `management` is a separate generated client on its own
contract version. See [testing instrumented code](/docs/sdks/testing) and
[the management client](/docs/sdks/management).

<Details summary="Subpaths and packaging">

Three import paths: `tokentriage`, `tokentriage/adapters/otel` and `tokentriage/management`.

The package is dual ESM and CommonJS with types nested per condition, so a CommonJS consumer gets
CommonJS declarations rather than ESM declarations served under a CommonJS build. The type shape is
identical across conditions; only the runtime differs.

`sideEffects` lists the Node entry alone, because installing the async-context carrier is an
import-time effect a bundler must not remove. The rest of the package is side-effect free and
tree-shakeable.

</Details>

<NextSteps items={[
  { title: 'Python SDK', href: '/docs/sdks/python', description: 'The same surface, idiomatic to Python.' },
  { 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: 'Traces, steps, and context', href: '/docs/sdks/context', description: 'What the two builds mean for identity propagation.' }
]} />
