Skip to content

deps(ai): migrate the Vercel AI SDK from 6 to 7 - #185

Merged
mrsibe merged 2 commits into
mainfrom
deps/ai-sdk-7
Sep 30, 2026
Merged

mrsibe merged 2 commits into
mainfrom
deps/ai-sdk-7

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

What

Upgrade the AI SDK from 6 to 7, on top of the already-merged 5 → 6 PR (#184).

Package Before After
ai 6.0.297 7.0.123
@ai-sdk/provider 3.0.18 4.0.20
@ai-sdk/openai 3.0.121 4.0.81
@ai-sdk/anthropic 3.0.125 4.0.69
@ai-sdk/google 3.0.129 4.0.86
@ai-sdk/openai-compatible 2.0.80 3.0.60

The codemod, and why it looked "stuck"

npx @ai-sdk/codemod v7 ignores its positional path argument and scans the repository root. Its ignore list covers node_modules, dist and build, but not out/ — the bundled renderer output (6 MB of index-*.js plus the 1.3 MB pdf worker). jscodeshift parsed that once per codemod, ~20 times, which reads as a hang.

Removing out/ and dist/ first takes the whole run from "stuck" to one minute. Worth knowing for the next major.

After that, the codemod touched exactly one line: createGoogleGenerativeAI → createGoogle. KnowNote does not use the other renamed surfaces (fullStream, onFinish, telemetry, tools, custom providers).

⚠️ Silent behavioural regression: v7 drops system messages and returns an empty answer

This is not a compatibility nit. It is the kind of change that makes RAG look like it
broke for no reason: no exception, no error event, no non-200 — just a blank answer.
It is recorded here and in commit 87c33fc deliberately, so it is findable later.

AI SDK 7 rejects { role: 'system' } messages inside messages by default — and it does not throw. Verified directly:

default:               OK -> ""          # empty answer, no error
allowSystemInMessages: OK -> "hi"

KnowNote puts the retrieved context (RAG) and the title-generation prompt into the history as a system message (chatHandlers.ts ×2, noteHandlers.ts). Left alone, every RAG turn would silently produce a blank answer — worse than a crash, because nothing surfaces.

ModelClient.streamChat now passes allowSystemInMessages: true. Every message it sends is built by this app, not by the user, so the untrusted-injection case that the default guards against does not apply. A regression test asserts the system message actually reaches the provider's prompt.

Also done: stateless toUIMessageStream

result.toUIMessageStream(...) is deprecated in v7 — it works but warns every turn and is slated for removal in v8. Moved to the stateless helper, same options:

toUIMessageStream({ stream: result.stream, sendReasoning, onError, messageMetadata })

The messageMetadata usage-on-finish wiring is unchanged, so the forwarded event stream is identical — which is exactly what the chat-stream and UI-message tests cover.

Verified (Linux x64, WSL2)

Command Result
npm run typecheck clean
npm test 471/471 pass (470 + the system-message regression test)
npm run build:unpack && npm run smoke:packaged 29 checks pass

The packaged smoke matters here because v7 is ESM-only: ai and the providers are devDependencies that rollup bundles into out/main. The build succeeds and the real packaged app starts and self-checks.

Also checked rather than assumed:

  • streamObject still exists in v7 (deprecated, not removed), so quiz / Anki / mind-map keep working.
  • LanguageModelUsage still exposes the numeric inputTokens / outputTokens / totalTokens this app reads.
  • readUIMessageStream and MockLanguageModelV3 are still present.

Known remaining deprecations (not fixed here)

  • streamObject is deprecated in favour of streamText + Output.object. Rewriting QuizService, AnkiCardService and MindMapService is a behaviour-sensitive change (partial-object streams, progress reporting) and belongs in its own PR, not a version bump.
  • The 'openai-compatible' providerOptions key is still read but should become 'openaiCompatible'. It only logs a warning; renaming changes which key the provider reports providerMetadata under, so I left it for a focused follow-up.

Rollback

Revert the branch. No schema, storage format, or persisted data changes.

  ai                          6.0.297 -> 7.0.123
  @ai-sdk/provider             3.0.18 -> 4.0.20
  @ai-sdk/openai               3.0.121 -> 4.0.81
  @ai-sdk/anthropic            3.0.125 -> 4.0.69
  @ai-sdk/google               3.0.129 -> 4.0.86
  @ai-sdk/openai-compatible    2.0.80 -> 3.0.60

Ran the official v7 codemod (npx @ai-sdk/codemod@4.0.3 v7), then fixed the one
change the codemod and the type checker cannot see.

The codemod only touched one line: `createGoogleGenerativeAI` -> `createGoogle`
in the Google adapter. KnowNote does not use the other renamed surfaces
(fullStream, onFinish, telemetry, tools, custom providers).

The real risk is that AI SDK 7 rejects `{ role: 'system' }` messages inside
`messages` by default, and it does not throw: the answer comes back empty.
KnowNote puts the retrieved context (RAG) and the title-generation prompt into
the history as a system message, in both chat handlers and noteHandlers. Left
alone, every RAG turn would silently produce a blank answer.

`ModelClient.streamChat` now passes `allowSystemInMessages: true`. Every message
it sends is built by this app, not by the user, so the untrusted-injection case
that default guards against does not apply. A regression test asserts the
system message actually reaches the provider's prompt.

Also verified rather than assumed:

- The ESM-only packaging is fine here: `ai` and the providers are
  devDependencies that rollup bundles into `out/main`, and the packaged smoke
  starts and self-checks.
- `streamObject` still exists in v7 (deprecated, not removed), so the quiz /
  Anki / mind-map services keep working.
- `LanguageModelUsage` still exposes the numeric `inputTokens` / `outputTokens`
  / `totalTokens` this app reads.

Verified on Linux x64 (WSL2):

- `npm run typecheck` - clean
- `npm test` - 471/471 pass
- `npm run build:unpack && npm run smoke:packaged` - 29 checks pass

Note on running the codemod: `@ai-sdk/codemod v7` ignores its positional path
argument and scans the repository root. Its ignore list covers
node_modules/dist/build but not `out/`, so it parses the 6 MB bundled renderer
output once per codemod and appears to hang. Removing `out/` and `dist/` first
takes it from "stuck" to one minute.
`result.toUIMessageStream(...)` is deprecated in v7: it still works but logs a
deprecation warning on every turn, and it is scheduled for removal in the next
major. The stateless top-level helper takes the same options plus the stream:

  result.toUIMessageStream({ ... })  ->  toUIMessageStream({ stream: result.stream, ... })

Same options, same `messageMetadata` usage-on-finish wiring, so the forwarded
event stream is unchanged - which is what the chat-stream and UI-message tests
assert.

Verified: typecheck clean, 471/471 tests pass, packaged smoke 29/29.
@github-actions github-actions Bot added the dependencies Dependency updates label Sep 30, 2026
@mrsibe
mrsibe merged commit a6b248d into main Sep 30, 2026
5 checks passed
@mrsibe
mrsibe deleted the deps/ai-sdk-7 branch September 30, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant