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

# Anthropic SDK

> Wrap your Anthropic client and capture every Messages call without changing app code.

`@voightxyz/anthropic` instruments the official Anthropic Node SDK. Wrap your client once and every `messages.create` call — non-streaming or streaming — lands in Voight with prompts, tokens, cache reads, cache creations, tool use, latency, and errors.

Same backend, same dashboard as [`@voightxyz/openai`](/ai-apps/openai) and [library mode](/sdk/library-mode) — events from all three land side-by-side under the same agent.

## Quick setup (recommended)

From the root of your app:

```bash theme={null}
npx -y @voightxyz/sdk init
```

The [wizard](/ai-apps/wizard) detects `@anthropic-ai/sdk` in your `package.json`, prompts for your Voight key + privacy level, validates the key, and writes a ready-to-import `src/lib/voight.ts` with the wrapped client. 30 seconds, zero copy-paste.

Continue below if you'd rather wire it manually.

## Install

```bash theme={null}
npm install @anthropic-ai/sdk @voightxyz/anthropic
```

Requirements:

* Node.js 18+ (uses global `fetch`)
* `@anthropic-ai/sdk` 0.30.0+

## Quick start

```ts theme={null}
import Anthropic from '@anthropic-ai/sdk'
import { wrapAnthropic } from '@voightxyz/anthropic'

const client = wrapAnthropic(new Anthropic(), {
  voightApiKey: process.env.VOIGHT_KEY,
  agent: 'my-prod-agent',
})

const response = await client.messages.create({
  model: 'claude-haiku-4-5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello' }],
})
```

That's it. Every call is captured automatically. Visit your [dashboard](https://voight.xyz/dashboard/ai-apps) to see them in the **AI Apps** section.

## Tracing & per-user tags

For production apps, wrap each request boundary with `withTrace` to group every LLM call inside one request into one trace, and to attribute cost per end-user with one line of code:

```ts theme={null}
import Anthropic from '@anthropic-ai/sdk'
import { wrapAnthropic, withTrace, log } from '@voightxyz/anthropic'

const anthropic = wrapAnthropic(new Anthropic(), { agent: 'production-chat-api' })

app.post('/api/chat', async (req, res) => {
  await withTrace(
    async () => {
      log('chat request received')

      const reply = await anthropic.messages.create({
        model: 'claude-haiku-4-5',
        max_tokens: 1024,
        messages: [{ role: 'user', content: req.body.prompt }],
      })

      res.json({ reply })
    },
    {
      routeTag: 'POST /api/chat',
      tags: { userId: req.user.id, plan: req.user.plan },
    },
  )
})
```

Every wrapped LLM call inside the `withTrace` block gets stamped with `metadata.tags = { userId, plan, ... }` automatically. The dashboard's [AI Apps section](/ai-apps/overview) then surfaces:

* **Users sub-tab** — per-user spend, traces, tokens (driven by `tags.userId`)
* **Trace cards** — every `withTrace` block as one drillable card with cost, latency, and event timeline
* **User filter pill** — narrow Overview / Models / Tools to one user

Full surface in [Tracing](/ai-apps/tracing). The per-user pattern in depth — with examples for Auth0, Clerk, NextAuth, custom JWT, and anonymous flows — lives at [per-user spend](/concepts/per-user-spend).

## Options

| Option         | Type                                | Default                                             | Purpose                                                                                                                                      |
| -------------- | ----------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `voightApiKey` | string                              | env `VOIGHT_KEY`                                    | Your Voight key from the dashboard                                                                                                           |
| `agent`        | string                              | env `VOIGHT_AGENT` → `HOSTNAME` → `'unknown-agent'` | Stable identifier surfaced in the dashboard                                                                                                  |
| `apiBase`      | string                              | `https://api.voight.xyz`                            | Override for self-hosted deployments                                                                                                         |
| `privacy`      | `'minimal' \| 'standard' \| 'full'` | `'standard'`                                        | Capture aggressiveness                                                                                                                       |
| `sessionId`    | string                              | auto UUID v4                                        | Trace grouping. Stable across calls of one wrapper instance — events sharing a `sessionId` render as a single trace in the dashboard         |
| `enabled`      | boolean                             | `true`                                              | Kill switch — when false, returns the original client untouched                                                                              |
| `otel`         | boolean                             | `false`                                             | Emit captured calls as OpenTelemetry spans alongside the direct ingest. See [OpenTelemetry side-channel](#opentelemetry-side-channel) below. |

A missing or empty API key is non-fatal: the wrapper prints a one-line warning and returns the original client. Production keeps running.

## What's captured

| Signal                                                              | Field on the event               |
| ------------------------------------------------------------------- | -------------------------------- |
| Model id (with version suffix, e.g. `claude-haiku-4-5-20251001`)    | `model`                          |
| Prompt messages                                                     | `input.messages`                 |
| Response text (aggregated from `content[].text` blocks)             | `metadata.responseText`          |
| Token counts (input / output / total)                               | `metadata.tokens`                |
| Cache reads (`cache_read_input_tokens`)                             | `metadata.tokens.cache_read`     |
| Cache creations (`cache_creation_input_tokens`)                     | `metadata.tokens.cache_creation` |
| Tool use (full array)                                               | `metadata.toolCalls`             |
| First tool's name (audit-log compat)                                | `toolExecuted`                   |
| Streaming flag                                                      | `metadata.streaming`             |
| Stop reason                                                         | `metadata.finishReason`          |
| Trace grouping                                                      | `metadata.sessionId`             |
| Trace ID (when inside `withTrace`)                                  | `metadata.traceId`               |
| Route tag (when inside `withTrace`)                                 | `metadata.routeTag`              |
| User / plan / org tags (when inside `withTrace`)                    | `metadata.tags`                  |
| Capture level used                                                  | `metadata.privacyLevel`          |
| Latency in milliseconds                                             | `durationMs`                     |
| Errors (re-thrown to the caller, recorded with `outcome: 'failed'`) | `errorMessage`, `outcome`        |

## Streaming

Streaming works without setup. Anthropic emits a typed event sequence (`message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`); the wrapper's state machine reacts to the events that drive capture and passes the rest through unchanged.

```ts theme={null}
const stream = await client.messages.create({
  model: 'claude-haiku-4-5',
  max_tokens: 256,
  stream: true,
  messages: [{ role: 'user', content: 'count to five' }],
})
for await (const event of stream) {
  if (
    event.type === 'content_block_delta' &&
    event.delta.type === 'text_delta'
  ) {
    process.stdout.write(event.delta.text)
  }
}
```

Initial usage from `message_start` carries `input_tokens` plus the two cache fields. Final `output_tokens` lands on `message_delta` and merges into the emitted event so the full breakdown is captured.

## Tool use

```ts theme={null}
const response = await client.messages.create({
  model: 'claude-haiku-4-5',
  max_tokens: 256,
  tools: [{
    name: 'get_weather',
    description: 'Get weather for a city',
    input_schema: {
      type: 'object',
      properties: { location: { type: 'string' } },
      required: ['location'],
    },
  }],
  messages: [{ role: 'user', content: "what's the weather in Tokyo?" }],
})
```

Tool calls live as `tool_use` blocks inside `response.content[]`. The wrapper flattens them into the same `{ id, name, arguments }` shape `@voightxyz/openai` produces — dashboards render tool calls from either provider identically. Anthropic's tool input is a parsed object on the wire; we JSON-stringify it so the captured `arguments` field is a string (matching the openai package).

On the captured event:

* `toolExecuted: 'get_weather'` — first tool's name
* `metadata.toolCalls: [{ id, name, arguments }]` — full array

Streaming tool use works the same — `input_json_delta.partial_json` fragments across `content_block_delta` events are concatenated per `index`.

## Path-A cache pricing

Anthropic distinguishes two cache operations:

* **Cache creation** (`cache_creation_input_tokens`) — writing context to the cache. Billed at 1.25× input rate.
* **Cache read** (`cache_read_input_tokens`) — reading cached context on subsequent turns. Billed at 0.10× input rate.

Both fields are surfaced on `metadata.tokens` only when strictly positive. The Voight backend pricing engine applies the multipliers automatically — your spend reflects the real cost, not a flat-rate over-estimate.

## OpenTelemetry side-channel

By default the wrapper POSTs each captured call directly to `api.voight.xyz`. Set `otel: true` to **additionally** emit each call as an OpenTelemetry span — useful when the host process already runs an OTel pipeline (Langfuse, Phoenix, Datadog, Sentry, or [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai)) and you want Voight events to appear there too.

```ts theme={null}
const client = wrapAnthropic(new Anthropic(), { agent: 'my-app', otel: true })
```

Each span is named `voight.anthropic.messages` and carries the standard `gen_ai.*` semantic-convention attributes (`gen_ai.system` resolves to `'anthropic'`, plus `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.usage.cache_read_input_tokens`, `gen_ai.response.finish_reasons`) plus the parallel Vercel-style `ai.*` namespace. The direct ingest path is unchanged — `otel: true` is purely additive.

### Dedup marker

Every emitted span carries `voight.source: 'wrapper'`. If you also use [`@voightxyz/vercel-ai`](/ai-apps/vercel-ai) ≥ 0.1.1 in the same process, that exporter skips wrapper-emitted spans automatically — no duplicate events in your dashboard.

### Optional peer dependency

`@opentelemetry/api` is now an optional peer dependency. If you never set `otel: true`, nothing changes. If you set `otel: true` but the package isn't installed, the wrapper logs a single warning and falls back to direct ingest only.

## Privacy

Three levels apply to prompts, response text, and tool-call arguments. The function name in `toolExecuted` is treated as a tag (not user content) and survives all levels.

| Level      | Prompts  | Response text | Tool arguments | `toolExecuted` (name) |
| ---------- | -------- | ------------- | -------------- | --------------------- |
| `minimal`  | dropped  | dropped       | dropped        | kept                  |
| `standard` | scrubbed | scrubbed      | scrubbed       | kept                  |
| `full`     | verbatim | verbatim      | verbatim       | kept                  |

Standard scrubs 12 patterns: PEM private keys, JWTs, Anthropic / OpenAI / Stripe live / GitHub / AWS / Slack / Voight API keys, emails, E.164 phones, and Luhn-validated credit cards.

See [PII patterns](/privacy/pii-patterns) for the full catalogue.

## How it compares

| Use case                                                              | Reach for                                  |
| --------------------------------------------------------------------- | ------------------------------------------ |
| Coding agent (Claude Code, Cursor, Codex) capturing your dev sessions | [Hooks-based SDK](/quickstart)             |
| Autonomous TS/JS bot you wrote yourself emitting custom events        | [Library mode](/sdk/library-mode)          |
| Production app calling Anthropic in user-facing flows                 | This package                               |
| Production app calling OpenAI                                         | [`@voightxyz/openai`](/ai-apps/openai)     |
| Per-user / per-tenant cost attribution in any of the above            | [Per-user spend](/concepts/per-user-spend) |
| Anything else (Python, Go, Rust)                                      | [HTTP API](/sdk/http-api)                  |

The packages coexist — wrap your Anthropic client AND call `voight.log()` for your own domain events under the same agent. Adding [`withTrace`](/ai-apps/tracing) on top groups them all per-request.

## Source

* [github.com/Voightxyz/voight-anthropic](https://github.com/Voightxyz/voight-anthropic)
* [npmjs.com/package/@voightxyz/anthropic](https://www.npmjs.com/package/@voightxyz/anthropic)

## Roadmap

* Bedrock and Vertex Anthropic clients
* Batch API (when GA)

See the [changelog](/changelog) for shipped releases.
