BrowserStack AI Evals
Integrations

Use with Anthropic

Record Anthropic prompt-caching tokens (cache_read / cache_creation) and cost in your traces.

Use with Anthropic

Anthropic prompt caching lets you reuse a large, stable prompt prefix across requests at a reduced rate. When caching is active, the Anthropic API returns two extra usage fields:

  • cache_creation_input_tokens — tokens written to the cache (billed ~1.25× base input)
  • cache_read_input_tokens — tokens served from the cache (billed ~0.1× base input)

The SDK records both into your trace so the platform can compute cost correctly, applying the cache multipliers automatically. There are two ways to do it: auto-instrumentation (zero tracing code) or the manual tracing API (you map usage yourself).

The examples on this page are Node.js / TypeScript. Token capture works for both auto-instrumented and manually-traced calls; the usageDetails keys below are the same regardless of language.

Enable prompt caching

Caching is a property of the Anthropic request, not the SDK. Mark a large, stable block with cache_control. The block must be big enough to cache (≥1024 tokens for Sonnet).

const message = await client.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 256,
  system: [
    { type: 'text', text: 'You are a concise assistant.' },
    // This large, stable block is cached and reused across calls:
    { type: 'text', text: LARGE_CONTEXT, cache_control: { type: 'ephemeral' } },
  ],
  messages: [{ role: 'user', content: 'In one sentence, what are you?' }],
});

The first call with a given prefix writes the cache (cache_creation_input_tokens > 0); a later call with the identical prefix reads it (cache_read_input_tokens > 0).

With auto-instrumentation, the SDK reads the cache fields off the Anthropic response automatically — you write no tracing code. The captured span (anthropic.chat) carries gen_ai.usage.cache_read_input_tokens and gen_ai.usage.cache_creation_input_tokens, and cost is computed server-side.

There are two ways to enable it.

Option A — --import flag

Preload the instrumentation so it patches @anthropic-ai/sdk before your code imports it.

// cache-auto.mjs
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); // reads ANTHROPIC_API_KEY

const LARGE_CONTEXT = 'You are a helpful assistant.\n' + 'Reference material. '.repeat(1200);

const res = await client.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 256,
  system: [{ type: 'text', text: LARGE_CONTEXT, cache_control: { type: 'ephemeral' } }],
  messages: [{ role: 'user', content: 'In one sentence, what are you?' }],
});

console.log('usage:', res.usage);

Run it with the instrument preload:

node --import @browserstack/ai-sdk/instrument cache-auto.mjs

Option B — Observe.init() (no flag)

Configure instrumentation in code. The catch is the ordering rule below: Observe.init() must run before @anthropic-ai/sdk loads, so the Anthropic import must be dynamic (after init).

// cache-auto-observe.mjs
import { Observe } from '@browserstack/ai-sdk';

// 1. Initialize instrumentation FIRST.
await Observe.init({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});

// 2. Let async patching finish before loading any provider SDK.
await new Promise((r) => setTimeout(r, 3000));

// 3. NOW import Anthropic — DYNAMIC, after init, so it is patched.
const Anthropic = (await import('@anthropic-ai/sdk')).default;
const client = new Anthropic();

const LARGE_CONTEXT = 'You are a helpful assistant.\n' + 'Reference material. '.repeat(1200);
const res = await client.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 256,
  system: [{ type: 'text', text: LARGE_CONTEXT, cache_control: { type: 'ephemeral' } }],
  messages: [{ role: 'user', content: 'In one sentence, what are you?' }],
});

console.log('usage:', res.usage);
node cache-auto-observe.mjs

The ordering rule: instrumentation must patch @anthropic-ai/sdk before that module is loaded. If Anthropic loads first, it loads unpatched, no spans are produced, and nothing reaches the dashboard — even though the LLM call and caching themselves work. A static top-level import Anthropic from '@anthropic-ai/sdk' placed above a later Observe.init() traces nothing; use the --import flag or the dynamic-import pattern in Option B.

Manual tracing

Use the manual API when you want full control over span names, or the call isn't auto-instrumented. Map the Anthropic usage object onto the SDK's usageDetails keys — these are what the platform reads for cost.

import { AISDK } from '@browserstack/ai-sdk';
import Anthropic from '@anthropic-ai/sdk';

const client = new AISDK({
  publicKey: process.env.AISDK_PUBLIC_KEY,
  secretKey: process.env.AISDK_SECRET_KEY,
});
const anthropic = new Anthropic();

const trace = client.trace({ name: 'cache-demo', tags: ['anthropic'] });

const generation = trace.generation({
  name: 'claude-call',
  model: 'claude-sonnet-4-5',
  input: { prompt: 'Summarize the document.' },
});

const message = await anthropic.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 128,
  system: [
    { type: 'text', text: 'You are a concise assistant.' },
    { type: 'text', text: LARGE_CONTEXT, cache_control: { type: 'ephemeral' } },
  ],
  messages: [{ role: 'user', content: 'Summarize the document.' }],
});

generation.end({
  output: message.content.map((c) => (c.type === 'text' ? c.text : '')).join(''),
  // Map real usage -> usageDetails. cache_* keys let the platform price cache hits correctly.
  usageDetails: {
    input: message.usage.input_tokens,
    output: message.usage.output_tokens,
    cache_read_input_tokens: message.usage.cache_read_input_tokens ?? 0,
    cache_creation_input_tokens: message.usage.cache_creation_input_tokens ?? 0,
  },
});

await client.flushAsync();
await client.shutdownAsync();

The basic usage: { input, output, total } field on generation.end() cannot represent cache tokens. Use usageDetails (shown above) whenever you need the cache_read_input_tokens / cache_creation_input_tokens breakdown.

Overriding cost

By default the platform computes cost from model + the token breakdown, applying the cache multipliers. To override it with your own figures, pass costDetails (manual API only):

generation.end({
  output: '…',
  usageDetails: {
    input: 12,
    output: 87,
    cache_read_input_tokens: 4096,
    cache_creation_input_tokens: 1024,
  },
  costDetails: { input: 0.0031, output: 0.0044, total: 0.0075 },
});

usageDetails reference

KeyMeaningRelative price
inputuncached input tokens
outputoutput tokensoutput rate
cache_read_input_tokenstokens served from cache~0.1× input
cache_creation_input_tokenstokens written to cache~1.25× input

Node.js environment notes

These apply to running the standalone examples above:

  • --env-file requires Node 20.6+. On Node 18, load env vars with node -r dotenv/config <file> or import 'dotenv/config' at the top of the script instead.
  • --import works on Node 18.18+. Use it (not -r) for the Option A preload.
  • Do not substitute -r @browserstack/ai-sdk/instrument for --import in ESM scripts. The CommonJS -r preload cannot patch an ESM import Anthropic in time, so the SDK loads unpatched and exports zero spans even though caching works. Use --import.
  • Harmless Module @anthropic-ai/sdk has been loaded before instrumentation-anthropic … warnings may still print under --import. As long as the span exports, they can be ignored.

See also