Skip to content
Open
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
91 changes: 91 additions & 0 deletions packages/anthropic/src/advanced-tool-use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { SharedV3ProviderMetadata, SharedV3Warning } from '@ai-sdk/provider';
import { validateTypes } from '@ai-sdk/provider-utils';
import {
AnthropicAdvancedToolUse,
anthropicInputExamplesSchema,
anthropicProgrammaticToolCallingSchema,
AnthropicTool,
anthropicToolSearchSchema,
} from './anthropic-messages-api';

/**
* Extracts and validates Anthropic advanced tool use configuration from provider metadata.
*
* This function processes provider metadata to extract tool use configuration options including
* deferred loading, allowed callers, and input examples. It supports both camelCase and snake_case
* property naming conventions for backwards compatibility.
*
* @param providerMetadata - Optional shared provider metadata containing Anthropic-specific configuration
* @returns A promise that resolves to an object containing validated advanced tool use settings:
* - `deferLoading`: Validated tool search/defer loading configuration
* - `allowedCallers`: Validated programmatic tool calling configuration
* - `inputExamples`: Validated input examples configuration
* @throws Will throw an error if any of the extracted configurations fail validation
*/

export async function getAnthropicAdvancedToolUseFeaturesSupport(
providerMetadata: SharedV3ProviderMetadata | undefined,
): Promise<AnthropicAdvancedToolUse> {
const anthropic = providerMetadata?.anthropic;
const deferLoading = anthropic?.defer_loading ?? anthropic?.deferLoading;
const inputExamples = anthropic?.input_examples ?? anthropic?.inputExamples;
const allowed_callers =
anthropic?.allowed_callers ?? anthropic?.allowedCallers;

const [parseDeferLoading, parseAllowedCallers, parseInputExamples] =
await Promise.all([
validateTypes({
value: deferLoading,
schema: anthropicToolSearchSchema,
}),
validateTypes({
value: allowed_callers,
schema: anthropicProgrammaticToolCallingSchema,
}),
validateTypes({
value: inputExamples,
schema: anthropicInputExamplesSchema,
}),
]);

let result: AnthropicAdvancedToolUse = {};
if (parseDeferLoading !== undefined) {
result.defer_loading = parseDeferLoading;
}

if (parseAllowedCallers !== undefined) {
result.allowed_callers = parseAllowedCallers;
}

if (parseInputExamples !== undefined) {
result.input_examples = parseInputExamples;
}

return result;
}

export const handleAnthropicAdvancedToolUseFeaturesWarnings = (
anthropicTools: AnthropicTool[],
) => {
const toolWarnings: SharedV3Warning[] = [];

// Check if any tool uses defer_loading
const anyToolUsesDeferLoading = anthropicTools.some(
t => 'defer_loading' in t && t.defer_loading === true,
);

const searchTool = anthropicTools.find(
t =>
t.name === 'tool_search_tool_bm25' || t.name === 'tool_search_tool_regex',
);

if (anyToolUsesDeferLoading && !searchTool) {
toolWarnings.push({
type: 'unsupported',
feature: `tool`,
details: `At least one tool has defer_loading set to true, but no tool search tool (tool_search_tool_bm25 or tool_search_tool_regex) is provided. A tool search tool is required when using deferred loading.`,
});
}

return toolWarnings;
};
31 changes: 28 additions & 3 deletions packages/anthropic/src/anthropic-messages-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,12 @@ export interface AnthropicMcpToolResultContent {
}

export type AnthropicTool =
| {
| ({
name: string;
description: string | undefined;
input_schema: JSONSchema7;
cache_control: AnthropicCacheControl | undefined;
strict?: boolean;
}
} & AnthropicAdvancedToolUse)
| {
type: 'code_execution_20250522';
name: string;
Expand Down Expand Up @@ -344,6 +343,14 @@ export type AnthropicTool =
timezone?: string;
};
cache_control: AnthropicCacheControl | undefined;
}
| {
type: 'tool_search_tool_regex_20251119';
name: string;
}
| {
type: 'tool_search_tool_bm25_20251119';
name: string;
};

export type AnthropicToolChoice =
Expand Down Expand Up @@ -886,3 +893,21 @@ export type Citation = NonNullable<
type: 'text';
})['citations']
>[number];

export const anthropicToolSearchSchema = lazySchema(() =>
zodSchema(z.boolean().optional()),
);

export const anthropicProgrammaticToolCallingSchema = lazySchema(() =>
zodSchema(z.array(z.enum(['code_execution_20250825', 'direct'])).optional()),
);

export const anthropicInputExamplesSchema = lazySchema(() =>
zodSchema(z.array(z.record(z.string(), z.unknown())).optional()),
);

export type AnthropicAdvancedToolUse = {
defer_loading?: boolean;
allowed_callers?: Array<'code_execution_20250825' | 'direct'>;
input_examples?: Array<Record<string, unknown>>;
};
42 changes: 42 additions & 0 deletions packages/anthropic/src/anthropic-prepare-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,4 +627,46 @@
]
`);
});

describe('anthropic advanced tools', () => {
it('should provide search tool when deferLoading is true', async () => {
const result = await prepareTools({

Check failure on line 633 in packages/anthropic/src/anthropic-prepare-tools.test.ts

View workflow job for this annotation

GitHub Actions / TypeScript

Argument of type '{ tools: { type: "function"; name: string; description: string; inputSchema: {}; providerOptions: { anthropic: { deferLoading: true; }; }; }[]; toolChoice: undefined; }' is not assignable to parameter of type '{ tools: (LanguageModelV3FunctionTool | LanguageModelV3ProviderTool)[] | undefined; toolChoice: LanguageModelV3ToolChoice | undefined; disableParallelToolUse?: boolean | undefined; cacheControlValidator?: CacheControlValidator | undefined; supportsStructuredOutput: boolean; }'.
tools: [
{
type: 'function',
name: 'testFunction',
description: 'A test function',
inputSchema: {},
providerOptions: {
anthropic: { deferLoading: true },
},
},
],
toolChoice: undefined,
});

expect(result).toMatchInlineSnapshot(`
{
"betas": Set {},
"toolChoice": undefined,
"toolWarnings": [
{
"details": "At least one tool has defer_loading set to true, but no tool search tool (tool_search_tool_bm25 or tool_search_tool_regex) is provided. A tool search tool is required when using deferred loading.",
"feature": "tool",
"type": "unsupported",
},
],
"tools": [
{
"cache_control": undefined,
"defer_loading": true,
"description": "A test function",
"input_schema": {},
"name": "testFunction",
},
],
}
`);
});
});
});
36 changes: 33 additions & 3 deletions packages/anthropic/src/anthropic-prepare-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ 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';
import {
getAnthropicAdvancedToolUseFeaturesSupport,
handleAnthropicAdvancedToolUseFeaturesWarnings,
} from './advanced-tool-use';

export async function prepareTools({
tools,
Expand Down Expand Up @@ -53,14 +57,17 @@ export async function prepareTools({
canCache: true,
});

const advancedToolFeatures =
Copy link
Contributor

Choose a reason for hiding this comment

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

The strict property from function tools is no longer being passed through to the Anthropic tool definition. This breaks the structured output feature when strict is set on a tool.

View Details
📝 Patch Details
diff --git a/packages/anthropic/src/anthropic-prepare-tools.ts b/packages/anthropic/src/anthropic-prepare-tools.ts
index ddf852bef..d5c8a47f4 100644
--- a/packages/anthropic/src/anthropic-prepare-tools.ts
+++ b/packages/anthropic/src/anthropic-prepare-tools.ts
@@ -68,6 +68,9 @@ export async function prepareTools({
           input_schema: tool.inputSchema,
           cache_control: cacheControl,
           ...advancedToolFeatures,
+          ...(supportsStructuredOutput === true && tool.strict != null
+            ? { strict: tool.strict }
+            : {}),
         });
         break;
       }

Analysis

Missing strict property passthrough in Anthropic tool preparation

What fails: The prepareTools() function in packages/anthropic/src/anthropic-prepare-tools.ts does not pass through the strict property from function tools to the Anthropic tool definition, breaking structured output feature when strict is set.

How to reproduce:

cd packages/anthropic
pnpm test:node -- src/anthropic-prepare-tools.test.ts

The test "should include strict when supportsStructuredOutput is true and strict is true" would fail with:

- Expected: "strict": true
+ Received: (property missing)

What was happening: When refactoring to use getAnthropicAdvancedToolUseFeaturesSupport(), the handling of the strict property was removed. The function only extracted defer_loading, allowed_callers, and input_examples from advancedToolFeatures, but never checked tool.strict.

What should happen: When supportsStructuredOutput is true AND tool.strict is defined, the strict property should be included in the output tool definition, matching the pattern used by other providers (Deepseek, Mistral, OpenAI).

Fix applied: Added conditional spread to include strict property:

...(supportsStructuredOutput === true && tool.strict != null
  ? { strict: tool.strict }
  : {}),

This matches the established pattern across the codebase and ensures structured output mode works correctly with Anthropic models.

Verification: All 177 tests in the anthropic package now pass, including the 3 strict mode tests in anthropic-prepare-tools.test.ts.

await getAnthropicAdvancedToolUseFeaturesSupport(
tool.providerOptions,
);

anthropicTools.push({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
cache_control: cacheControl,
...(supportsStructuredOutput === true && tool.strict != null
? { strict: tool.strict }
: {}),
...advancedToolFeatures,
Copy link
Contributor

@AVtheking AVtheking Dec 4, 2025

Choose a reason for hiding this comment

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

wouldn't this make all tool follow same defer_loading property ? if we gonna go this way then how could we have mix of tools with some being defered and some (could be most used or critical ones) not defered ?

Copy link
Author

Choose a reason for hiding this comment

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

no since advancedToolFeatures returns only the properties specified by the user , as example if defer_loading is not specified, so it won't be there.

Copy link
Contributor

Choose a reason for hiding this comment

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

This would still be specified in provider options right, and that make it applied to all tools then. I mean how could you have mix of tools with some having defer_loading set to true and some with false

Copy link
Contributor

Choose a reason for hiding this comment

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

Oops sorry, providerMetadata is tool property, I missed that, this works then

});
break;
}
Expand Down Expand Up @@ -212,6 +219,22 @@ export async function prepareTools({
break;
}

case 'anthropic.tool_search_tool_regex_20251119': {
anthropicTools.push({
type: 'tool_search_tool_regex_20251119',
name: 'tool_search_tool_regex',
});
break;
}

case 'anthropic.tool_search_tool_bm25_20251119': {
anthropicTools.push({
type: 'tool_search_tool_bm25_20251119',
name: 'tool_search_tool_bm25',
});
break;
}

default: {
toolWarnings.push({
type: 'unsupported',
Expand All @@ -233,6 +256,13 @@ export async function prepareTools({
}
}

const advancedToolFeaturesWarnings =
handleAnthropicAdvancedToolUseFeaturesWarnings(anthropicTools);

if (advancedToolFeaturesWarnings.length > 0) {
toolWarnings.push(...advancedToolFeaturesWarnings);
}

if (toolChoice == null) {
return {
tools: anthropicTools,
Expand Down
14 changes: 14 additions & 0 deletions packages/anthropic/src/anthropic-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { textEditor_20241022 } from './tool/text-editor_20241022';
import { textEditor_20250124 } from './tool/text-editor_20250124';
import { textEditor_20250429 } from './tool/text-editor_20250429';
import { textEditor_20250728 } from './tool/text-editor_20250728';
import { toolSearchToolBm25_20251119 } from './tool/tool_search_tool_bm25_20251119';
import { toolSearchToolRegex_20251119 } from './tool/tool_search_tool_regex_20251119';
import { webFetch_20250910 } from './tool/web-fetch-20250910';
import { webSearch_20250305 } from './tool/web-search_20250305';

Expand Down Expand Up @@ -173,4 +175,16 @@ export const anthropicTools = {
* @param userLocation - Optional user location information to provide geographically relevant search results.
*/
webSearch_20250305,
/**
* Claude constructs regex patterns to search for tools
*
* Tool name must be `tool_search_tool_regex`.
*/
toolSearchToolRegex_20251119,
/**
* Claude uses natural language queries to search for tools
*
* Tool name must be `tool_search_tool_bm25`.
*/
toolSearchToolBm25_20251119,
};
15 changes: 15 additions & 0 deletions packages/anthropic/src/tool/tool_search_tool_bm25_20251119.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
createProviderToolFactory,
lazySchema,
zodSchema,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';

const toolSearchToolBm25_20251119ArgsSchema = lazySchema(() =>
zodSchema(z.object({})),
);

export const toolSearchToolBm25_20251119 = createProviderToolFactory<{}, {}>({
id: 'anthropic.tool_search_tool_bm25_20251119',
inputSchema: toolSearchToolBm25_20251119ArgsSchema,
});
15 changes: 15 additions & 0 deletions packages/anthropic/src/tool/tool_search_tool_regex_20251119.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
createProviderToolFactory,
lazySchema,
zodSchema,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';

const toolSearchToolRegex_20251119ArgsSchema = lazySchema(() =>
zodSchema(z.object({})),
);

export const toolSearchToolRegex_20251119 = createProviderToolFactory<{}, {}>({
id: 'anthropic.tool_search_tool_regex_20251119',
inputSchema: toolSearchToolRegex_20251119ArgsSchema,
});
Loading