Skip to content

chore(deps): bump the AI SDK and re-port the tool-call tracker patch - #2193

Merged
Astro-Han merged 4 commits into
mainfrom
chore/ai-sdk-drop-tool-call-patch
Aug 5, 2026
Merged

chore(deps): bump the AI SDK and re-port the tool-call tracker patch#2193
Astro-Han merged 4 commits into
mainfrom
chore/ai-sdk-drop-tool-call-patch

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Bumps the AI SDK (ai 7.0.31→7.0.52, @ai-sdk/provider-utils 5.0.11→5.0.21 and the provider packages alongside it) and re-ports the StreamingToolCallTracker patch 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 a Set plus toolCallsById / toolCallsByIndex lookups, 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.md says 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:

  • A gateway that omits id (all calls at index: 0, the Ollama shape this patch exists for) throws Expected 'id' to be a string. and kills the turn. function.name distinguishes the calls but is never consulted.
  • A whitespace id on a continuation misses the id lookup, starts a new call, and throws Expected 'function.name' to be a string.
  • A repeated id merges two calls: {"path":"a"}{"path":"b"} in one input, second call dropped.
  • name: '' mints a call with toolName: ''; an empty wire id reaches tool-runtime.ts verbatim, where operationId collides or is empty.
  • latestToolCall absorbs 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.name rejecting the match, mint an id when the wire's cannot address a call, and order flush() by index only when every index is present and unique.

The remaining behaviour is reported upstream as vercel/ai#18440; patches/README.md links 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 --reverse silently 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, both deep-research-tools failing on SQLite 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 in generate-third-party-notices.mjs is keyed by exact version, so the bump left provider-utils with no LICENSE file and no override, failing the package lane. Re-checked rather than carried over (5.0.21 still declares Apache-2.0 and still omits the file), and the regenerated notices pick up undici@7.29.0 as a new transitive production dependency.
  • Full CI green on the PR, including package, e2e_shard, storybook, and the Windows baseline lane.
  • Not run locally: desktop E2E and the other workspaces' suites — covered by CI above.

Review focus

The patch body is the whole risk surface. patches/@ai-sdk+provider-utils+5.0.21.patch is not a rewrite of our previous logic — the rules are byte-for-byte the ones the 2448-run differential enumeration validated at dce22434e — 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: processNewToolCall now returns the record (we ignore the return and let processDelta bail early), and upstream's toolCallsById / toolCallsByIndex / latestToolCall are dropped in favour of the newest-first scan, which is not an equivalent lookup — a delta whose id and index address 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. findByAlias compared 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: assertToolCallIdsUsable now 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.js is 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; and ci-test-plan.mjs selects packages/runtime for a patches/ or lockfile change, so the guard runs on this PR — verified by running it.

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
Astro-Han marked this pull request as ready for review August 5, 2026 05:59
@Astro-Han
Astro-Han merged commit d8426e6 into main Aug 5, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant