Skip to content

Commit 0893170

Browse files
authored
fix(amazon-bedrock): handle empty activeTools with tool conversation history (#7600)
## background Users integrating Amazon Bedrock with multi-step agents hit validation errors when setting `activeTools: []` or `toolChoice: 'none'` in conversations that previously used tools. Bedrock requires toolConfig to be present when conversation contains tool content, but rejects empty tools arrays. ## summary - add placeholder tool when activeTools is empty but conversation has tool content - handle both `activeTools: []` and `toolChoice: 'none'` scenarios - include helpful warning about workaround ## tasks - [x] placeholder tool logic in bedrock-prepare-tools.ts - [x] updated test expectations for both scenarios - [x] warning messages for user awareness ## future work * remove workaround if Amazon Bedrock API supports empty tools with conversation history related issue #7528
1 parent bcca3f8 commit 0893170

File tree

5 files changed

+148
-21
lines changed

5 files changed

+148
-21
lines changed

.changeset/khaki-walls-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/amazon-bedrock': patch
3+
---
4+
5+
fix(amazon-bedrock): handle empty activeTools with tool conversation history
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { bedrock } from '@ai-sdk/amazon-bedrock';
2+
import { streamText, tool, stepCountIs } from 'ai';
3+
import { z } from 'zod';
4+
import 'dotenv/config';
5+
6+
async function main() {
7+
const result = streamText({
8+
model: bedrock('anthropic.claude-3-5-sonnet-20241022-v2:0'),
9+
tools: {
10+
weather: tool({
11+
description: 'Get the weather in a location',
12+
inputSchema: z.object({ city: z.string() }),
13+
execute: async ({ city }) => ({
14+
result: `The weather in ${city} is 20°C.`,
15+
}),
16+
}),
17+
},
18+
stopWhen: [stepCountIs(5)],
19+
prepareStep: ({ stepNumber }) => {
20+
if (stepNumber > 0) {
21+
console.log(`Setting activeTools: [] for step ${stepNumber}`);
22+
return {
23+
activeTools: [],
24+
};
25+
}
26+
return undefined;
27+
},
28+
toolChoice: 'auto',
29+
prompt: 'What is the weather in Toronto, Calgary, and Vancouver?',
30+
});
31+
32+
for await (const part of result.fullStream) {
33+
switch (part.type) {
34+
case 'start-step':
35+
console.log('Step started');
36+
break;
37+
case 'tool-call':
38+
console.log(
39+
`Tool call: ${part.toolName}(${JSON.stringify(part.input)})`,
40+
);
41+
break;
42+
case 'tool-result':
43+
console.log(`Tool result: ${JSON.stringify(part.output)}`);
44+
break;
45+
case 'text-delta':
46+
process.stdout.write(part.text);
47+
break;
48+
case 'finish-step':
49+
console.log('Step finished');
50+
break;
51+
case 'finish':
52+
console.log('Stream finished');
53+
break;
54+
}
55+
}
56+
57+
console.log();
58+
console.log('Usage:', await result.usage);
59+
}
60+
61+
main().catch(console.error);

packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,7 +2091,7 @@ describe('doGenerate', () => {
20912091
`);
20922092
});
20932093

2094-
it('should include toolConfig when conversation has tool calls but no active tools', async () => {
2094+
it('should omit toolConfig and filter tool content when conversation has tool calls but no active tools', async () => {
20952095
prepareJsonResponse({});
20962096

20972097
const conversationWithToolCalls: LanguageModelV2Prompt = [
@@ -2130,17 +2130,39 @@ describe('doGenerate', () => {
21302130
},
21312131
];
21322132

2133-
await model.doGenerate({
2133+
const result = await model.doGenerate({
21342134
prompt: conversationWithToolCalls,
21352135
tools: [],
21362136
});
21372137

21382138
const requestBody = await server.calls[0].requestBodyJson;
21392139

2140-
expect(requestBody.toolConfig).toMatchInlineSnapshot(`
2141-
{
2142-
"tools": [],
2143-
}
2140+
expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`);
2141+
2142+
expect(requestBody.messages).toMatchInlineSnapshot(`
2143+
[
2144+
{
2145+
"content": [
2146+
{
2147+
"text": "What is the weather in Toronto?",
2148+
},
2149+
{
2150+
"text": "Now give me a summary.",
2151+
},
2152+
],
2153+
"role": "user",
2154+
},
2155+
]
2156+
`);
2157+
2158+
expect(result.warnings).toMatchInlineSnapshot(`
2159+
[
2160+
{
2161+
"details": "Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.",
2162+
"setting": "toolContent",
2163+
"type": "unsupported-setting",
2164+
},
2165+
]
21442166
`);
21452167
});
21462168

@@ -2256,7 +2278,7 @@ describe('doGenerate', () => {
22562278
]);
22572279
});
22582280

2259-
it('should include toolConfig when conversation has tool calls but toolChoice is none', async () => {
2281+
it('should omit toolConfig when conversation has tool calls but toolChoice is none', async () => {
22602282
prepareJsonResponse({});
22612283

22622284
const conversationWithToolCalls: LanguageModelV2Prompt = [
@@ -2315,10 +2337,6 @@ describe('doGenerate', () => {
23152337

23162338
const requestBody = await server.calls[0].requestBodyJson;
23172339

2318-
expect(requestBody.toolConfig).toMatchInlineSnapshot(`
2319-
{
2320-
"tools": [],
2321-
}
2322-
`);
2340+
expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`);
23232341
});
23242342
});

packages/amazon-bedrock/src/bedrock-chat-language-model.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,6 @@ export class BedrockChatLanguageModel implements LanguageModelV2 {
143143
}
144144
: undefined;
145145

146-
const { system, messages } = await convertToBedrockChatMessages(prompt);
147-
148146
const isThinking = bedrockOptions.reasoningConfig?.type === 'enabled';
149147
const thinkingBudget = bedrockOptions.reasoningConfig?.budgetTokens;
150148

@@ -193,13 +191,57 @@ export class BedrockChatLanguageModel implements LanguageModelV2 {
193191
});
194192
}
195193

194+
// Filter tool content from prompt when no tools are available
195+
const activeTools =
196+
jsonResponseTool != null ? [jsonResponseTool] : (tools ?? []);
197+
let filteredPrompt = prompt;
198+
199+
if (activeTools.length === 0) {
200+
const hasToolContent = prompt.some(
201+
message =>
202+
'content' in message &&
203+
Array.isArray(message.content) &&
204+
message.content.some(
205+
part => part.type === 'tool-call' || part.type === 'tool-result',
206+
),
207+
);
208+
209+
if (hasToolContent) {
210+
filteredPrompt = prompt
211+
.map(message =>
212+
message.role === 'system'
213+
? message
214+
: {
215+
...message,
216+
content: message.content.filter(
217+
part =>
218+
part.type !== 'tool-call' && part.type !== 'tool-result',
219+
),
220+
},
221+
)
222+
.filter(
223+
message => message.role === 'system' || message.content.length > 0,
224+
) as typeof prompt;
225+
226+
warnings.push({
227+
type: 'unsupported-setting',
228+
setting: 'toolContent',
229+
details:
230+
'Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.',
231+
});
232+
}
233+
}
234+
235+
const { system, messages } =
236+
await convertToBedrockChatMessages(filteredPrompt);
237+
196238
const { toolConfig, toolWarnings } = prepareTools({
197-
tools: jsonResponseTool != null ? [jsonResponseTool] : (tools ?? []),
239+
tools: activeTools,
198240
toolChoice:
199241
jsonResponseTool != null
200242
? { type: 'tool', toolName: jsonResponseTool.name }
201243
: toolChoice,
202-
prompt,
244+
prompt: filteredPrompt,
203245
});
204246

205247
// Filter out reasoningConfig from providerOptions.bedrock to prevent sending it to Bedrock API

packages/amazon-bedrock/src/bedrock-prepare-tools.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { BedrockTool, BedrockToolConfiguration } from './bedrock-api-types';
99

1010
/**
1111
* Check if the conversation contains any tool calls or tool results.
12-
* Bedrock requires toolConfig to be present when messages contain toolUse or toolResult blocks.
1312
*/
1413
function promptContainsToolContent(prompt: LanguageModelV2Prompt): boolean {
1514
return prompt.some(message => {
@@ -39,9 +38,11 @@ export function prepareTools({
3938
const hasToolContent = promptContainsToolContent(prompt);
4039

4140
if (tools == null) {
41+
// When no tools are provided, completely omit toolConfig from the request.
42+
// This works regardless of conversation history - Bedrock handles it gracefully.
4243
return {
4344
toolConfig: {
44-
tools: hasToolContent ? [] : undefined,
45+
tools: undefined,
4546
toolChoice: undefined,
4647
},
4748
toolWarnings: [],
@@ -88,11 +89,11 @@ export function prepareTools({
8889
toolWarnings,
8990
};
9091
case 'none':
91-
// Bedrock does not support 'none' tool choice, so we remove the tools.
92-
// However, if conversation contains tool content, we need empty tools array for API.
92+
// Bedrock does not support 'none' tool choice, so we omit toolConfig entirely.
93+
// This works regardless of conversation history - Bedrock handles it gracefully.
9394
return {
9495
toolConfig: {
95-
tools: hasToolContent ? [] : undefined,
96+
tools: undefined,
9697
toolChoice: undefined,
9798
},
9899
toolWarnings,

0 commit comments

Comments
 (0)