From 97e856e7dfd289e6010e1e964060f62bf81e38a8 Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Fri, 5 Dec 2025 16:29:36 -0500 Subject: [PATCH 1/5] spec change --- .../src/anthropic-message-metadata.ts | 29 ++++++++++++ .../anthropic/src/anthropic-messages-api.ts | 30 ++++++++++++ .../src/anthropic-messages-language-model.ts | 47 ++++++++++++++++++- .../src/anthropic-messages-options.ts | 16 +++++++ .../anthropic/src/anthropic-prepare-tools.ts | 32 ++++++++++++- packages/anthropic/src/index.ts | 11 ++++- 6 files changed, 160 insertions(+), 5 deletions(-) diff --git a/packages/anthropic/src/anthropic-message-metadata.ts b/packages/anthropic/src/anthropic-message-metadata.ts index f1b6b39074f5..770d359108e5 100644 --- a/packages/anthropic/src/anthropic-message-metadata.ts +++ b/packages/anthropic/src/anthropic-message-metadata.ts @@ -1,5 +1,34 @@ import { JSONObject } from '@ai-sdk/provider'; +export type AnthropicToolCallCaller = + | { + /** + * Direct invocation by Claude. + */ + type: 'direct'; + } + | { + /** + * Programmatic invocation from within code execution. + */ + type: 'code_execution_20250825'; + /** + * The ID of the code execution tool that made the programmatic call. + */ + toolId: string; + }; + +/** + * Anthropic-specific metadata for tool calls. + */ +export interface AnthropicToolCallMetadata { + /** + * Information about how the tool was called. + * Present when programmatic tool calling is used. + */ + caller?: AnthropicToolCallCaller; +} + export interface AnthropicMessageMetadata { usage: JSONObject; // TODO remove cacheCreationInputTokens in AI SDK 6 diff --git a/packages/anthropic/src/anthropic-messages-api.ts b/packages/anthropic/src/anthropic-messages-api.ts index 0c6d90947824..3eda61063278 100644 --- a/packages/anthropic/src/anthropic-messages-api.ts +++ b/packages/anthropic/src/anthropic-messages-api.ts @@ -272,6 +272,13 @@ export interface AnthropicMcpToolResultContent { cache_control: AnthropicCacheControl | undefined; } +/** + * The `allowed_callers` field specifies which contexts can invoke a tool. + * + * @see https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling + */ +export type AnthropicAllowedCaller = 'direct' | 'code_execution_20250825'; + export type AnthropicTool = | { name: string; @@ -279,6 +286,7 @@ export type AnthropicTool = input_schema: JSONSchema7; cache_control: AnthropicCacheControl | undefined; strict?: boolean; + allowed_callers?: AnthropicAllowedCaller[]; } | { type: 'code_execution_20250522'; @@ -416,6 +424,17 @@ export const anthropicMessagesResponseSchema = lazySchema(() => id: z.string(), name: z.string(), input: z.unknown(), + caller: z + .discriminatedUnion('type', [ + z.object({ + type: z.literal('direct'), + }), + z.object({ + type: z.literal('code_execution_20250825'), + tool_id: z.string(), + }), + ]) + .optional(), }), z.object({ type: z.literal('server_tool_use'), @@ -620,6 +639,17 @@ export const anthropicMessagesChunkSchema = lazySchema(() => type: z.literal('tool_use'), id: z.string(), name: z.string(), + caller: z + .discriminatedUnion('type', [ + z.object({ + type: z.literal('direct'), + }), + z.object({ + type: z.literal('code_execution_20250825'), + tool_id: z.string(), + }), + ]) + .optional(), }), z.object({ type: z.literal('redacted_thinking'), diff --git a/packages/anthropic/src/anthropic-messages-language-model.ts b/packages/anthropic/src/anthropic-messages-language-model.ts index f46e783d4571..0454732f040a 100644 --- a/packages/anthropic/src/anthropic-messages-language-model.ts +++ b/packages/anthropic/src/anthropic-messages-language-model.ts @@ -29,7 +29,11 @@ import { resolve, } from '@ai-sdk/provider-utils'; import { anthropicFailedResponseHandler } from './anthropic-error'; -import { AnthropicMessageMetadata } from './anthropic-message-metadata'; +import { + AnthropicMessageMetadata, + AnthropicToolCallCaller, + AnthropicToolCallMetadata, +} from './anthropic-message-metadata'; import { AnthropicContainer, anthropicMessagesChunkSchema, @@ -637,11 +641,30 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { text: JSON.stringify(part.input), }); } else { + // Map caller info from API response to SDK format + const caller: AnthropicToolCallCaller | undefined = part.caller + ? part.caller.type === 'direct' + ? { type: 'direct' } + : { + type: 'code_execution_20250825', + toolId: part.caller.tool_id, + } + : undefined; + content.push({ type: 'tool-call', toolCallId: part.id, toolName: part.name, input: JSON.stringify(part.input), + ...(caller != null + ? { + providerMetadata: { + anthropic: { + caller, + } satisfies AnthropicToolCallMetadata, + }, + } + : {}), }); } @@ -920,6 +943,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { providerExecuted?: boolean; firstDelta: boolean; providerToolName?: string; + caller?: AnthropicToolCallCaller; } | { type: 'text' | 'reasoning' } > = {}; @@ -1032,12 +1056,24 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { id: String(value.index), }); } else { + // Map caller info from API response to SDK format + const caller: AnthropicToolCallCaller | undefined = + part.caller + ? part.caller.type === 'direct' + ? { type: 'direct' } + : { + type: 'code_execution_20250825', + toolId: part.caller.tool_id, + } + : undefined; + contentBlocks[value.index] = { type: 'tool-call', toolCallId: part.id, toolName: part.name, input: '', firstDelta: true, + caller, }; controller.enqueue({ @@ -1307,6 +1343,15 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { input: contentBlock.input === '' ? '{}' : contentBlock.input, providerExecuted: contentBlock.providerExecuted, + ...(contentBlock.caller != null + ? { + providerMetadata: { + anthropic: { + caller: contentBlock.caller, + } satisfies AnthropicToolCallMetadata, + }, + } + : {}), }); } break; diff --git a/packages/anthropic/src/anthropic-messages-options.ts b/packages/anthropic/src/anthropic-messages-options.ts index 0bb9245ba36a..dad392ce5399 100644 --- a/packages/anthropic/src/anthropic-messages-options.ts +++ b/packages/anthropic/src/anthropic-messages-options.ts @@ -57,6 +57,22 @@ export type AnthropicFilePartProviderOptions = z.infer< typeof anthropicFilePartProviderOptions >; +/** + * Anthropic tool provider options for function tools. + * These options allow configuring how tools can be called. + * + * @see https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling + */ +export const anthropicToolProviderOptions = z.object({ + allowedCallers: z + .array(z.enum(['direct', 'code_execution_20250825'])) + .optional(), +}); + +export type AnthropicToolProviderOptions = z.infer< + typeof anthropicToolProviderOptions +>; + export const anthropicProviderOptions = z.object({ /** * Whether to send reasoning to the model. diff --git a/packages/anthropic/src/anthropic-prepare-tools.ts b/packages/anthropic/src/anthropic-prepare-tools.ts index 872c737ae0ff..e0348952dfe1 100644 --- a/packages/anthropic/src/anthropic-prepare-tools.ts +++ b/packages/anthropic/src/anthropic-prepare-tools.ts @@ -3,12 +3,17 @@ import { SharedV3Warning, UnsupportedFunctionalityError, } from '@ai-sdk/provider'; -import { AnthropicTool, AnthropicToolChoice } from './anthropic-messages-api'; +import { parseProviderOptions, validateTypes } from '@ai-sdk/provider-utils'; +import { + AnthropicAllowedCaller, + AnthropicTool, + AnthropicToolChoice, +} from './anthropic-messages-api'; +import { anthropicToolProviderOptions } from './anthropic-messages-options'; import { CacheControlValidator } from './get-cache-control'; import { textEditor_20250728ArgsSchema } from './tool/text-editor_20250728'; import { webSearch_20250305ArgsSchema } from './tool/web-search_20250305'; import { webFetch_20250910ArgsSchema } from './tool/web-fetch-20250910'; -import { validateTypes } from '@ai-sdk/provider-utils'; export async function prepareTools({ tools, @@ -53,6 +58,18 @@ export async function prepareTools({ canCache: true, }); + // Parse Anthropic-specific tool options + const anthropicToolOptions = await parseProviderOptions({ + provider: 'anthropic', + providerOptions: tool.providerOptions, + schema: anthropicToolProviderOptions, + }); + + // Convert allowedCallers to Anthropic API format + const allowedCallers = anthropicToolOptions?.allowedCallers as + | AnthropicAllowedCaller[] + | undefined; + anthropicTools.push({ name: tool.name, description: tool.description, @@ -68,12 +85,23 @@ export async function prepareTools({ ), } : {}), + ...(allowedCallers != null && allowedCallers.length > 0 + ? { allowed_callers: allowedCallers } + : {}), }); if (tool.inputExamples != null) { betas.add('advanced-tool-use-2025-11-20'); } + // Add advanced-tool-use beta when programmatic tool calling is used + if ( + allowedCallers != null && + allowedCallers.includes('code_execution_20250825') + ) { + betas.add('advanced-tool-use-2025-11-20'); + } + break; } diff --git a/packages/anthropic/src/index.ts b/packages/anthropic/src/index.ts index 27d9e5b479a1..a68bf6d97c8f 100644 --- a/packages/anthropic/src/index.ts +++ b/packages/anthropic/src/index.ts @@ -1,5 +1,12 @@ -export type { AnthropicMessageMetadata } from './anthropic-message-metadata'; -export type { AnthropicProviderOptions } from './anthropic-messages-options'; +export type { + AnthropicMessageMetadata, + AnthropicToolCallCaller, + AnthropicToolCallMetadata, +} from './anthropic-message-metadata'; +export type { + AnthropicProviderOptions, + AnthropicToolProviderOptions, +} from './anthropic-messages-options'; export { anthropic, createAnthropic } from './anthropic-provider'; export type { AnthropicProvider, From 258ed06aaaa970c68e2b933efe44c9ea46fbe029 Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Mon, 8 Dec 2025 10:27:32 -0500 Subject: [PATCH 2/5] adding example + flow changes --- .../anthropic-programmatic-tool-calling.ts | 63 ++ .../src/generate-text/generate-text.test.ts | 54 +- .../ai/src/generate-text/generate-text.ts | 28 +- .../ai/src/generate-text/parse-tool-call.ts | 31 + packages/ai/src/generate-text/prepare-step.ts | 8 +- .../ai/src/generate-text/stream-text.test.ts | 4 + packages/ai/src/generate-text/stream-text.ts | 10 +- .../anthropic/src/anthropic-messages-api.ts | 7 + .../anthropic-messages-language-model.test.ts | 1 + .../src/anthropic-messages-language-model.ts | 26 +- ...nvert-to-anthropic-messages-prompt.test.ts | 3 + .../convert-to-anthropic-messages-prompt.ts | 28 +- packages/anthropic/src/tool/programmatic.md | 853 ++++++++++++++++++ 13 files changed, 1049 insertions(+), 67 deletions(-) create mode 100644 examples/ai-core/src/generate-text/anthropic-programmatic-tool-calling.ts create mode 100644 packages/anthropic/src/tool/programmatic.md diff --git a/examples/ai-core/src/generate-text/anthropic-programmatic-tool-calling.ts b/examples/ai-core/src/generate-text/anthropic-programmatic-tool-calling.ts new file mode 100644 index 000000000000..93ddbdbfe6ad --- /dev/null +++ b/examples/ai-core/src/generate-text/anthropic-programmatic-tool-calling.ts @@ -0,0 +1,63 @@ +import { anthropic, AnthropicMessageMetadata } from '@ai-sdk/anthropic'; +import { generateText, stepCountIs, tool } from 'ai'; +import { z } from 'zod'; +import { run } from '../lib/run'; + +run(async () => { + const result = await generateText({ + model: anthropic('claude-sonnet-4-5'), + prompt: + 'Query sales data for West, East, and Central regions, ' + + 'then tell me which region had the highest revenue', + stopWhen: stepCountIs(10), + tools: { + code_execution: anthropic.tools.codeExecution_20250825(), + + queryDatabase: tool({ + description: 'Execute a SQL query against the sales database', + inputSchema: z.object({ + sql: z.string().describe('SQL query to execute'), + }), + execute: async () => { + return [ + { region: 'West', revenue: 45000 }, + { region: 'East', revenue: 38000 }, + { region: 'Central', revenue: 52000 }, + ]; + }, + providerOptions: { + anthropic: { + allowedCallers: ['code_execution_20250825'], + }, + }, + }), + }, + prepareStep: ({ steps }) => { + if (steps.length === 0) { + return undefined; + } + + const lastStep = steps[steps.length - 1]; + const containerId = ( + lastStep.providerMetadata?.anthropic as + | AnthropicMessageMetadata + | undefined + )?.container?.id; + + if (!containerId) { + return undefined; + } + + return { + providerOptions: { + anthropic: { + container: { id: containerId }, + }, + }, + }; + }, + }); + + console.log('Text:', result.text); + console.log('Steps:', result.steps.length); +}); diff --git a/packages/ai/src/generate-text/generate-text.test.ts b/packages/ai/src/generate-text/generate-text.test.ts index c81eb8f4aa42..9382c3141a81 100644 --- a/packages/ai/src/generate-text/generate-text.test.ts +++ b/packages/ai/src/generate-text/generate-text.test.ts @@ -2635,6 +2635,7 @@ describe('generateText', () => { expect(result.content).toMatchInlineSnapshot(` [ { + "dynamic": true, "input": { "value": "value", }, @@ -2646,7 +2647,7 @@ describe('generateText', () => { "type": "tool-call", }, { - "dynamic": undefined, + "dynamic": true, "input": { "value": "value", }, @@ -2657,6 +2658,7 @@ describe('generateText', () => { "type": "tool-result", }, { + "dynamic": true, "input": { "value": "value", }, @@ -2668,7 +2670,7 @@ describe('generateText', () => { "type": "tool-call", }, { - "dynamic": undefined, + "dynamic": true, "error": "ERROR", "input": { "value": "value", @@ -2683,50 +2685,11 @@ describe('generateText', () => { }); it('should include provider-executed tool calls in staticToolCalls', async () => { - expect(result.staticToolCalls).toMatchInlineSnapshot(` - [ - { - "input": { - "value": "value", - }, - "providerExecuted": true, - "providerMetadata": undefined, - "title": undefined, - "toolCallId": "call-1", - "toolName": "web_search", - "type": "tool-call", - }, - { - "input": { - "value": "value", - }, - "providerExecuted": true, - "providerMetadata": undefined, - "title": undefined, - "toolCallId": "call-2", - "toolName": "web_search", - "type": "tool-call", - }, - ] - `); + expect(result.staticToolCalls).toMatchInlineSnapshot(`[]`); }); it('should include provider-executed results in staticToolResults (errors excluded)', async () => { - expect(result.staticToolResults).toMatchInlineSnapshot(` - [ - { - "dynamic": undefined, - "input": { - "value": "value", - }, - "output": "{ "value": "result1" }", - "providerExecuted": true, - "toolCallId": "call-1", - "toolName": "web_search", - "type": "tool-result", - }, - ] - `); + expect(result.staticToolResults).toMatchInlineSnapshot(`[]`); }); it('should only execute a single step', async () => { @@ -3286,6 +3249,7 @@ describe('generateText', () => { expect(result.content).toMatchInlineSnapshot(` [ { + "dynamic": true, "input": { "value": "test", }, @@ -3297,7 +3261,7 @@ describe('generateText', () => { "type": "tool-call", }, { - "dynamic": undefined, + "dynamic": true, "input": { "value": "test", }, @@ -3316,7 +3280,7 @@ describe('generateText', () => { expect(result.toolResults).toMatchInlineSnapshot(` [ { - "dynamic": undefined, + "dynamic": true, "input": { "value": "test", }, diff --git a/packages/ai/src/generate-text/generate-text.ts b/packages/ai/src/generate-text/generate-text.ts index b13ccb4ce81d..8dacf25d75a1 100644 --- a/packages/ai/src/generate-text/generate-text.ts +++ b/packages/ai/src/generate-text/generate-text.ts @@ -473,13 +473,21 @@ A function that attempts to repair a tool call that failed to parse. }), tracer, fn: async span => { + // Merge base providerOptions with step-specific providerOptions + const stepProviderOptions = prepareStepResult?.providerOptions + ? { + ...providerOptions, + ...prepareStepResult.providerOptions, + } + : providerOptions; + const result = await stepModel.doGenerate({ ...callSettings, tools: stepTools, toolChoice: stepToolChoice, responseFormat: await output?.responseFormat, prompt: promptMessages, - providerOptions, + providerOptions: stepProviderOptions, abortSignal, headers: headersWithUserAgent, }); @@ -978,21 +986,23 @@ function asContent({ case 'tool-result': { const toolCall = toolCalls.find( toolCall => toolCall.toolCallId === part.toolCallId, - )!; + ); - if (toolCall == null) { - throw new Error(`Tool call ${part.toolCallId} not found.`); - } + // For provider-executed tool results (like code_execution_tool_result), + // the tool call might be from a previous step (programmatic tool calling). + // In this case, we use the information from the tool result itself. + const input = toolCall?.input; + const dynamic = part.dynamic ?? toolCall?.dynamic; if (part.isError) { return { type: 'tool-error' as const, toolCallId: part.toolCallId, toolName: part.toolName as keyof TOOLS & string, - input: toolCall.input, + input, error: part.result, providerExecuted: true, - dynamic: toolCall.dynamic, + dynamic, } as TypedToolError; } @@ -1000,10 +1010,10 @@ function asContent({ type: 'tool-result' as const, toolCallId: part.toolCallId, toolName: part.toolName as keyof TOOLS & string, - input: toolCall.input, + input, output: part.result, providerExecuted: true, - dynamic: toolCall.dynamic, + dynamic, } as TypedToolResult; } } diff --git a/packages/ai/src/generate-text/parse-tool-call.ts b/packages/ai/src/generate-text/parse-tool-call.ts index f0bd624cb06d..292b24ea6119 100644 --- a/packages/ai/src/generate-text/parse-tool-call.ts +++ b/packages/ai/src/generate-text/parse-tool-call.ts @@ -148,6 +148,37 @@ async function doParseToolCall({ }); } + // For provider-executed tools, skip schema validation since the provider + // already executed the tool. Just parse the JSON input. + // This is needed for cases like programmatic tool calling where the + // provider returns a different input format than the tool's schema. + // We treat these as dynamic since we don't have type guarantees on the input. + if (toolCall.providerExecuted) { + const parseResult = + toolCall.input.trim() === '' + ? { success: true as const, value: {} } + : await safeParseJSON({ text: toolCall.input }); + + if (parseResult.success === false) { + throw new InvalidToolInputError({ + toolName, + toolInput: toolCall.input, + cause: parseResult.error, + }); + } + + return { + type: 'tool-call', + toolCallId: toolCall.toolCallId, + toolName: toolCall.toolName, + input: parseResult.value, + providerExecuted: true, + providerMetadata: toolCall.providerMetadata, + dynamic: true, + title: tool.title, + }; + } + const schema = asSchema(tool.inputSchema); // when the tool call has no arguments, we try passing an empty object to the schema diff --git a/packages/ai/src/generate-text/prepare-step.ts b/packages/ai/src/generate-text/prepare-step.ts index 7cfb84eac697..1904fd89525e 100644 --- a/packages/ai/src/generate-text/prepare-step.ts +++ b/packages/ai/src/generate-text/prepare-step.ts @@ -1,4 +1,9 @@ -import { ModelMessage, SystemModelMessage, Tool } from '@ai-sdk/provider-utils'; +import { + ModelMessage, + ProviderOptions, + SystemModelMessage, + Tool, +} from '@ai-sdk/provider-utils'; import { LanguageModel, ToolChoice } from '../types/language-model'; import { StepResult } from './step-result'; @@ -31,5 +36,6 @@ export type PrepareStepResult< activeTools?: Array>; system?: string | SystemModelMessage; messages?: Array; + providerOptions?: ProviderOptions; } | undefined; diff --git a/packages/ai/src/generate-text/stream-text.test.ts b/packages/ai/src/generate-text/stream-text.test.ts index b635a13073a9..2125c657ab46 100644 --- a/packages/ai/src/generate-text/stream-text.test.ts +++ b/packages/ai/src/generate-text/stream-text.test.ts @@ -8757,6 +8757,7 @@ describe('streamText', () => { expect(await result.content).toMatchInlineSnapshot(` [ { + "dynamic": true, "input": { "value": "value", }, @@ -8779,6 +8780,7 @@ describe('streamText', () => { "type": "tool-result", }, { + "dynamic": true, "input": { "value": "value", }, @@ -8834,6 +8836,7 @@ describe('streamText', () => { "type": "tool-input-end", }, { + "dynamic": true, "input": { "value": "value", }, @@ -8856,6 +8859,7 @@ describe('streamText', () => { "type": "tool-result", }, { + "dynamic": true, "input": { "value": "value", }, diff --git a/packages/ai/src/generate-text/stream-text.ts b/packages/ai/src/generate-text/stream-text.ts index 7f85024ee86d..1e173f2ea044 100644 --- a/packages/ai/src/generate-text/stream-text.ts +++ b/packages/ai/src/generate-text/stream-text.ts @@ -1208,6 +1208,14 @@ class DefaultStreamTextResult activeTools: prepareStepResult?.activeTools ?? activeTools, }); + // Merge base providerOptions with step-specific providerOptions + const stepProviderOptions = prepareStepResult?.providerOptions + ? { + ...providerOptions, + ...prepareStepResult.providerOptions, + } + : providerOptions; + const { result: { stream, response, request }, doStreamSpan, @@ -1266,7 +1274,7 @@ class DefaultStreamTextResult toolChoice: stepToolChoice, responseFormat: await output?.responseFormat, prompt: promptMessages, - providerOptions, + providerOptions: stepProviderOptions, abortSignal, headers, includeRawChunks, diff --git a/packages/anthropic/src/anthropic-messages-api.ts b/packages/anthropic/src/anthropic-messages-api.ts index 3eda61063278..b157de18eced 100644 --- a/packages/anthropic/src/anthropic-messages-api.ts +++ b/packages/anthropic/src/anthropic-messages-api.ts @@ -102,6 +102,13 @@ export interface AnthropicToolCallContent { name: string; input: unknown; cache_control: AnthropicCacheControl | undefined; + /** + * Information about how the tool was called. + * Present when programmatic tool calling is used. + */ + caller?: + | { type: 'direct' } + | { type: 'code_execution_20250825'; tool_id: string }; } export interface AnthropicServerToolUseContent { diff --git a/packages/anthropic/src/anthropic-messages-language-model.test.ts b/packages/anthropic/src/anthropic-messages-language-model.test.ts index 26bcb491a02e..75abb81db006 100644 --- a/packages/anthropic/src/anthropic-messages-language-model.test.ts +++ b/packages/anthropic/src/anthropic-messages-language-model.test.ts @@ -2766,6 +2766,7 @@ describe('AnthropicMessagesLanguageModel', () => { }, { "result": { + "content": [], "return_code": 0, "stderr": "", "stdout": "Hello, World! diff --git a/packages/anthropic/src/anthropic-messages-language-model.ts b/packages/anthropic/src/anthropic-messages-language-model.ts index 0454732f040a..0d92951bea2b 100644 --- a/packages/anthropic/src/anthropic-messages-language-model.ts +++ b/packages/anthropic/src/anthropic-messages-language-model.ts @@ -307,16 +307,20 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { })), }), - // container with agent skills: + // container: string ID for resuming, or object with skills for agent skills ...(anthropicOptions?.container && { - container: { - id: anthropicOptions.container.id, - skills: anthropicOptions.container.skills?.map(skill => ({ - type: skill.type, - skill_id: skill.skillId, - version: skill.version, - })), - } satisfies AnthropicContainer, + container: + anthropicOptions.container.skills && + anthropicOptions.container.skills.length > 0 + ? ({ + id: anthropicOptions.container.id, + skills: anthropicOptions.container.skills.map(skill => ({ + type: skill.type, + skill_id: skill.skillId, + version: skill.version, + })), + } satisfies AnthropicContainer) + : anthropicOptions.container.id, }), // prompt: @@ -809,7 +813,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { break; } - // code execution 20250522: + // code execution 20250522 (also used for programmatic tool calling with 20250825): case 'code_execution_tool_result': { if (part.content.type === 'code_execution_result') { content.push({ @@ -821,6 +825,8 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 { stdout: part.content.stdout, stderr: part.content.stderr, return_code: part.content.return_code, + // Include content array for programmatic tool calling compatibility + content: (part.content as any).content ?? [], }, }); } else if (part.content.type === 'code_execution_tool_result_error') { diff --git a/packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts b/packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts index 0a79e0888a53..508763ca7d25 100644 --- a/packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts +++ b/packages/anthropic/src/convert-to-anthropic-messages-prompt.test.ts @@ -2464,6 +2464,7 @@ describe('citations', () => { }, { "cache_control": undefined, + "caller": undefined, "id": "weather-call-1", "input": { "location": "berlin", @@ -2473,6 +2474,7 @@ describe('citations', () => { }, { "cache_control": undefined, + "caller": undefined, "id": "weather-call-2", "input": { "location": "london", @@ -2482,6 +2484,7 @@ describe('citations', () => { }, { "cache_control": undefined, + "caller": undefined, "id": "weather-call-3", "input": { "location": "paris", diff --git a/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts b/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts index 18ebcf497d46..c52312ffe061 100644 --- a/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts +++ b/packages/anthropic/src/convert-to-anthropic-messages-prompt.ts @@ -17,10 +17,12 @@ import { AnthropicAssistantMessage, AnthropicMessagesPrompt, anthropicReasoningMetadataSchema, + AnthropicToolCallContent, AnthropicToolResultContent, AnthropicUserMessage, AnthropicWebFetchToolResultContent, } from './anthropic-messages-api'; +import { AnthropicToolCallMetadata } from './anthropic-message-metadata'; import { anthropicFilePartProviderOptions } from './anthropic-messages-options'; import { CacheControlValidator } from './get-cache-control'; import { codeExecution_20250522OutputSchema } from './tool/code-execution_20250522'; @@ -563,12 +565,27 @@ export async function convertToAnthropicMessagesPrompt({ break; } + // Extract caller metadata if present (for programmatic tool calling) + const toolCallMetadata = part.providerOptions?.anthropic as + | AnthropicToolCallMetadata + | undefined; + const caller: AnthropicToolCallContent['caller'] = + toolCallMetadata?.caller + ? toolCallMetadata.caller.type === 'direct' + ? { type: 'direct' } + : { + type: 'code_execution_20250825', + tool_id: toolCallMetadata.caller.toolId, + } + : undefined; + anthropicContent.push({ type: 'tool_use', id: part.toolCallId, name: part.toolName, input: part.input, cache_control: cacheControl, + caller, }); break; } @@ -627,12 +644,17 @@ export async function convertToAnthropicMessagesPrompt({ // to distinguish between code execution 20250522 and 20250825, // we check if a type property is present in the output.value if (output.value.type === 'code_execution_result') { - // code execution 20250522 + // code execution 20250522 (also used for programmatic tool calling) const codeExecutionOutput = await validateTypes({ value: output.value, schema: codeExecution_20250522OutputSchema, }); + // Include content array if present (for programmatic tool calling) + const outputWithContent = output.value as { + content?: unknown[]; + }; + anthropicContent.push({ type: 'code_execution_tool_result', tool_use_id: part.toolCallId, @@ -641,6 +663,10 @@ export async function convertToAnthropicMessagesPrompt({ stdout: codeExecutionOutput.stdout, stderr: codeExecutionOutput.stderr, return_code: codeExecutionOutput.return_code, + // Include content array for programmatic tool calling compatibility + ...(outputWithContent.content != null + ? { content: outputWithContent.content } + : {}), }, cache_control: cacheControl, }); diff --git a/packages/anthropic/src/tool/programmatic.md b/packages/anthropic/src/tool/programmatic.md new file mode 100644 index 000000000000..ae027a3c3aa6 --- /dev/null +++ b/packages/anthropic/src/tool/programmatic.md @@ -0,0 +1,853 @@ +# Programmatic tool calling + +--- + +Programmatic tool calling allows Claude to write code that calls your tools programmatically within a [code execution](/docs/en/agents-and-tools/tool-use/code-execution-tool) container, rather than requiring round trips through the model for each tool invocation. This reduces latency for multi-tool workflows and decreases token consumption by allowing Claude to filter or process data before it reaches the model's context window. + + +Programmatic tool calling is currently in public beta. + +To use this feature, add the `"advanced-tool-use-2025-11-20"` [beta header](/docs/en/api/beta-headers) to your API requests. + +This feature requires the code execution tool to be enabled. + + +## Model compatibility + +Programmatic tool calling is available on the following models: + +| Model | Tool Version | +| ------------------------------------------------ | ------------------------- | +| Claude Opus 4.5 (`claude-opus-4-5-20251101`) | `code_execution_20250825` | +| Claude Sonnet 4.5 (`claude-sonnet-4-5-20250929`) | `code_execution_20250825` | + + +Programmatic tool calling is available via the Claude API and Microsoft Foundry. + + +## Quick start + +Here's a simple example where Claude programmatically queries a database multiple times and aggregates results: + + +```bash Shell +curl https://api.anthropic.com/v1/messages \ + --header "x-api-key: $ANTHROPIC_API_KEY" \ + --header "anthropic-version: 2023-06-01" \ + --header "anthropic-beta: advanced-tool-use-2025-11-20" \ + --header "content-type: application/json" \ + --data '{ + "model": "claude-sonnet-4-5", + "max_tokens": 4096, + "messages": [ + { + "role": "user", + "content": "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" + } + ], + "tools": [ + { + "type": "code_execution_20250825", + "name": "code_execution" + }, + { + "name": "query_database", + "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", + "input_schema": { + "type": "object", + "properties": { + "sql": { + "type": "string", + "description": "SQL query to execute" + } + }, + "required": ["sql"] + }, + "allowed_callers": ["code_execution_20250825"] + } + ] + }' +``` + +```python Python +import anthropic + +client = anthropic.Anthropic() + +response = client.beta.messages.create( + model="claude-sonnet-4-5", + betas=["advanced-tool-use-2025-11-20"], + max_tokens=4096, + messages=[{ + "role": "user", + "content": "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" + }], + tools=[ + { + "type": "code_execution_20250825", + "name": "code_execution" + }, + { + "name": "query_database", + "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", + "input_schema": { + "type": "object", + "properties": { + "sql": { + "type": "string", + "description": "SQL query to execute" + } + }, + "required": ["sql"] + }, + "allowed_callers": ["code_execution_20250825"] + } + ] +) + +print(response) +``` + +```typescript TypeScript +import { Anthropic } from '@anthropic-ai/sdk'; + +const anthropic = new Anthropic(); + +async function main() { + const response = await anthropic.beta.messages.create({ + model: 'claude-sonnet-4-5', + betas: ['advanced-tool-use-2025-11-20'], + max_tokens: 4096, + messages: [ + { + role: 'user', + content: + 'Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue', + }, + ], + tools: [ + { + type: 'code_execution_20250825', + name: 'code_execution', + }, + { + name: 'query_database', + description: + 'Execute a SQL query against the sales database. Returns a list of rows as JSON objects.', + input_schema: { + type: 'object', + properties: { + sql: { + type: 'string', + description: 'SQL query to execute', + }, + }, + required: ['sql'], + }, + allowed_callers: ['code_execution_20250825'], + }, + ], + }); + + console.log(response); +} + +main().catch(console.error); +``` + + + +## How programmatic tool calling works + +When you configure a tool to be callable from code execution and Claude decides to use that tool: + +1. Claude writes Python code that invokes the tool as a function, potentially including multiple tool calls and pre/post-processing logic +2. Claude runs this code in a sandboxed container via code execution +3. When a tool function is called, code execution pauses and the API returns a `tool_use` block +4. You provide the tool result, and code execution continues (intermediate results are not loaded into Claude's context window) +5. Once all code execution completes, Claude receives the final output and continues working on the task + +This approach is particularly useful for: + +- **Large data processing**: Filter or aggregate tool results before they reach Claude's context +- **Multi-step workflows**: Save tokens and latency by calling tools serially or in a loop without sampling Claude in-between tool calls +- **Conditional logic**: Make decisions based on intermediate tool results + + +Custom tools are converted to async Python functions to support parallel tool calling. When Claude writes code that calls your tools, it uses `await` (e.g., `result = await query_database("")`) and automatically includes the appropriate async wrapper function. + +The async wrapper is omitted from code examples in this documentation for clarity. + + +## Core concepts + +### The `allowed_callers` field + +The `allowed_callers` field specifies which contexts can invoke a tool: + +```json +{ + "name": "query_database", + "description": "Execute a SQL query against the database", + "input_schema": {...}, + "allowed_callers": ["code_execution_20250825"] +} +``` + +**Possible values:** + +- `["direct"]` - Only Claude can call this tool directly (default if omitted) +- `["code_execution_20250825"]` - Only callable from within code execution +- `["direct", "code_execution_20250825"]` - Callable both directly and from code execution + + +We recommend choosing either `["direct"]` or `["code_execution_20250825"]` for each tool rather than enabling both, as this provides clearer guidance to Claude for how best to use the tool. + + +### The `caller` field in responses + +Every tool use block includes a `caller` field indicating how it was invoked: + +**Direct invocation (traditional tool use):** + +```json +{ + "type": "tool_use", + "id": "toolu_abc123", + "name": "query_database", + "input": { "sql": "" }, + "caller": { "type": "direct" } +} +``` + +**Programmatic invocation:** + +```json +{ + "type": "tool_use", + "id": "toolu_xyz789", + "name": "query_database", + "input": { "sql": "" }, + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_abc123" + } +} +``` + +The `tool_id` references the code execution tool that made the programmatic call. + +### Container lifecycle + +Programmatic tool calling uses the same containers as code execution: + +- **Container creation**: A new container is created for each session unless you reuse an existing one +- **Expiration**: Containers expire after approximately 4.5 minutes of inactivity (subject to change) +- **Container ID**: Returned in responses via the `container` field +- **Reuse**: Pass the container ID to maintain state across requests + + +When a tool is called programmatically and the container is waiting for your tool result, you must respond before the container expires. Monitor the `expires_at` field. If the container expires, Claude may treat the tool call as timed out and retry it. + + +## Example workflow + +Here's how a complete programmatic tool calling flow works: + +### Step 1: Initial request + +Send a request with code execution and a tool that allows programmatic calling. To enable programmatic calling, add the `allowed_callers` field to your tool definition. + + +Provide detailed descriptions of your tool's output format in the tool description. If you specify that the tool returns JSON, Claude will attempt to deserialize and process the result in code. The more detail you provide about the output schema, the better Claude can handle the response programmatically. + + + +```python Python +response = client.beta.messages.create( + model="claude-sonnet-4-5", + betas=["advanced-tool-use-2025-11-20"], + max_tokens=4096, + messages=[{ + "role": "user", + "content": "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" + }], + tools=[ + { + "type": "code_execution_20250825", + "name": "code_execution" + }, + { + "name": "query_database", + "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", + "input_schema": {...}, + "allowed_callers": ["code_execution_20250825"] + } + ] +) +``` + +```typescript TypeScript +const response = await anthropic.beta.messages.create({ + model: "claude-sonnet-4-5", + betas: ["advanced-tool-use-2025-11-20"], + max_tokens: 4096, + messages: [{ + role: "user", + content: "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" + }], + tools: [ + { + type: "code_execution_20250825", + name: "code_execution" + }, + { + name: "query_database", + description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", + input_schema: {...}, + allowed_callers: ["code_execution_20250825"] + } + ] +}); +``` + + + +### Step 2: API response with tool call + +Claude writes code that calls your tool. The API pauses and returns: + +```json +{ + "role": "assistant", + "content": [ + { + "type": "text", + "text": "I'll query the purchase history and analyze the results." + }, + { + "type": "server_tool_use", + "id": "srvtoolu_abc123", + "name": "code_execution", + "input": { + "code": "results = await query_database('')\ntop_customers = sorted(results, key=lambda x: x['revenue'], reverse=True)[:5]\nprint(f'Top 5 customers: {top_customers}')" + } + }, + { + "type": "tool_use", + "id": "toolu_def456", + "name": "query_database", + "input": { "sql": "" }, + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_abc123" + } + } + ], + "container": { + "id": "container_xyz789", + "expires_at": "2025-01-15T14:30:00Z" + }, + "stop_reason": "tool_use" +} +``` + +### Step 3: Provide tool result + +Include the full conversation history plus your tool result: + + +```python Python +response = client.beta.messages.create( + model="claude-sonnet-4-5", + betas=["advanced-tool-use-2025-11-20"], + max_tokens=4096, + container="container_xyz789", # Reuse the container + messages=[ + {"role": "user", "content": "Query customer purchase history from the last quarter and identify our top 5 customers by revenue"}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "I'll query the purchase history and analyze the results."}, + { + "type": "server_tool_use", + "id": "srvtoolu_abc123", + "name": "code_execution", + "input": {"code": "..."} + }, + { + "type": "tool_use", + "id": "toolu_def456", + "name": "query_database", + "input": {"sql": ""}, + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_abc123" + } + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_def456", + "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]" + } + ] + } + ], + tools=[...] +) +``` + +```typescript TypeScript +const response = await anthropic.beta.messages.create({ + model: "claude-sonnet-4-5", + betas: ["advanced-tool-use-2025-11-20"], + max_tokens: 4096, + container: "container_xyz789", // Reuse the container + messages: [ + { role: "user", content: "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" }, + { + role: "assistant", + content: [ + { type: "text", text: "I'll query the purchase history and analyze the results." }, + { + type: "server_tool_use", + id: "srvtoolu_abc123", + name: "code_execution", + input: { code: "..." } + }, + { + type: "tool_use", + id: "toolu_def456", + name: "query_database", + input: { sql: "" }, + caller: { + type: "code_execution_20250825", + tool_id: "srvtoolu_abc123" + } + } + ] + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_def456", + content: "[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]" + } + ] + } + ], + tools: [...] +}); +``` + + + +### Step 4: Next tool call or completion + +The code execution continues and processes the results. If additional tool calls are needed, repeat Step 3 until all tool calls are satisfied. + +### Step 5: Final response + +Once the code execution completes, Claude provides the final response: + +```json +{ + "content": [ + { + "type": "code_execution_tool_result", + "tool_use_id": "srvtoolu_abc123", + "content": { + "type": "code_execution_result", + "stdout": "Top 5 customers by revenue:\n1. Customer C1: $45,000\n2. Customer C2: $38,000\n3. Customer C5: $32,000\n4. Customer C8: $28,500\n5. Customer C3: $24,000", + "stderr": "", + "return_code": 0, + "content": [] + } + }, + { + "type": "text", + "text": "I've analyzed the purchase history from last quarter. Your top 5 customers generated $167,500 in total revenue, with Customer C1 leading at $45,000." + } + ], + "stop_reason": "end_turn" +} +``` + +## Advanced patterns + +### Batch processing with loops + +Claude can write code that processes multiple items efficiently: + +```python +# async wrapper omitted for clarity +regions = ["West", "East", "Central", "North", "South"] +results = {} +for region in regions: + data = await query_database(f"") + results[region] = sum(row["revenue"] for row in data) + +# Process results programmatically +top_region = max(results.items(), key=lambda x: x[1]) +print(f"Top region: {top_region[0]} with ${top_region[1]:,} in revenue") +``` + +This pattern: + +- Reduces model round-trips from N (one per region) to 1 +- Processes large result sets programmatically before returning to Claude +- Saves tokens by only returning aggregated conclusions instead of raw data + +### Early termination + +Claude can stop processing as soon as success criteria are met: + +```python +# async wrapper omitted for clarity +endpoints = ["us-east", "eu-west", "apac"] +for endpoint in endpoints: + status = await check_health(endpoint) + if status == "healthy": + print(f"Found healthy endpoint: {endpoint}") + break # Stop early, don't check remaining +``` + +### Conditional tool selection + +```python +# async wrapper omitted for clarity +file_info = await get_file_info(path) +if file_info["size"] < 10000: + content = await read_full_file(path) +else: + content = await read_file_summary(path) +print(content) +``` + +### Data filtering + +```python +# async wrapper omitted for clarity +logs = await fetch_logs(server_id) +errors = [log for log in logs if "ERROR" in log] +print(f"Found {len(errors)} errors") +for error in errors[-10:]: # Only return last 10 errors + print(error) +``` + +## Response format + +### Programmatic tool call + +When code execution calls a tool: + +```json +{ + "type": "tool_use", + "id": "toolu_abc123", + "name": "query_database", + "input": { "sql": "" }, + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_xyz789" + } +} +``` + +### Tool result handling + +Your tool result is passed back to the running code: + +```json +{ + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_abc123", + "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000, \"orders\": 23}, {\"customer_id\": \"C2\", \"revenue\": 38000, \"orders\": 18}, ...]" + } + ] +} +``` + +### Code execution completion + +When all tool calls are satisfied and code completes: + +```json +{ + "type": "code_execution_tool_result", + "tool_use_id": "srvtoolu_xyz789", + "content": { + "type": "code_execution_result", + "stdout": "Analysis complete. Top 5 customers identified from 847 total records.", + "stderr": "", + "return_code": 0, + "content": [] + } +} +``` + +## Error handling + +### Common errors + +| Error | Description | Solution | +| --------------------- | -------------------------------------------- | --------------------------------------------------- | +| `invalid_tool_input` | Tool input doesn't match schema | Validate your tool's input_schema | +| `tool_not_allowed` | Tool doesn't allow the requested caller type | Check `allowed_callers` includes the right contexts | +| `missing_beta_header` | PTC beta header not provided | Add both beta headers to your request | + +### Container expiration during tool call + +If your tool takes too long to respond, the code execution will receive a `TimeoutError`. Claude sees this in stderr and will typically retry: + +```json +{ + "type": "code_execution_tool_result", + "tool_use_id": "srvtoolu_abc123", + "content": { + "type": "code_execution_result", + "stdout": "", + "stderr": "TimeoutError: Calling tool ['query_database'] timed out.", + "return_code": 0, + "content": [] + } +} +``` + +To prevent timeouts: + +- Monitor the `expires_at` field in responses +- Implement timeouts for your tool execution +- Consider breaking long operations into smaller chunks + +### Tool execution errors + +If your tool returns an error: + +```python +# Provide error information in the tool result +{ + "type": "tool_result", + "tool_use_id": "toolu_abc123", + "content": "Error: Query timeout - table lock exceeded 30 seconds" +} +``` + +Claude's code will receive this error and can handle it appropriately. + +## Constraints and limitations + +### Feature incompatibilities + +- **Structured outputs**: Tools with `strict: true` are not supported with programmatic calling +- **Tool choice**: You cannot force programmatic calling of a specific tool via `tool_choice` +- **Parallel tool use**: `disable_parallel_tool_use: true` is not supported with programmatic calling + +### Tool restrictions + +The following tools cannot currently be called programmatically, but support may be added in future releases: + +- Web search +- Web fetch +- Tools provided by an [MCP connector](/docs/en/agents-and-tools/mcp-connector) + +### Message formatting restrictions + +When responding to programmatic tool calls, there are strict formatting requirements: + +**Tool result only responses**: If there are pending programmatic tool calls waiting for results, your response message must contain **only** `tool_result` blocks. You cannot include any text content, even after the tool results. + +```json +// ❌ INVALID - Cannot include text when responding to programmatic tool calls +{ + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]"}, + {"type": "text", "text": "What should I do next?"} // This will cause an error + ] +} + +// ✅ VALID - Only tool results when responding to programmatic tool calls +{ + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]"} + ] +} +``` + +This restriction only applies when responding to programmatic (code execution) tool calls. For regular client-side tool calls, you can include text content after tool results. + +### Rate limits + +Programmatic tool calls are subject to the same rate limits as regular tool calls. Each tool call from code execution counts as a separate invocation. + +### Validate tool results before use + +When implementing custom tools that will be called programmatically: + +- **Tool results are returned as strings**: They can contain any content, including code snippets or executable commands that may be processed by the execution environment. +- **Validate external tool results**: If your tool returns data from external sources or accepts user input, be aware of code injection risks if the output will be interpreted or executed as code. + +## Token efficiency + +Programmatic tool calling can significantly reduce token consumption: + +- **Tool results from programmatic calls are not added to Claude's context** - only the final code output is +- **Intermediate processing happens in code** - filtering, aggregation, etc. don't consume model tokens +- **Multiple tool calls in one code execution** - reduces overhead compared to separate model turns + +For example, calling 10 tools directly uses ~10x the tokens of calling them programmatically and returning a summary. + +## Usage and pricing + +Programmatic tool calling uses the same pricing as code execution. See the [code execution pricing](/docs/en/agents-and-tools/tool-use/code-execution-tool#usage-and-pricing) for details. + + +Token counting for programmatic tool calls: Tool results from programmatic invocations do not count toward your input/output token usage. Only the final code execution result and Claude's response count. + + +## Best practices + +### Tool design + +- **Provide detailed output descriptions**: Since Claude deserializes tool results in code, clearly document the format (JSON structure, field types, etc.) +- **Return structured data**: JSON or other easily parseable formats work best for programmatic processing +- **Keep responses concise**: Return only necessary data to minimize processing overhead + +### When to use programmatic calling + +**Good use cases:** + +- Processing large datasets where you only need aggregates or summaries +- Multi-step workflows with 3+ dependent tool calls +- Operations requiring filtering, sorting, or transformation of tool results +- Tasks where intermediate data shouldn't influence Claude's reasoning +- Parallel operations across many items (e.g., checking 50 endpoints) + +**Less ideal use cases:** + +- Single tool calls with simple responses +- Tools that need immediate user feedback +- Very fast operations where code execution overhead would outweigh the benefit + +### Performance optimization + +- **Reuse containers** when making multiple related requests to maintain state +- **Batch similar operations** in a single code execution when possible + +## Troubleshooting + +### Common issues + +**"Tool not allowed" error** + +- Verify your tool definition includes `"allowed_callers": ["code_execution_20250825"]` +- Check that you're using the correct beta headers + +**Container expiration** + +- Ensure you respond to tool calls within the container's lifetime (~4.5 minutes) +- Monitor the `expires_at` field in responses +- Consider implementing faster tool execution + +**Beta header issues** + +- You need the header: `"advanced-tool-use-2025-11-20"` + +**Tool result not parsed correctly** + +- Ensure your tool returns string data that Claude can deserialize +- Provide clear output format documentation in your tool description + +### Debugging tips + +1. **Log all tool calls and results** to track the flow +2. **Check the `caller` field** to confirm programmatic invocation +3. **Monitor container IDs** to ensure proper reuse +4. **Test tools independently** before enabling programmatic calling + +## Why programmatic tool calling works + +Claude's training includes extensive exposure to code, making it effective at reasoning through and chaining function calls. When tools are presented as callable functions within a code execution environment, Claude can leverage this strength to: + +- **Reason naturally about tool composition**: Chain operations and handle dependencies as naturally as writing any Python code +- **Process large results efficiently**: Filter down large tool outputs, extract only relevant data, or write intermediate results to files before returning summaries to the context window +- **Reduce latency significantly**: Eliminate the overhead of re-sampling Claude between each tool call in multi-step workflows + +This approach enables workflows that would be impractical with traditional tool use—such as processing files over 1M tokens—by allowing Claude to work with data programmatically rather than loading everything into the conversation context. + +## Alternative implementations + +Programmatic tool calling is a generalizable pattern that can be implemented outside of Anthropic's managed code execution. Here's an overview of the approaches: + +### Client-side direct execution + +Provide Claude with a code execution tool and describe what functions are available in that environment. When Claude invokes the tool with code, your application executes it locally where those functions are defined. + +**Advantages:** + +- Simple to implement with minimal re-architecting +- Full control over the environment and instructions + +**Disadvantages:** + +- Executes untrusted code outside of a sandbox +- Tool invocations can be vectors for code injection + +**Use when:** Your application can safely execute arbitrary code, you want a simple solution, and Anthropic's managed offering doesn't fit your needs. + +### Self-managed sandboxed execution + +Same approach from Claude's perspective, but code runs in a sandboxed container with security restrictions (e.g., no network egress). If your tools require external resources, you'll need a protocol for executing tool calls outside the sandbox. + +**Advantages:** + +- Safe programmatic tool calling on your own infrastructure +- Full control over the execution environment + +**Disadvantages:** + +- Complex to build and maintain +- Requires managing both infrastructure and inter-process communication + +**Use when:** Security is critical and Anthropic's managed solution doesn't fit your requirements. + +### Anthropic-managed execution + +Anthropic's programmatic tool calling is a managed version of sandboxed execution with an opinionated Python environment tuned for Claude. Anthropic handles container management, code execution, and secure tool invocation communication. + +**Advantages:** + +- Safe and secure by default +- Easy to enable with minimal configuration +- Environment and instructions optimized for Claude + +We recommend using Anthropic's managed solution if you're using the Claude API. + +## Related features + + + + Learn about the underlying code execution capability that powers programmatic tool calling. + + + Understand the fundamentals of tool use with Claude. + + + Step-by-step guide for implementing tools. + + + Optimize your tool implementations for better performance. + + From ebf74a39adc130cefa62ce9e0d97dd6e2d75cd48 Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Mon, 8 Dec 2025 10:40:05 -0500 Subject: [PATCH 3/5] cs --- .changeset/tall-scissors-help.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tall-scissors-help.md diff --git a/.changeset/tall-scissors-help.md b/.changeset/tall-scissors-help.md new file mode 100644 index 000000000000..3d9c108c5535 --- /dev/null +++ b/.changeset/tall-scissors-help.md @@ -0,0 +1,6 @@ +--- +'@ai-sdk/anthropic': patch +'ai': patch +--- + +feat(anthropic): add programmatic tool calling From 83fe03fa5b4ece8ca29a1ee5cb08e9ffbffdfcf6 Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Mon, 8 Dec 2025 10:49:31 -0500 Subject: [PATCH 4/5] udpate toolProvOptions --- packages/anthropic/src/anthropic-messages-options.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/anthropic/src/anthropic-messages-options.ts b/packages/anthropic/src/anthropic-messages-options.ts index 3646ae05f134..7437a1abb29a 100644 --- a/packages/anthropic/src/anthropic-messages-options.ts +++ b/packages/anthropic/src/anthropic-messages-options.ts @@ -67,6 +67,11 @@ export const anthropicToolProviderOptions = z.object({ allowedCallers: z .array(z.enum(['direct', 'code_execution_20250825'])) .optional(), + /** + * Whether to defer loading of this tool's definition. + * When true, the tool definition will be loaded lazily. + */ + deferLoading: z.boolean().optional(), }); export type AnthropicToolProviderOptions = z.infer< From 2a9488a72bd50b9798407797f9c702577c7d5c73 Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Mon, 8 Dec 2025 10:51:07 -0500 Subject: [PATCH 5/5] test --- ...ropic-messages-language-model.test.ts.snap | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap b/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap index 0c4b604ae910..fbd9bb3ba1a0 100644 --- a/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap +++ b/packages/anthropic/src/__snapshots__/anthropic-messages-language-model.test.ts.snap @@ -712,6 +712,13 @@ exports[`AnthropicMessagesLanguageModel > doGenerate > tool search tool > bm25 v }, { "input": "{"location":"San Francisco, CA"}", + "providerMetadata": { + "anthropic": { + "caller": { + "type": "direct", + }, + }, + }, "toolCallId": "toolu_01PFQG18bVLaXcEaCxTZtX4G", "toolName": "get_weather", "type": "tool-call", @@ -745,6 +752,13 @@ exports[`AnthropicMessagesLanguageModel > doGenerate > tool search tool > regex }, { "input": "{"location":"San Francisco, CA","unit":"fahrenheit"}", + "providerMetadata": { + "anthropic": { + "caller": { + "type": "direct", + }, + }, + }, "toolCallId": "toolu_01X4r989CAhzqnFqDJn1gVvp", "toolName": "get_temp_data", "type": "tool-call", @@ -12138,6 +12152,13 @@ exports[`AnthropicMessagesLanguageModel > doStream > tool search tool > bm25 var { "input": "{"location": "San Francisco, CA"}", "providerExecuted": undefined, + "providerMetadata": { + "anthropic": { + "caller": { + "type": "direct", + }, + }, + }, "toolCallId": "toolu_019nRrfqqXcU5NPTUSYfEMAY", "toolName": "get_weather", "type": "tool-call", @@ -12416,6 +12437,13 @@ exports[`AnthropicMessagesLanguageModel > doStream > tool search tool > regex va { "input": "{"location": "San Francisco, CA"}", "providerExecuted": undefined, + "providerMetadata": { + "anthropic": { + "caller": { + "type": "direct", + }, + }, + }, "toolCallId": "toolu_01UmPwkecewaEpMupy2ywk8b", "toolName": "get_temp_data", "type": "tool-call",