Skip to content

Commit 088986a

Browse files
nirgaclaude
andauthored
fix(anthropic): add support for Claude thinking API (#671)
Co-authored-by: Claude <[email protected]>
1 parent db60e4b commit 088986a

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import type {
4545
Message,
4646
MessageStreamEvent,
4747
} from "@anthropic-ai/sdk/resources/messages";
48+
import type { MessageCreateParamsNonStreaming as BetaMessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/beta/messages";
4849
import type { Stream } from "@anthropic-ai/sdk/streaming";
4950
import type { APIPromise, BaseAnthropic } from "@anthropic-ai/sdk";
5051

@@ -72,6 +73,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
7273
"create",
7374
this.patchAnthropic("chat", module),
7475
);
76+
this._wrap(
77+
module.Anthropic.Beta.Messages.prototype,
78+
"create",
79+
this.patchAnthropic("chat", module),
80+
);
7581
}
7682

7783
protected init(): InstrumentationModuleDefinition {
@@ -97,6 +103,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
97103
"create",
98104
this.patchAnthropic("chat", moduleExports),
99105
);
106+
this._wrap(
107+
moduleExports.Anthropic.Beta.Messages.prototype,
108+
"create",
109+
this.patchAnthropic("chat", moduleExports),
110+
);
100111
return moduleExports;
101112
}
102113

@@ -108,6 +119,7 @@ export class AnthropicInstrumentation extends InstrumentationBase {
108119

109120
this._unwrap(moduleExports.Anthropic.Completions.prototype, "create");
110121
this._unwrap(moduleExports.Anthropic.Messages.prototype, "create");
122+
this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
111123
}
112124

113125
private patchAnthropic(
@@ -202,6 +214,14 @@ export class AnthropicInstrumentation extends InstrumentationBase {
202214
attributes[SpanAttributes.LLM_REQUEST_TOP_P] = params.top_p;
203215
attributes[SpanAttributes.LLM_TOP_K] = params.top_k;
204216

217+
// Handle thinking parameters (for beta messages)
218+
const betaParams = params as BetaMessageCreateParamsNonStreaming;
219+
if (betaParams.thinking && betaParams.thinking.type === "enabled") {
220+
attributes["llm.request.thinking.type"] = betaParams.thinking.type;
221+
attributes["llm.request.thinking.budget_tokens"] =
222+
betaParams.thinking.budget_tokens;
223+
}
224+
205225
if (type === "completion") {
206226
attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] =
207227
params.max_tokens_to_sample;

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,101 @@ 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+
{
338+
role: "user",
339+
content: "What is 2+2? Think through this step by step.",
340+
},
341+
],
342+
model: "claude-opus-4-1-20250805",
343+
thinking: {
344+
type: "enabled",
345+
budget_tokens: 1024,
346+
},
347+
});
348+
349+
const spans = memoryExporter.getFinishedSpans();
350+
const chatSpan = spans.find((span) => span.name === "anthropic.chat");
351+
352+
assert.ok(message);
353+
assert.ok(chatSpan);
354+
assert.strictEqual(
355+
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`],
356+
"claude-opus-4-1-20250805",
357+
);
358+
assert.strictEqual(
359+
chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`],
360+
"claude-opus-4-1-20250805",
361+
);
362+
assert.strictEqual(
363+
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`],
364+
2048,
365+
);
366+
367+
// Check if thinking parameters are captured (these will fail initially)
368+
assert.strictEqual(
369+
chatSpan.attributes["llm.request.thinking.type"],
370+
"enabled",
371+
);
372+
assert.strictEqual(
373+
chatSpan.attributes["llm.request.thinking.budget_tokens"],
374+
1024,
375+
);
376+
377+
// Check prompts
378+
assert.strictEqual(
379+
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`],
380+
"user",
381+
);
382+
assert.strictEqual(
383+
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`],
384+
"What is 2+2? Think through this step by step.",
385+
);
386+
387+
// Check that we capture both thinking and regular content blocks
388+
const content = JSON.parse(
389+
chatSpan.attributes[
390+
`${SpanAttributes.LLM_COMPLETIONS}.0.content`
391+
] as string,
392+
);
393+
assert.ok(Array.isArray(content));
394+
395+
interface ContentBlock {
396+
type: string;
397+
thinking?: string;
398+
text?: string;
399+
}
400+
401+
const thinkingBlock = content.find(
402+
(block: ContentBlock) => block.type === "thinking",
403+
);
404+
const textBlock = content.find(
405+
(block: ContentBlock) => block.type === "text",
406+
);
407+
408+
assert.ok(thinkingBlock, "Should contain a thinking block");
409+
assert.ok(
410+
thinkingBlock.thinking,
411+
"Thinking block should have thinking content",
412+
);
413+
assert.ok(textBlock, "Should contain a text block");
414+
assert.ok(textBlock.text, "Text block should have text content");
415+
416+
// Verify token usage includes thinking tokens
417+
const completionTokens =
418+
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`];
419+
const promptTokens =
420+
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`];
421+
const totalTokens =
422+
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`];
423+
424+
assert.ok(completionTokens && +completionTokens > 0);
425+
assert.ok(promptTokens && +promptTokens > 0);
426+
assert.equal(+promptTokens + +completionTokens, totalTokens);
427+
}).timeout(30000);
331428
});

0 commit comments

Comments
 (0)