chore(deps): bump the AI SDK and re-port the tool-call tracker patch - #2193
Merged
Conversation
vercel/ai#18333, our report of the sparse-index crash in StreamingToolCallTracker.flush, was fixed in vercel/ai#18382 and shipped in @ai-sdk/provider-utils@5.0.21. Upstream replaced the sparse array with a Set plus id/index lookups, so the patch no longer has to carry that fix, nor the half of the identity collision that distinct ids resolve. It still has to carry the rest. Removing it on 5.0.21 fails 25 of the 53 cases in model-factory-tool-call-index, with real property violations rather than the undecidable-shape cases the guard deliberately asserts fail: a repeated id merges two calls' arguments into one input or drops the second call, blank-but-present ids read as a fresh identity, function.name is never consulted so the Ollama shape this patch exists for cannot be separated, wire ids reach tool-runtime unminted where operationId collides, and latestToolCall absorbs a bare delta while several calls are open. So the patch is re-ported onto upstream's new shape rather than deleted. The rules are unchanged; the diff shrinks from 180 to 159 lines because flush() and the creation path are now upstream's. Guard passes 53/53 on a clean npm ci tree.
…didate Three external reviews of the re-port. One found a real defect; the rest were coverage and documentation gaps at the same seam. The defect: `findByAlias` checked a delta's aliases against the candidate alone, so an alias the candidate had never claimed could not contradict it — even when that alias demonstrably belonged to another call. A delta carrying call A's index and call B's id therefore resolved to A instead of to neither, silently: the fragment landed on a call the gateway never addressed, and A then learned B's id as a late alias. The already-pinned form of this shape throws, and only because both calls happened to carry an id; the variant where the matched call has none was the one that got through. Agreement is now read across the record set, which makes the two forms behave alike, and both orderings are pinned. Also in the guard, none of which changes behaviour: - `assertToolCallIdsUsable` rejects blank ids, not just empty ones. A whitespace id is truthy, so it passes runtime-commit-sink's falsy check and reaches operationId as a key naming nothing. Pinned on the path that emits an id; the existing whitespace cases are continuations, where the wire id is consumed as an alias and never surfaces. - A name-only continuation with several calls open is recorded as a boundary and pinned. It invents a call for the same reason the reused-index shape stays decidable: `name` rejects a match but never proposes one. Both readings cannot hold. - The three cases that assert an undecidable shape fails now say so in the test file, naming the fix that would flip them green. The README claimed seven without listing them, and the count was wrong in both directions — every other failure-asserting case is one any correct implementation should fail. - Dropped a stale claim that upstream resolved a shape "by slot" (5.0.11) and one that the openai path had no other coverage. README: minting is stated as an injectivity requirement of the tracker rather than a consumer-side concern, since the merge it prevents happens above every consumer; the differential-enumeration record is compressed to its conclusion, its 5.0.11 baseline being gone. Deferred: the failure-asserting cases asserting only that a failure occurred, not which delta caused it. Four pass on pristine upstream for unrelated reasons. Real, but fixing it means reshaping those cases, and the boundary comments now carry the classification that mattered. Guard 57/57; runtime suite unchanged at 2 pre-existing local-state failures; lint and format clean.
The `package` lane failed on `check:release`: the Apache license-text override is keyed by exact version, so bumping provider-utils to 5.0.21 left it with no LICENSE file and no override. Re-checked rather than carried over — 5.0.21 declares Apache-2.0 in package.json and still omits the file from its tarball, same as 5.0.11 — and noted why the key is version-exact so the next bump re-checks instead of inheriting. Regenerated notices: the bumped AI SDK versions plus undici@7.29.0, a new transitive production dependency.
Astro-Han
marked this pull request as ready for review
August 5, 2026 05:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bumps the AI SDK (
ai7.0.31→7.0.52,@ai-sdk/provider-utils5.0.11→5.0.21 and the provider packages alongside it) and re-ports theStreamingToolCallTrackerpatch onto upstream's rewritten class.vercel/ai#18333 — our report of the sparse-index crash in
flush()— was fixed in vercel/ai#18382 and shipped in 5.0.21. Upstream replaced the sparse array with aSetplustoolCallsById/toolCallsByIndexlookups, so the patch no longer has to carry that fix, nor the half of the identity collision that distinct ids resolve. The diff shrinks from 180 to 159 lines.It still has to carry the rest. The deletion criterion in
patches/README.mdsays to remove the patch, reinstall, and read the guard by property rather than by count — done, and 25 of the 53 cases fail with real property violations rather than the undecidable-shape cases the guard deliberately asserts fail. Measured on clean 5.0.21:id(all calls atindex: 0, the Ollama shape this patch exists for) throwsExpected 'id' to be a string.and kills the turn.function.namedistinguishes the calls but is never consulted.idon a continuation misses the id lookup, starts a new call, and throwsExpected 'function.name' to be a string.idmerges two calls:{"path":"a"}{"path":"b"}in one input, second call dropped.name: ''mints a call withtoolName: ''; an empty wireidreachestool-runtime.tsverbatim, whereoperationIdcollides or is empty.latestToolCallabsorbs a delta carrying no alias at all while several calls are open.So the patch is re-ported rather than deleted. The rules are unchanged — normalize blank aliases to absent, keep records in creation order with the wire's aliases learned separately from the emitted id, resolve newest-first by claimed alias with a disagreeing
function.namerejecting the match, mint an id when the wire's cannot address a call, and orderflush()by index only when every index is present and unique.The remaining behaviour is reported upstream as vercel/ai#18440;
patches/README.mdlinks it and its lifecycle section now says "partly upstreamed" instead of "not upstreamed, by decision".Refs #1967, #1976.
Verification
rm -rf node_modules && npm ci— patch applies on a clean tree (this is the path that matters;patch-package --reversesilently no-ops here, so the round-trip was verified by reinstalling instead).packages/runtime/src/__tests__/model-factory-tool-call-index.test.ts— 53/53 on the clean-install tree.npm --workspace @maka/runtime run test:dist— 3163 tests, 2 failures, bothdeep-research-toolsfailing onSQLite runtime schema 10 is newer than supported version 9. Pre-existing local-state issue, unrelated to this change: the local database was written by a newer build.npm --workspace @maka/headless run test— 1403 pass, 0 fail.npm run build,npm run lint,npm run format:check— clean.npm run check:release— clean after regenerating third-party notices. This is the one that caught a real gap: the Apache license-text override ingenerate-third-party-notices.mjsis keyed by exact version, so the bump left provider-utils with no LICENSE file and no override, failing thepackagelane. Re-checked rather than carried over (5.0.21 still declares Apache-2.0 and still omits the file), and the regenerated notices pick upundici@7.29.0as a new transitive production dependency.package,e2e_shard,storybook, and the Windows baseline lane.Review focus
The patch body is the whole risk surface.
patches/@ai-sdk+provider-utils+5.0.21.patchis not a rewrite of our previous logic — the rules are byte-for-byte the ones the 2448-run differential enumeration validated atdce22434e— but it is a re-application onto a class upstream restructured, so the question worth checking is whether upstream's new surroundings change what those rules mean. Specifically:processNewToolCallnow returns the record (we ignore the return and letprocessDeltabail early), and upstream'stoolCallsById/toolCallsByIndex/latestToolCallare dropped in favour of the newest-first scan, which is not an equivalent lookup — a delta whoseidandindexaddress different calls must resolve to neither, and the map pair cannot express that.Review round (Pi
deepseek-v4-flash×3, added in 0fa4650)Three independent reviews: patch correctness/minimality, guard quality, and solution layer. No P0 or P1. One real defect, since fixed; the rest were coverage and documentation gaps at the same seam.
Fixed — alias agreement was read per-candidate.
findByAliascompared a delta's aliases against the matched call only, so an alias that call had never claimed could not contradict it even when another call demonstrably owned it. A delta carrying call A's index and call B's id resolved to A rather than to neither, silently. The already-pinned form of this shape throws only because both calls carried an id; the variant where the matched call has none got through. Agreement is now read across the record set. Both orderings pinned; guard 53→57.Also fixed, no behaviour change:
assertToolCallIdsUsablenow rejects blank ids rather than only empty ones, pinned on the path that emits an id; the name-only continuation with several calls open is recorded in the boundary table and pinned; the three cases asserting an undecidable shape fails now carry that classification in the test file, replacing a README count that was wrong in both directions; stale 5.0.11-era comments dropped; minting restated as a tracker-level injectivity requirement rather than a consumer concern.Deferred: the failure-asserting cases assert that a failure occurred, not which delta caused it — four pass on pristine upstream for unrelated reasons. Real, but fixing it means reshaping those cases, and the boundary comments now carry the classification that mattered.
Rejected in part: the whitespace-id finding claimed two colliding whitespace ids would pass the guard. They would not — the distinctness check catches that. The real gap was a single blank id emitted verbatim, which is what the new pin covers.
Confirmed by review, not assumed:
dist/index.jsis still the only seam that can express these rules (both adapters construct the tracker internally with no injection point, and a merge happens before any downstream layer can see two calls); no rule including id minting can move into our own typed code while upstream merges on repeated ids; andci-test-plan.mjsselectspackages/runtimefor apatches/or lockfile change, so the guard runs on this PR — verified by running it.