UNPKG

@ai-sdk/amazon-bedrock

Version:
384 lines (351 loc) • 11.8 kB
import { UnsupportedFunctionalityError, type JSONObject, type JSONSchema7Definition, type LanguageModelV4CallOptions, type SharedV4Warning, } from '@ai-sdk/provider'; import { asSchema } from '@ai-sdk/provider-utils'; import { anthropicTools, prepareTools as prepareAnthropicTools, } from '@ai-sdk/anthropic/internal'; import { isAnthropicModel as detectAnthropicModel, supportsStrictTools, } from './amazon-bedrock-anthropic-model-support'; import type { AmazonBedrockTool, AmazonBedrockToolConfiguration, } from './amazon-bedrock-api-types'; import type { AmazonBedrockChatModelSettings } from './amazon-bedrock-chat-language-model-options'; export async function prepareTools({ tools, toolChoice, modelId, modelFamily, reasoningBudgetTokens, disableParallelToolUse, rejectsForcedToolUse = false, }: { tools: LanguageModelV4CallOptions['tools']; toolChoice?: LanguageModelV4CallOptions['toolChoice']; modelId: string; modelFamily?: AmazonBedrockChatModelSettings['modelFamily']; reasoningBudgetTokens?: number; disableParallelToolUse?: boolean; rejectsForcedToolUse?: boolean; }): Promise<{ toolConfig: AmazonBedrockToolConfiguration; additionalTools: Record<string, unknown> | undefined; betas: Set<string>; toolWarnings: SharedV4Warning[]; }> { const toolWarnings: SharedV4Warning[] = []; const betas = new Set<string>(); if (tools == null || tools.length === 0) { return { toolConfig: {}, additionalTools: undefined, betas, toolWarnings, }; } // Filter out Anthropic web tools that Amazon Bedrock does not support. const supportedTools = tools.filter(tool => { if ( tool.type === 'provider' && (tool.id === 'anthropic.web_search_20250305' || tool.id === 'anthropic.web_search_20260318' || tool.id === 'anthropic.web_fetch_20260318') ) { toolWarnings.push({ type: 'unsupported', feature: `${tool.id.slice('anthropic.'.length)} tool`, details: `The ${tool.id.slice('anthropic.'.length)} tool is not supported on Amazon Bedrock.`, }); return false; // Exclude this tool } return true; // Include all other tools }); if (supportedTools.length === 0) { return { toolConfig: {}, additionalTools: undefined, betas, toolWarnings, }; } const isAnthropicModel = detectAnthropicModel({ modelId, modelFamily, reasoningBudgetTokens, }); const providerTools = supportedTools.filter(t => t.type === 'provider'); const functionTools = supportedTools.filter(t => t.type === 'function'); const rejectsForcedToolChoice = isAnthropicModel && rejectsForcedToolUse && (toolChoice?.type === 'required' || toolChoice?.type === 'tool'); const providerToolsForRequest = rejectsForcedToolChoice && toolChoice.type === 'tool' ? providerTools.filter(tool => tool.name === toolChoice.toolName) : providerTools; // A named tool choice keeps every function tool for these models (see // below), so parallel tool use is disabled by default to keep the response // to the named tool. `disableParallelToolUse: false` opts out. const disableParallelToolUseForRequest = disableParallelToolUse ?? (isAnthropicModel && toolChoice?.type === 'tool' && !rejectsForcedToolChoice ? true : undefined); let additionalTools: Record<string, unknown> | undefined = undefined; const amazonBedrockTools: AmazonBedrockTool[] = []; const usingAnthropicTools = isAnthropicModel && providerToolsForRequest.length > 0; // Handle Anthropic provider-defined tools for Anthropic models on Bedrock if (usingAnthropicTools) { const { toolChoice: preparedAnthropicToolChoice, toolWarnings: anthropicToolWarnings, betas: anthropicBetas, } = await prepareAnthropicTools({ tools: providerTools, toolChoice, disableParallelToolUse: disableParallelToolUseForRequest, supportsStructuredOutput: false, supportsStrictTools: false, rejectsForcedToolUse, }); toolWarnings.push(...anthropicToolWarnings); anthropicBetas.forEach(beta => betas.add(beta)); // For Anthropic tools on Bedrock, only the 'tool_choice' goes into additional fields. // The tool definitions themselves are sent in the standard 'toolConfig'. if (preparedAnthropicToolChoice) { additionalTools = { tool_choice: preparedAnthropicToolChoice, }; } // Create a standard Bedrock tool representation for validation purposes for (const tool of providerToolsForRequest) { const toolFactory = Object.values(anthropicTools).find(factory => { const instance = (factory as (args: any) => any)({}); return instance.id === tool.id; }); if (toolFactory != null) { const fullToolDefinition = (toolFactory as (args: any) => any)({}); amazonBedrockTools.push({ toolSpec: { name: tool.name, inputSchema: { json: (await asSchema(fullToolDefinition.inputSchema) .jsonSchema) as JSONObject, }, }, }); } else { toolWarnings.push({ type: 'unsupported', feature: `tool ${tool.id}` }); } } } else { // Report unsupported provider-defined tools for non-anthropic models for (const tool of providerToolsForRequest) { toolWarnings.push({ type: 'unsupported', feature: `tool ${tool.id}` }); } } let preparedToolChoice = toolChoice; if (!usingAnthropicTools && rejectsForcedToolChoice) { if (toolChoice.type === 'tool') { toolWarnings.push({ type: 'unsupported', feature: 'toolChoice', details: `toolChoice 'tool' is not supported by this model because it rejects forced tool use. ` + `Only the '${toolChoice.toolName}' tool is sent with 'auto' tool choice. ` + `Instruct the model to use the tool in the prompt and verify that a tool call was made.`, }); } else { toolWarnings.push({ type: 'unsupported', feature: 'toolChoice', details: `toolChoice 'required' is not supported by this model because it rejects forced tool use. ` + `Using 'auto' instead. Instruct the model to use a tool in the prompt and verify that a tool call was made.`, }); } preparedToolChoice = { type: 'auto' }; } // Anthropic models that accept forced tool use guarantee the named tool is // called, so they keep every function tool. Other models only receive the // named tool so they cannot call a different one. const filteredFunctionTools = toolChoice?.type === 'tool' && (!isAnthropicModel || rejectsForcedToolChoice) ? functionTools.filter(t => t.name === toolChoice.toolName) : functionTools; const supportsStrictOnTools = supportsStrictTools(modelId); for (const tool of filteredFunctionTools) { const supportsStrictForTool = supportsStrictOnTools && (tool.strict !== true || isStrictToolSchemaCompatible(tool.inputSchema)); if (!supportsStrictOnTools && tool.strict === true) { toolWarnings.push({ type: 'unsupported', feature: 'strict', details: `Tool '${tool.name}' has strict: ${tool.strict}, but strict mode is not supported by this model on Amazon Bedrock. The strict property will be ignored.`, }); } else if (tool.strict === true && !supportsStrictForTool) { toolWarnings.push({ type: 'unsupported', feature: 'strict', details: `Tool '${tool.name}' has strict: true, but Amazon Bedrock requires every object in a strict tool schema to set additionalProperties: false. The strict property will be ignored.`, }); } amazonBedrockTools.push({ toolSpec: { name: tool.name, ...(tool.description?.trim() !== '' ? { description: tool.description } : {}), ...(tool.strict != null && supportsStrictForTool ? { strict: tool.strict } : {}), inputSchema: { json: tool.inputSchema as JSONObject, }, }, }); } if ( isAnthropicModel && !usingAnthropicTools && disableParallelToolUseForRequest && amazonBedrockTools.length > 0 && preparedToolChoice?.type !== 'none' ) { additionalTools = { tool_choice: preparedToolChoice?.type === 'required' ? { type: 'any', disable_parallel_tool_use: true } : preparedToolChoice?.type === 'tool' ? { type: 'tool', name: preparedToolChoice.toolName, disable_parallel_tool_use: true, } : { type: 'auto', disable_parallel_tool_use: true }, }; } // Handle toolChoice for standard Bedrock tools, but NOT for Anthropic provider-defined tools let amazonBedrockToolChoice: AmazonBedrockToolConfiguration['toolChoice'] = undefined; if ( !usingAnthropicTools && additionalTools?.tool_choice == null && amazonBedrockTools.length > 0 && preparedToolChoice ) { const type = preparedToolChoice.type; switch (type) { case 'auto': amazonBedrockToolChoice = { auto: {} }; break; case 'required': amazonBedrockToolChoice = { any: {} }; break; case 'none': amazonBedrockTools.length = 0; amazonBedrockToolChoice = undefined; break; case 'tool': amazonBedrockToolChoice = { tool: { name: preparedToolChoice.toolName }, }; break; default: { const _exhaustiveCheck: never = type; throw new UnsupportedFunctionalityError({ functionality: `tool choice type: ${_exhaustiveCheck}`, }); } } } const toolConfig: AmazonBedrockToolConfiguration = amazonBedrockTools.length > 0 ? { tools: amazonBedrockTools, toolChoice: amazonBedrockToolChoice } : {}; return { toolConfig, additionalTools, betas, toolWarnings, }; } function isStrictToolSchemaCompatible(schema: JSONSchema7Definition): boolean { if (typeof schema === 'boolean') { return true; } const schemaWithDefs = schema as typeof schema & { $defs?: Record<string, JSONSchema7Definition>; }; if ( (schema.type === 'object' || (Array.isArray(schema.type) && schema.type.includes('object'))) && schema.additionalProperties !== false ) { return false; } const schemaMaps = [ schema.properties, schema.patternProperties, schema.definitions, schemaWithDefs.$defs, ]; for (const schemaMap of schemaMaps) { if ( schemaMap != null && Object.values(schemaMap).some( definition => !isStrictToolSchemaCompatible(definition), ) ) { return false; } } if ( schema.dependencies != null && Object.values(schema.dependencies).some( dependency => !Array.isArray(dependency) && !isStrictToolSchemaCompatible(dependency), ) ) { return false; } const nestedSchemas = [ schema.propertyNames, schema.contains, schema.not, schema.if, schema.then, schema.else, ]; if ( nestedSchemas.some( nestedSchema => nestedSchema != null && !isStrictToolSchemaCompatible(nestedSchema), ) ) { return false; } if ( schema.items != null && (Array.isArray(schema.items) ? schema.items.some(item => !isStrictToolSchemaCompatible(item)) : !isStrictToolSchemaCompatible(schema.items)) ) { return false; } return [schema.anyOf, schema.allOf, schema.oneOf].every( alternatives => alternatives == null || alternatives.every(isStrictToolSchemaCompatible), ); }