Skip to content

Commit d97bd7e

Browse files
nirgaclaude
andcommitted
feat(anthropic): add support for Claude thinking API
Adds instrumentation support for Claude's extended thinking feature via the beta messages API. Changes: - Add instrumentation for anthropic.beta.messages.create() calls - Capture thinking parameters (type and budget_tokens) in span attributes - Handle response content with both thinking and text blocks - Add comprehensive test for thinking API functionality This enables proper observability for Claude thinking API calls, capturing both the reasoning process and final response in telemetry data. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent db60e4b commit d97bd7e

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
{
2+
"log": {
3+
"_recordingName": "Test Anthropic instrumentation/should set attributes in span for beta messages with thinking",
4+
"creator": {
5+
"comment": "persister:fs",
6+
"name": "Polly.JS",
7+
"version": "6.0.6"
8+
},
9+
"entries": [
10+
{
11+
"_id": "36fcbb1741e0f2a632c89d8c928a5d53",
12+
"_order": 0,
13+
"cache": {},
14+
"request": {
15+
"bodySize": 192,
16+
"cookies": [],
17+
"headers": [
18+
{
19+
"name": "accept",
20+
"value": "application/json"
21+
},
22+
{
23+
"name": "anthropic-beta",
24+
"value": "interleaved-thinking-2025-05-14"
25+
},
26+
{
27+
"name": "anthropic-version",
28+
"value": "2023-06-01"
29+
},
30+
{
31+
"name": "content-type",
32+
"value": "application/json"
33+
},
34+
{
35+
"name": "user-agent",
36+
"value": "Anthropic/JS 0.56.0"
37+
},
38+
{
39+
"name": "x-stainless-arch",
40+
"value": "arm64"
41+
},
42+
{
43+
"name": "x-stainless-lang",
44+
"value": "js"
45+
},
46+
{
47+
"name": "x-stainless-os",
48+
"value": "MacOS"
49+
},
50+
{
51+
"name": "x-stainless-package-version",
52+
"value": "0.56.0"
53+
},
54+
{
55+
"name": "x-stainless-retry-count",
56+
"value": "0"
57+
},
58+
{
59+
"name": "x-stainless-runtime",
60+
"value": "node"
61+
},
62+
{
63+
"name": "x-stainless-runtime-version",
64+
"value": "v20.11.1"
65+
},
66+
{
67+
"name": "x-stainless-timeout",
68+
"value": "600"
69+
}
70+
],
71+
"headersSize": 584,
72+
"httpVersion": "HTTP/1.1",
73+
"method": "POST",
74+
"postData": {
75+
"mimeType": "application/json",
76+
"params": [],
77+
"text": "{\"max_tokens\":2048,\"messages\":[{\"role\":\"user\",\"content\":\"What is 2+2? Think through this step by step.\"}],\"model\":\"claude-opus-4-1-20250805\",\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":1024}}"
78+
},
79+
"queryString": [
80+
{
81+
"name": "beta",
82+
"value": "true"
83+
}
84+
],
85+
"url": "https://api.anthropic.com/v1/messages?beta=true"
86+
},
87+
"response": {
88+
"bodySize": 1570,
89+
"content": {
90+
"mimeType": "application/json",
91+
"size": 1570,
92+
"text": "{\"id\":\"msg_018V3xGyrq6nc25GVuWiaKHx\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-opus-4-1-20250805\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"This is a very simple arithmetic question. The user is asking for 2+2, which equals 4. They've asked me to think through it step by step, so I should show the basic addition process even though it's elementary.\",\"signature\":\"EvsCCkYIBhgCKkDcMyQ9Uh8CsGT5WmyeuwbI5yYSB1cbUyx5DC/zqmUMb0n5Zyi+Oz/fXpxGLJUmfnKp3zXuuOhybxupMRhBmK+3EgxVj1F8BGfmpqpOxjcaDIbIp9dBQHkej5KsTyIwJDTMjuH/q/vu4Pk/Zf4w9htqsZOPLfYdg/EbXIdeBNV4sJ6Jtiu+KzBg4O5fTgjPKuIBuD8ob8cR9xna6cV8JHxfUT9IeX3huQ2oF/vJC/99vqn4F//OEjiN8kCKPlJo28+S72odghUyF8TUITL/UIBWZ3kcQtwCdmytlB1+2Bld5osVVmOi4KApBkl9cRTOemDzkJHBFmhJ1AuUyZ2Fl2hVGmE2ACE8CPYU+iCZpZX2l4tWCT2M1wCaNTwNSqHcQtC/C0H9geP6Vyc2K2P6TcUIuUv8CFVIdqwcYDnbREhlY2Jv7nmaVDSraCvCXWj3Y/sQulDsBOqp6drQAITcWPJI8wbDmw8fIEhcCyujlcpanKzBhhgB\"},{\"type\":\"text\",\"text\":\"I'll work through this simple addition step by step.\\n\\n**Step 1:** Identify what we're adding\\n- We have two numbers: 2 and 2\\n- We need to add them together\\n\\n**Step 2:** Perform the addition\\n- Start with the first number: 2\\n- Add the second number: + 2\\n- When we combine 2 items with 2 more items, we get 4 items total\\n\\n**Step 3:** State the result\\n- 2 + 2 = 4\\n\\nThe answer is **4**.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":49,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":186,\"service_tier\":\"standard\"}}"
93+
},
94+
"cookies": [],
95+
"headers": [
96+
{
97+
"name": "anthropic-organization-id",
98+
"value": "617d109c-a187-4902-889d-689223d134aa"
99+
},
100+
{
101+
"name": "anthropic-ratelimit-input-tokens-limit",
102+
"value": "2000000"
103+
},
104+
{
105+
"name": "anthropic-ratelimit-input-tokens-remaining",
106+
"value": "2000000"
107+
},
108+
{
109+
"name": "anthropic-ratelimit-input-tokens-reset",
110+
"value": "2025-08-21T11:41:58Z"
111+
},
112+
{
113+
"name": "anthropic-ratelimit-output-tokens-limit",
114+
"value": "400000"
115+
},
116+
{
117+
"name": "anthropic-ratelimit-output-tokens-remaining",
118+
"value": "400000"
119+
},
120+
{
121+
"name": "anthropic-ratelimit-output-tokens-reset",
122+
"value": "2025-08-21T11:42:02Z"
123+
},
124+
{
125+
"name": "anthropic-ratelimit-requests-limit",
126+
"value": "4000"
127+
},
128+
{
129+
"name": "anthropic-ratelimit-requests-remaining",
130+
"value": "3999"
131+
},
132+
{
133+
"name": "anthropic-ratelimit-requests-reset",
134+
"value": "2025-08-21T11:41:57Z"
135+
},
136+
{
137+
"name": "anthropic-ratelimit-tokens-limit",
138+
"value": "2400000"
139+
},
140+
{
141+
"name": "anthropic-ratelimit-tokens-remaining",
142+
"value": "2400000"
143+
},
144+
{
145+
"name": "anthropic-ratelimit-tokens-reset",
146+
"value": "2025-08-21T11:41:58Z"
147+
},
148+
{
149+
"name": "cf-cache-status",
150+
"value": "DYNAMIC"
151+
},
152+
{
153+
"name": "cf-ray",
154+
"value": "9729dd411cdd6756-ATL"
155+
},
156+
{
157+
"name": "connection",
158+
"value": "keep-alive"
159+
},
160+
{
161+
"name": "content-encoding",
162+
"value": "gzip"
163+
},
164+
{
165+
"name": "content-type",
166+
"value": "application/json"
167+
},
168+
{
169+
"name": "date",
170+
"value": "Thu, 21 Aug 2025 11:42:02 GMT"
171+
},
172+
{
173+
"name": "request-id",
174+
"value": "req_011CSLo11ceKMKF1kTBWoKxZ"
175+
},
176+
{
177+
"name": "server",
178+
"value": "cloudflare"
179+
},
180+
{
181+
"name": "strict-transport-security",
182+
"value": "max-age=31536000; includeSubDomains; preload"
183+
},
184+
{
185+
"name": "transfer-encoding",
186+
"value": "chunked"
187+
},
188+
{
189+
"name": "via",
190+
"value": "1.1 google"
191+
},
192+
{
193+
"name": "x-envoy-upstream-service-time",
194+
"value": "5555"
195+
},
196+
{
197+
"name": "x-robots-tag",
198+
"value": "none"
199+
}
200+
],
201+
"headersSize": 1098,
202+
"httpVersion": "HTTP/1.1",
203+
"redirectURL": "",
204+
"status": 200,
205+
"statusText": "OK"
206+
},
207+
"startedDateTime": "2025-08-21T11:41:56.089Z",
208+
"time": 6594,
209+
"timings": {
210+
"blocked": -1,
211+
"connect": -1,
212+
"dns": -1,
213+
"receive": 0,
214+
"send": 0,
215+
"ssl": -1,
216+
"wait": 6594
217+
}
218+
}
219+
],
220+
"pages": [],
221+
"version": "1.2"
222+
}
223+
}

packages/instrumentation-anthropic/src/instrumentation.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
7272
"create",
7373
this.patchAnthropic("chat", module),
7474
);
75+
this._wrap(
76+
module.Anthropic.Beta.Messages.prototype,
77+
"create",
78+
this.patchAnthropic("chat", module),
79+
);
7580
}
7681

7782
protected init(): InstrumentationModuleDefinition {
@@ -97,6 +102,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
97102
"create",
98103
this.patchAnthropic("chat", moduleExports),
99104
);
105+
this._wrap(
106+
moduleExports.Anthropic.Beta.Messages.prototype,
107+
"create",
108+
this.patchAnthropic("chat", moduleExports),
109+
);
100110
return moduleExports;
101111
}
102112

@@ -108,6 +118,7 @@ export class AnthropicInstrumentation extends InstrumentationBase {
108118

109119
this._unwrap(moduleExports.Anthropic.Completions.prototype, "create");
110120
this._unwrap(moduleExports.Anthropic.Messages.prototype, "create");
121+
this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
111122
}
112123

113124
private patchAnthropic(
@@ -202,6 +213,17 @@ export class AnthropicInstrumentation extends InstrumentationBase {
202213
attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.top_p;
203214
attributes[SpanAttributes.LLM_TOP_K] = params.top_k;
204215

216+
// Handle thinking parameters
217+
if ((params as any).thinking) {
218+
const thinking = (params as any).thinking;
219+
if (thinking.type) {
220+
attributes["llm.request.thinking.type"] = thinking.type;
221+
}
222+
if (thinking.budget_tokens) {
223+
attributes["llm.request.thinking.budget_tokens"] = thinking.budget_tokens;
224+
}
225+
}
226+
205227
if (type === "completion") {
206228
attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] =
207229
params.max_tokens_to_sample;

packages/instrumentation-anthropic/test/instrumentation.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,82 @@ describe("Test Anthropic instrumentation", async function () {
328328
"user",
329329
);
330330
}).timeout(30000);
331+
332+
it("should set attributes in span for beta messages with thinking", async () => {
333+
const message = await anthropic.beta.messages.create({
334+
max_tokens: 2048,
335+
betas: ['interleaved-thinking-2025-05-14'],
336+
messages: [
337+
{ role: "user", content: "What is 2+2? Think through this step by step." },
338+
],
339+
model: "claude-opus-4-1-20250805",
340+
thinking: {
341+
type: "enabled",
342+
budget_tokens: 1024,
343+
},
344+
});
345+
346+
const spans = memoryExporter.getFinishedSpans();
347+
const chatSpan = spans.find((span) => span.name === "anthropic.chat");
348+
349+
assert.ok(message);
350+
assert.ok(chatSpan);
351+
assert.strictEqual(
352+
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`],
353+
"claude-opus-4-1-20250805",
354+
);
355+
assert.strictEqual(
356+
chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`],
357+
"claude-opus-4-1-20250805",
358+
);
359+
assert.strictEqual(
360+
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`],
361+
2048,
362+
);
363+
364+
// Check if thinking parameters are captured (these will fail initially)
365+
assert.strictEqual(
366+
chatSpan.attributes["llm.request.thinking.type"],
367+
"enabled",
368+
);
369+
assert.strictEqual(
370+
chatSpan.attributes["llm.request.thinking.budget_tokens"],
371+
1024,
372+
);
373+
374+
// Check prompts
375+
assert.strictEqual(
376+
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`],
377+
"user",
378+
);
379+
assert.strictEqual(
380+
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`],
381+
"What is 2+2? Think through this step by step.",
382+
);
383+
384+
// Check that we capture both thinking and regular content blocks
385+
const content = JSON.parse(chatSpan.attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] as string);
386+
assert.ok(Array.isArray(content));
387+
388+
const thinkingBlock = content.find((block: any) => block.type === "thinking");
389+
const textBlock = content.find((block: any) => block.type === "text");
390+
391+
assert.ok(thinkingBlock, "Should contain a thinking block");
392+
assert.ok(thinkingBlock.thinking, "Thinking block should have thinking content");
393+
assert.ok(textBlock, "Should contain a text block");
394+
assert.ok(textBlock.text, "Text block should have text content");
395+
396+
// Verify token usage includes thinking tokens
397+
assert.ok(
398+
+chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]! > 0,
399+
);
400+
assert.ok(
401+
+chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]! > 0,
402+
);
403+
assert.equal(
404+
+chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]! +
405+
+chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]!,
406+
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`],
407+
);
408+
}).timeout(30000);
331409
});

0 commit comments

Comments
 (0)