@ai-sdk/amazon-bedrock
Version:
384 lines (351 loc) • 11.8 kB
text/typescript
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),
);
}