Skip to main content
@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 and library mode — events from all three land side-by-side under the same agent. From the root of your app:
npx -y @voightxyz/sdk init
The 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

npm install @anthropic-ai/sdk @voightxyz/anthropic
Requirements:
  • Node.js 18+ (uses global fetch)
  • @anthropic-ai/sdk 0.30.0+

Quick start

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 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:
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 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. The per-user pattern in depth — with examples for Auth0, Clerk, NextAuth, custom JWT, and anonymous flows — lives at per-user spend.

Options

OptionTypeDefaultPurpose
voightApiKeystringenv VOIGHT_KEYYour Voight key from the dashboard
agentstringenv VOIGHT_AGENTHOSTNAME'unknown-agent'Stable identifier surfaced in the dashboard
apiBasestringhttps://api.voight.xyzOverride for self-hosted deployments
privacy'minimal' | 'standard' | 'full''standard'Capture aggressiveness
sessionIdstringauto UUID v4Trace grouping. Stable across calls of one wrapper instance — events sharing a sessionId render as a single trace in the dashboard
enabledbooleantrueKill switch — when false, returns the original client untouched
otelbooleanfalseEmit captured calls as OpenTelemetry spans alongside the direct ingest. See 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

SignalField on the event
Model id (with version suffix, e.g. claude-haiku-4-5-20251001)model
Prompt messagesinput.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 flagmetadata.streaming
Stop reasonmetadata.finishReason
Trace groupingmetadata.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 usedmetadata.privacyLevel
Latency in millisecondsdurationMs
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.
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

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) and you want Voight events to appear there too.
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 ≥ 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.
LevelPromptsResponse textTool argumentstoolExecuted (name)
minimaldroppeddroppeddroppedkept
standardscrubbedscrubbedscrubbedkept
fullverbatimverbatimverbatimkept
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 for the full catalogue.

How it compares

Use caseReach for
Coding agent (Claude Code, Cursor, Codex) capturing your dev sessionsHooks-based SDK
Autonomous TS/JS bot you wrote yourself emitting custom eventsLibrary mode
Production app calling Anthropic in user-facing flowsThis package
Production app calling OpenAI@voightxyz/openai
Per-user / per-tenant cost attribution in any of the abovePer-user spend
Anything else (Python, Go, Rust)HTTP API
The packages coexist — wrap your Anthropic client AND call voight.log() for your own domain events under the same agent. Adding withTrace on top groups them all per-request.

Source

Roadmap

  • Bedrock and Vertex Anthropic clients
  • Batch API (when GA)
See the changelog for shipped releases.