Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
{
"log": {
"_recordingName": "Test Anthropic instrumentation/should set attributes in span for beta messages with thinking",
"creator": {
"comment": "persister:fs",
"name": "Polly.JS",
"version": "6.0.6"
},
"entries": [
{
"_id": "36fcbb1741e0f2a632c89d8c928a5d53",
"_order": 0,
"cache": {},
"request": {
"bodySize": 192,
"cookies": [],
"headers": [
{
"name": "accept",
"value": "application/json"
},
{
"name": "anthropic-beta",
"value": "interleaved-thinking-2025-05-14"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "user-agent",
"value": "Anthropic/JS 0.56.0"
},
{
"name": "x-stainless-arch",
"value": "arm64"
},
{
"name": "x-stainless-lang",
"value": "js"
},
{
"name": "x-stainless-os",
"value": "MacOS"
},
{
"name": "x-stainless-package-version",
"value": "0.56.0"
},
{
"name": "x-stainless-retry-count",
"value": "0"
},
{
"name": "x-stainless-runtime",
"value": "node"
},
{
"name": "x-stainless-runtime-version",
"value": "v20.11.1"
},
{
"name": "x-stainless-timeout",
"value": "600"
}
],
"headersSize": 584,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "application/json",
"params": [],
"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}}"
},
"queryString": [
{
"name": "beta",
"value": "true"
}
],
"url": "https://api.anthropic.com/v1/messages?beta=true"
},
"response": {
"bodySize": 1570,
"content": {
"mimeType": "application/json",
"size": 1570,
"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\"}}"
},
"cookies": [],
"headers": [
{
"name": "anthropic-organization-id",
"value": "617d109c-a187-4902-889d-689223d134aa"
},
{
"name": "anthropic-ratelimit-input-tokens-limit",
"value": "2000000"
},
{
"name": "anthropic-ratelimit-input-tokens-remaining",
"value": "2000000"
},
{
"name": "anthropic-ratelimit-input-tokens-reset",
"value": "2025-08-21T11:41:58Z"
},
{
"name": "anthropic-ratelimit-output-tokens-limit",
"value": "400000"
},
{
"name": "anthropic-ratelimit-output-tokens-remaining",
"value": "400000"
},
{
"name": "anthropic-ratelimit-output-tokens-reset",
"value": "2025-08-21T11:42:02Z"
},
{
"name": "anthropic-ratelimit-requests-limit",
"value": "4000"
},
{
"name": "anthropic-ratelimit-requests-remaining",
"value": "3999"
},
{
"name": "anthropic-ratelimit-requests-reset",
"value": "2025-08-21T11:41:57Z"
},
{
"name": "anthropic-ratelimit-tokens-limit",
"value": "2400000"
},
{
"name": "anthropic-ratelimit-tokens-remaining",
"value": "2400000"
},
{
"name": "anthropic-ratelimit-tokens-reset",
"value": "2025-08-21T11:41:58Z"
},
{
"name": "cf-cache-status",
"value": "DYNAMIC"
},
{
"name": "cf-ray",
"value": "9729dd411cdd6756-ATL"
},
{
"name": "connection",
"value": "keep-alive"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "date",
"value": "Thu, 21 Aug 2025 11:42:02 GMT"
},
{
"name": "request-id",
"value": "req_011CSLo11ceKMKF1kTBWoKxZ"
},
{
"name": "server",
"value": "cloudflare"
},
{
"name": "strict-transport-security",
"value": "max-age=31536000; includeSubDomains; preload"
},
{
"name": "transfer-encoding",
"value": "chunked"
},
{
"name": "via",
"value": "1.1 google"
},
{
"name": "x-envoy-upstream-service-time",
"value": "5555"
},
{
"name": "x-robots-tag",
"value": "none"
}
],
"headersSize": 1098,
"httpVersion": "HTTP/1.1",
"redirectURL": "",
"status": 200,
"statusText": "OK"
},
"startedDateTime": "2025-08-21T11:41:56.089Z",
"time": 6594,
"timings": {
"blocked": -1,
"connect": -1,
"dns": -1,
"receive": 0,
"send": 0,
"ssl": -1,
"wait": 6594
}
}
],
"pages": [],
"version": "1.2"
}
}
20 changes: 20 additions & 0 deletions packages/instrumentation-anthropic/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
Message,
MessageStreamEvent,
} from "@anthropic-ai/sdk/resources/messages";
import type { MessageCreateParamsNonStreaming as BetaMessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/beta/messages";
import type { Stream } from "@anthropic-ai/sdk/streaming";
import type { APIPromise, BaseAnthropic } from "@anthropic-ai/sdk";

Expand Down Expand Up @@ -72,6 +73,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
"create",
this.patchAnthropic("chat", module),
);
this._wrap(
module.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", module),
);
Comment on lines +76 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard Beta.Messages patching for SDK versions without Beta API.

Accessing module.Anthropic.Beta.Messages.prototype unconditionally can throw on older @anthropic-ai/sdk versions. Add a structural guard to avoid runtime errors and log that Beta patching is skipped.

Apply this diff:

-    this._wrap(
-      module.Anthropic.Beta.Messages.prototype,
-      "create",
-      this.patchAnthropic("chat", module),
-    );
+    if (module?.Anthropic?.Beta?.Messages?.prototype?.create) {
+      this._wrap(
+        module.Anthropic.Beta.Messages.prototype,
+        "create",
+        this.patchAnthropic("chat", module),
+      );
+    } else {
+      this._diag.debug(
+        "Anthropic Beta.Messages.create not found; skipping manual beta patch"
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._wrap(
module.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", module),
);
// Guard Beta.Messages patching for SDK versions without Beta API.
if (module?.Anthropic?.Beta?.Messages?.prototype?.create) {
this._wrap(
module.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", module),
);
} else {
this._diag.debug(
"Anthropic Beta.Messages.create not found; skipping manual beta patch"
);
}
🤖 Prompt for AI Agents
In packages/instrumentation-anthropic/src/instrumentation.ts around lines 75 to
79, the code unconditionally accesses module.Anthropic.Beta.Messages.prototype
which throws on older @anthropic-ai/sdk; modify this section to check that
module.Anthropic, module.Anthropic.Beta, and module.Anthropic.Beta.Messages (and
its prototype) exist before calling this._wrap, and if any are missing call the
logger to record that Beta patching was skipped so runtime errors are avoided.

}

protected init(): InstrumentationModuleDefinition {
Expand All @@ -97,6 +103,11 @@ export class AnthropicInstrumentation extends InstrumentationBase {
"create",
this.patchAnthropic("chat", moduleExports),
);
this._wrap(
moduleExports.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", moduleExports),
);
Comment on lines +106 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same guard needed in automatic patch() path.

Mirror the safety check here to prevent TypeError when the installed SDK lacks the Beta API.

-    this._wrap(
-      moduleExports.Anthropic.Beta.Messages.prototype,
-      "create",
-      this.patchAnthropic("chat", moduleExports),
-    );
+    if (moduleExports?.Anthropic?.Beta?.Messages?.prototype?.create) {
+      this._wrap(
+        moduleExports.Anthropic.Beta.Messages.prototype,
+        "create",
+        this.patchAnthropic("chat", moduleExports),
+      );
+    } else {
+      this._diag.debug(
+        "Anthropic Beta.Messages.create not found; skipping beta patch"
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._wrap(
moduleExports.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", moduleExports),
);
if (moduleExports?.Anthropic?.Beta?.Messages?.prototype?.create) {
this._wrap(
moduleExports.Anthropic.Beta.Messages.prototype,
"create",
this.patchAnthropic("chat", moduleExports),
);
} else {
this._diag.debug(
"Anthropic Beta.Messages.create not found; skipping beta patch"
);
}
🤖 Prompt for AI Agents
In packages/instrumentation-anthropic/src/instrumentation.ts around lines 105 to
109, the automatic patch path unconditionally attempts to wrap
moduleExports.Anthropic.Beta.Messages.prototype.create which throws a TypeError
when the installed SDK lacks the Beta API; add the same safety guard used
elsewhere to first verify moduleExports.Anthropic, moduleExports.Anthropic.Beta
and moduleExports.Anthropic.Beta.Messages and that prototype.create exists
before calling this._wrap, and return/skip the wrap when any of those are
missing so the patch is safe for SDKs without Beta.

return moduleExports;
}

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

this._unwrap(moduleExports.Anthropic.Completions.prototype, "create");
this._unwrap(moduleExports.Anthropic.Messages.prototype, "create");
this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard unwrap to match conditional wrapping.

Unwrapping an undefined prototype will also throw. Protect the unwrap with the same shape check.

-    this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
+    if (moduleExports?.Anthropic?.Beta?.Messages?.prototype) {
+      this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
if (moduleExports?.Anthropic?.Beta?.Messages?.prototype) {
this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create");
}
🤖 Prompt for AI Agents
In packages/instrumentation-anthropic/src/instrumentation.ts around line 121,
the call this._unwrap(moduleExports.Anthropic.Beta.Messages.prototype, "create")
can throw if the prototype is undefined; guard the unwrap with the same
conditional shape check used when wrapping (check that moduleExports.Anthropic,
Beta, Messages and Messages.prototype all exist) and only call this._unwrap when
that prototype is present so unwrapping mirrors the conditional wrapping logic.

}

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

// Handle thinking parameters (for beta messages)
const betaParams = params as BetaMessageCreateParamsNonStreaming;
if (betaParams.thinking && betaParams.thinking.type === "enabled") {
attributes["llm.request.thinking.type"] = betaParams.thinking.type;
attributes["llm.request.thinking.budget_tokens"] =
betaParams.thinking.budget_tokens;
}

if (type === "completion") {
attributes[SpanAttributes.LLM_REQUEST_MAX_TOKENS] =
params.max_tokens_to_sample;
Expand Down
97 changes: 97 additions & 0 deletions packages/instrumentation-anthropic/test/instrumentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,101 @@ describe("Test Anthropic instrumentation", async function () {
"user",
);
}).timeout(30000);

it("should set attributes in span for beta messages with thinking", async () => {
const message = await anthropic.beta.messages.create({
max_tokens: 2048,
betas: ["interleaved-thinking-2025-05-14"],
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,
},
});

const spans = memoryExporter.getFinishedSpans();
const chatSpan = spans.find((span) => span.name === "anthropic.chat");

assert.ok(message);
assert.ok(chatSpan);
assert.strictEqual(
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MODEL}`],
"claude-opus-4-1-20250805",
);
assert.strictEqual(
chatSpan.attributes[`${SpanAttributes.LLM_RESPONSE_MODEL}`],
"claude-opus-4-1-20250805",
);
assert.strictEqual(
chatSpan.attributes[`${SpanAttributes.LLM_REQUEST_MAX_TOKENS}`],
2048,
);

// Check if thinking parameters are captured (these will fail initially)
assert.strictEqual(
chatSpan.attributes["llm.request.thinking.type"],
"enabled",
);
assert.strictEqual(
chatSpan.attributes["llm.request.thinking.budget_tokens"],
1024,
);

// Check prompts
assert.strictEqual(
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`],
"user",
);
assert.strictEqual(
chatSpan.attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`],
"What is 2+2? Think through this step by step.",
);

// Check that we capture both thinking and regular content blocks
const content = JSON.parse(
chatSpan.attributes[
`${SpanAttributes.LLM_COMPLETIONS}.0.content`
] as string,
);
assert.ok(Array.isArray(content));

interface ContentBlock {
type: string;
thinking?: string;
text?: string;
}

const thinkingBlock = content.find(
(block: ContentBlock) => block.type === "thinking",
);
const textBlock = content.find(
(block: ContentBlock) => block.type === "text",
);

assert.ok(thinkingBlock, "Should contain a thinking block");
assert.ok(
thinkingBlock.thinking,
"Thinking block should have thinking content",
);
assert.ok(textBlock, "Should contain a text block");
assert.ok(textBlock.text, "Text block should have text content");

// Verify token usage includes thinking tokens
const completionTokens =
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`];
const promptTokens =
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`];
const totalTokens =
chatSpan.attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`];

assert.ok(completionTokens && +completionTokens > 0);
assert.ok(promptTokens && +promptTokens > 0);
assert.equal(+promptTokens + +completionTokens, totalTokens);
}).timeout(30000);
});