From 8adc9079bccde26ba611c3d7b7ded8d0cd8038e0 Mon Sep 17 00:00:00 2001 From: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:07:57 +0530 Subject: [PATCH 1/3] fix: support AI SDK 7 in react-headless --- .github/scripts/verify-ai-sdk-v7.mjs | 123 ++++++++++++++++++ .github/workflows/ai-sdk-v7-compat.yml | 77 +++++++++++ packages/assistant-ui/README.md | 3 + packages/react-headless/README.md | 9 +- packages/react-headless/package.json | 2 +- .../adapters/__tests__/vercel-ai-sdk.test.ts | 2 +- .../src/stream/adapters/vercel-ai-sdk.ts | 5 +- .../formats/vercel-ai-message-format.ts | 2 +- 8 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 .github/scripts/verify-ai-sdk-v7.mjs create mode 100644 .github/workflows/ai-sdk-v7-compat.yml diff --git a/.github/scripts/verify-ai-sdk-v7.mjs b/.github/scripts/verify-ai-sdk-v7.mjs new file mode 100644 index 000000000..dd4825c7e --- /dev/null +++ b/.github/scripts/verify-ai-sdk-v7.mjs @@ -0,0 +1,123 @@ +import { shouldContinueAfterOpenUIPrompt } from "@openuidev/assistant-ui/ai-sdk"; +import { vercelAIAdapter, vercelAIMessageFormat } from "@openuidev/react-headless"; +import { convertToModelMessages } from "ai"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const { version } = require("ai/package.json"); + +assert.equal(version.split(".")[0], "7", `Expected AI SDK 7, received ${version}`); + +const sse = (chunk) => `data: ${JSON.stringify(chunk)}\n\n`; +const response = new Response( + [ + { type: "start", messageId: "assistant-1" }, + { type: "start-step" }, + { type: "custom", kind: "openui.progress" }, + { + type: "reasoning-file", + url: "data:text/plain;base64,b2s=", + mediaType: "text/plain", + }, + { type: "text-start", id: "text-1" }, + { type: "text-delta", id: "text-1", delta: "Ready" }, + { type: "text-end", id: "text-1" }, + { + type: "tool-input-available", + toolCallId: "prompt-1", + toolName: "prompt_openui", + input: { ui: "root = Card([])" }, + }, + { + type: "tool-output-available", + toolCallId: "prompt-1", + output: { message: "Submit" }, + }, + { + type: "tool-approval-response", + approvalId: "approval-1", + approved: true, + }, + { type: "finish-step" }, + { type: "finish", finishReason: "stop" }, + ] + .map(sse) + .join(""), + { headers: { "Content-Type": "text/event-stream" } }, +); + +const events = []; +for await (const event of vercelAIAdapter().parse(response)) { + events.push(event); +} + +assert.deepEqual( + events.map((event) => event.type), + [ + "STEP_STARTED", + "TEXT_MESSAGE_START", + "TEXT_MESSAGE_CONTENT", + "TOOL_CALL_START", + "TOOL_CALL_ARGS", + "TOOL_CALL_END", + "TOOL_CALL_RESULT", + "TEXT_MESSAGE_END", + "STEP_FINISHED", + ], +); + +const uiMessages = vercelAIMessageFormat.toApi([ + { id: "user-1", role: "user", content: "Show a form" }, + { + id: "assistant-1", + role: "assistant", + content: "Ready", + toolCalls: [ + { + id: "prompt-1", + type: "function", + function: { + name: "prompt_openui", + arguments: JSON.stringify({ ui: "root = Card([])" }), + }, + }, + ], + }, + { + id: "tool-result-prompt-1", + role: "tool", + toolCallId: "prompt-1", + content: JSON.stringify({ message: "Submit" }), + }, +]); + +const modelMessages = await convertToModelMessages(uiMessages); +assert.deepEqual( + modelMessages.map((message) => message.role), + ["user", "assistant", "tool"], +); + +assert.equal( + shouldContinueAfterOpenUIPrompt({ + messages: [ + { + id: "assistant-1", + role: "assistant", + parts: [ + { type: "step-start" }, + { + type: "tool-prompt_openui", + toolCallId: "prompt-1", + state: "output-available", + input: { ui: "root = Card([])" }, + output: { message: "Submit" }, + }, + ], + }, + ], + }), + true, +); + +console.log(`Verified OpenUI compatibility with AI SDK ${version}.`); diff --git a/.github/workflows/ai-sdk-v7-compat.yml b/.github/workflows/ai-sdk-v7-compat.yml new file mode 100644 index 000000000..a95712ed8 --- /dev/null +++ b/.github/workflows/ai-sdk-v7-compat.yml @@ -0,0 +1,77 @@ +name: AI SDK 7 Compatibility + +on: + push: + branches: [main] + paths: + - ".github/scripts/verify-ai-sdk-v7.mjs" + - ".github/workflows/ai-sdk-v7-compat.yml" + - "packages/assistant-ui/**" + - "packages/react-headless/**" + - "pnpm-lock.yaml" + pull_request: + branches: [main] + paths: + - ".github/scripts/verify-ai-sdk-v7.mjs" + - ".github/workflows/ai-sdk-v7-compat.yml" + - "packages/assistant-ui/**" + - "packages/react-headless/**" + - "pnpm-lock.yaml" + +permissions: + contents: read + +jobs: + compatibility: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v2 + with: + version: 9.15.4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: "pnpm-lock.yaml" + + - name: Install package dependencies + run: >- + pnpm install --frozen-lockfile + --filter @openuidev/react-headless... + --filter @openuidev/react-lang... + --filter @openuidev/react-ui... + --filter @openuidev/assistant-ui + + - name: Test package sources with AI SDK 7 + run: | + pnpm --filter @openuidev/react-headless add --save-dev "ai@^7" --lockfile=false + pnpm --filter @openuidev/assistant-ui add --save-dev "ai@^7" --lockfile=false + pnpm --filter @openuidev/react-headless run build + pnpm --filter @openuidev/react-headless run test + pnpm --filter @openuidev/assistant-ui run typecheck + pnpm --filter @openuidev/assistant-ui run test + + - name: Pack OpenUI packages + env: + PACKAGE_DIR: ${{ runner.temp }}/openui-packages + run: | + mkdir -p "${PACKAGE_DIR}" + pnpm --dir packages/react-headless pack --pack-destination "${PACKAGE_DIR}" + pnpm --dir packages/assistant-ui pack --pack-destination "${PACKAGE_DIR}" + + - name: Verify an npm consumer installation + env: + CONSUMER_DIR: ${{ runner.temp }}/ai-sdk-v7-consumer + PACKAGE_DIR: ${{ runner.temp }}/openui-packages + run: | + mkdir -p "${CONSUMER_DIR}" + cd "${CONSUMER_DIR}" + npm install --ignore-scripts --no-audit --no-fund \ + "ai@^7" \ + "${PACKAGE_DIR}"/openuidev-react-headless-*.tgz \ + "${PACKAGE_DIR}"/openuidev-assistant-ui-*.tgz + cp "${GITHUB_WORKSPACE}/.github/scripts/verify-ai-sdk-v7.mjs" ./verify-ai-sdk-v7.mjs + node ./verify-ai-sdk-v7.mjs diff --git a/packages/assistant-ui/README.md b/packages/assistant-ui/README.md index 2a9caefbd..d82d44d87 100644 --- a/packages/assistant-ui/README.md +++ b/packages/assistant-ui/README.md @@ -104,6 +104,9 @@ can implement that policy using the runtime's continuation API. For a Vercel AI SDK runtime, the package provides that policy as an optional convenience helper. Install `ai` and import it from the `/ai-sdk` subpath: +The helper supports AI SDK 6 and 7. AI SDK 7 requires Node.js 22 or later and +an ESM application. + ```tsx import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; import { shouldContinueAfterOpenUIPrompt } from "@openuidev/assistant-ui/ai-sdk"; diff --git a/packages/react-headless/README.md b/packages/react-headless/README.md index c79650864..4397958f5 100644 --- a/packages/react-headless/README.md +++ b/packages/react-headless/README.md @@ -22,8 +22,13 @@ The Vercel AI SDK integration has one optional peer dependency: ```bash npm install ai@^6 +# or, on Node.js 22+ with ESM +npm install ai@^7 ``` +Both AI SDK 6 and 7 are supported. AI SDK 7 requires Node.js 22 or later and +is ESM-only. + ## Overview Use `@openuidev/react-headless` when you want OpenUI's chat behavior without OpenUI's visual components: @@ -185,7 +190,7 @@ const llm = fetchLLM({ url: "/api/chat", streamAdapter: openAIAdapter() }); | `openAIAdapter()` | Parses OpenAI Chat Completions streaming (`ChatCompletionChunk`) | | `openAIResponsesAdapter()` | Parses OpenAI Responses API streaming (`ResponseStreamEvent`) | | `openAIReadableStreamAdapter()` | Parses OpenAI SDK's `Stream.toReadableStream()` NDJSON output | -| `vercelAIAdapter()` | Parses Vercel AI SDK v6 UIMessage streams from `toUIMessageStreamResponse()` | +| `vercelAIAdapter()` | Parses Vercel AI SDK v6 and v7 UIMessage streams | For a Vercel AI SDK route, use its stream adapter and message format together: @@ -236,7 +241,7 @@ const llm = fetchLLM({ | `identityMessageFormat` | Default format when messages are already AG-UI shaped | | `openAIMessageFormat` | Converts to/from OpenAI `ChatCompletionMessageParam[]` | | `openAIConversationMessageFormat` | Converts to/from OpenAI Responses API `ResponseInputItem[]` | -| `vercelAIMessageFormat` | Converts to/from Vercel AI SDK v6 `UIMessage[]` | +| `vercelAIMessageFormat` | Converts to/from Vercel AI SDK v6 and v7 `UIMessage[]` | ### Custom format diff --git a/packages/react-headless/package.json b/packages/react-headless/package.json index 3fdc18013..73c423111 100644 --- a/packages/react-headless/package.json +++ b/packages/react-headless/package.json @@ -39,7 +39,7 @@ "ci": "pnpm run lint:check && pnpm run format:check" }, "peerDependencies": { - "ai": "^6.0.0", + "ai": "^6.0.0 || ^7.0.0", "react": "catalog:", "zustand": "catalog:" }, diff --git a/packages/react-headless/src/stream/adapters/__tests__/vercel-ai-sdk.test.ts b/packages/react-headless/src/stream/adapters/__tests__/vercel-ai-sdk.test.ts index e517ca882..6014eddb4 100644 --- a/packages/react-headless/src/stream/adapters/__tests__/vercel-ai-sdk.test.ts +++ b/packages/react-headless/src/stream/adapters/__tests__/vercel-ai-sdk.test.ts @@ -492,7 +492,7 @@ describe("vercelAIAdapter", () => { try { await expect(collect(vercelAIAdapter().parse(makeResponse("")))).rejects.toThrow( - 'vercelAIAdapter requires the optional peer dependency "ai" (Vercel AI SDK v6).', + 'vercelAIAdapter requires the optional peer dependency "ai" (Vercel AI SDK v6 or v7).', ); } finally { vi.doUnmock("ai"); diff --git a/packages/react-headless/src/stream/adapters/vercel-ai-sdk.ts b/packages/react-headless/src/stream/adapters/vercel-ai-sdk.ts index fea294e4c..b52d749a1 100644 --- a/packages/react-headless/src/stream/adapters/vercel-ai-sdk.ts +++ b/packages/react-headless/src/stream/adapters/vercel-ai-sdk.ts @@ -2,7 +2,7 @@ import type { UIMessage, UIMessageChunk } from "ai"; import { AGUIEvent, EventType, StreamProtocolAdapter } from "../../types"; const MISSING_AI_SDK_MESSAGE = - 'vercelAIAdapter requires the optional peer dependency "ai" (Vercel AI SDK v6).'; + 'vercelAIAdapter requires the optional peer dependency "ai" (Vercel AI SDK v6 or v7).'; const PROVIDER_EXECUTED_TOOLS_UNSUPPORTED_MESSAGE = "Vercel AI SDK provider-executed tools are not supported because AG-UI messages cannot preserve providerExecuted semantics."; const TOOL_EXECUTION_DENIED_MESSAGE = "Tool execution was denied"; @@ -64,8 +64,7 @@ function toolResult(toolCallId: string, content: string, error?: string): AGUIEv } /** - * Adapter for Vercel AI SDK v6 UIMessage streams, such as responses returned by - * `toUIMessageStreamResponse()`. + * Adapter for Vercel AI SDK v6 and v7 UIMessage streams. * * The AI SDK is loaded only when parsing begins so it can remain an optional * peer dependency for consumers that use other stream adapters. Its diff --git a/packages/react-headless/src/stream/formats/vercel-ai-message-format.ts b/packages/react-headless/src/stream/formats/vercel-ai-message-format.ts index 5a9d1315b..dd82b7c76 100644 --- a/packages/react-headless/src/stream/formats/vercel-ai-message-format.ts +++ b/packages/react-headless/src/stream/formats/vercel-ai-message-format.ts @@ -438,7 +438,7 @@ function fromVercelMessages(data: unknown): Message[] { } /** - * Converts messages between AG-UI and Vercel AI SDK v6 `UIMessage` format. + * Converts messages between AG-UI and Vercel AI SDK v6 and v7 `UIMessage` format. * * AG-UI tool-result messages are folded into the matching assistant tool part, * because `UIMessage` represents a tool invocation and its result as one part. From 6db72dac8d9d064289ca779f01ca791c51687d42 Mon Sep 17 00:00:00 2001 From: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:10:04 +0530 Subject: [PATCH 2/3] chore: bump react-headless to 0.9.8 --- packages/react-headless/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-headless/package.json b/packages/react-headless/package.json index 73c423111..f91f43059 100644 --- a/packages/react-headless/package.json +++ b/packages/react-headless/package.json @@ -1,6 +1,6 @@ { "name": "@openuidev/react-headless", - "version": "0.9.7", + "version": "0.9.8", "description": "Headless React primitives for AI chat — state management, streaming adapters for OpenAI, Vercel AI SDK, and AG-UI, message format converters, and thread management for OpenUI generative UI apps", "license": "MIT", "type": "module", From cb51996e4345dc05a5ae59efc59f542ec8d393ab Mon Sep 17 00:00:00 2001 From: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:16:23 +0530 Subject: [PATCH 3/3] chore: remove temporary AI SDK 7 CI --- .github/scripts/verify-ai-sdk-v7.mjs | 123 ------------------------- .github/workflows/ai-sdk-v7-compat.yml | 77 ---------------- 2 files changed, 200 deletions(-) delete mode 100644 .github/scripts/verify-ai-sdk-v7.mjs delete mode 100644 .github/workflows/ai-sdk-v7-compat.yml diff --git a/.github/scripts/verify-ai-sdk-v7.mjs b/.github/scripts/verify-ai-sdk-v7.mjs deleted file mode 100644 index dd4825c7e..000000000 --- a/.github/scripts/verify-ai-sdk-v7.mjs +++ /dev/null @@ -1,123 +0,0 @@ -import { shouldContinueAfterOpenUIPrompt } from "@openuidev/assistant-ui/ai-sdk"; -import { vercelAIAdapter, vercelAIMessageFormat } from "@openuidev/react-headless"; -import { convertToModelMessages } from "ai"; -import assert from "node:assert/strict"; -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); -const { version } = require("ai/package.json"); - -assert.equal(version.split(".")[0], "7", `Expected AI SDK 7, received ${version}`); - -const sse = (chunk) => `data: ${JSON.stringify(chunk)}\n\n`; -const response = new Response( - [ - { type: "start", messageId: "assistant-1" }, - { type: "start-step" }, - { type: "custom", kind: "openui.progress" }, - { - type: "reasoning-file", - url: "data:text/plain;base64,b2s=", - mediaType: "text/plain", - }, - { type: "text-start", id: "text-1" }, - { type: "text-delta", id: "text-1", delta: "Ready" }, - { type: "text-end", id: "text-1" }, - { - type: "tool-input-available", - toolCallId: "prompt-1", - toolName: "prompt_openui", - input: { ui: "root = Card([])" }, - }, - { - type: "tool-output-available", - toolCallId: "prompt-1", - output: { message: "Submit" }, - }, - { - type: "tool-approval-response", - approvalId: "approval-1", - approved: true, - }, - { type: "finish-step" }, - { type: "finish", finishReason: "stop" }, - ] - .map(sse) - .join(""), - { headers: { "Content-Type": "text/event-stream" } }, -); - -const events = []; -for await (const event of vercelAIAdapter().parse(response)) { - events.push(event); -} - -assert.deepEqual( - events.map((event) => event.type), - [ - "STEP_STARTED", - "TEXT_MESSAGE_START", - "TEXT_MESSAGE_CONTENT", - "TOOL_CALL_START", - "TOOL_CALL_ARGS", - "TOOL_CALL_END", - "TOOL_CALL_RESULT", - "TEXT_MESSAGE_END", - "STEP_FINISHED", - ], -); - -const uiMessages = vercelAIMessageFormat.toApi([ - { id: "user-1", role: "user", content: "Show a form" }, - { - id: "assistant-1", - role: "assistant", - content: "Ready", - toolCalls: [ - { - id: "prompt-1", - type: "function", - function: { - name: "prompt_openui", - arguments: JSON.stringify({ ui: "root = Card([])" }), - }, - }, - ], - }, - { - id: "tool-result-prompt-1", - role: "tool", - toolCallId: "prompt-1", - content: JSON.stringify({ message: "Submit" }), - }, -]); - -const modelMessages = await convertToModelMessages(uiMessages); -assert.deepEqual( - modelMessages.map((message) => message.role), - ["user", "assistant", "tool"], -); - -assert.equal( - shouldContinueAfterOpenUIPrompt({ - messages: [ - { - id: "assistant-1", - role: "assistant", - parts: [ - { type: "step-start" }, - { - type: "tool-prompt_openui", - toolCallId: "prompt-1", - state: "output-available", - input: { ui: "root = Card([])" }, - output: { message: "Submit" }, - }, - ], - }, - ], - }), - true, -); - -console.log(`Verified OpenUI compatibility with AI SDK ${version}.`); diff --git a/.github/workflows/ai-sdk-v7-compat.yml b/.github/workflows/ai-sdk-v7-compat.yml deleted file mode 100644 index a95712ed8..000000000 --- a/.github/workflows/ai-sdk-v7-compat.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: AI SDK 7 Compatibility - -on: - push: - branches: [main] - paths: - - ".github/scripts/verify-ai-sdk-v7.mjs" - - ".github/workflows/ai-sdk-v7-compat.yml" - - "packages/assistant-ui/**" - - "packages/react-headless/**" - - "pnpm-lock.yaml" - pull_request: - branches: [main] - paths: - - ".github/scripts/verify-ai-sdk-v7.mjs" - - ".github/workflows/ai-sdk-v7-compat.yml" - - "packages/assistant-ui/**" - - "packages/react-headless/**" - - "pnpm-lock.yaml" - -permissions: - contents: read - -jobs: - compatibility: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v2 - with: - version: 9.15.4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: "pnpm-lock.yaml" - - - name: Install package dependencies - run: >- - pnpm install --frozen-lockfile - --filter @openuidev/react-headless... - --filter @openuidev/react-lang... - --filter @openuidev/react-ui... - --filter @openuidev/assistant-ui - - - name: Test package sources with AI SDK 7 - run: | - pnpm --filter @openuidev/react-headless add --save-dev "ai@^7" --lockfile=false - pnpm --filter @openuidev/assistant-ui add --save-dev "ai@^7" --lockfile=false - pnpm --filter @openuidev/react-headless run build - pnpm --filter @openuidev/react-headless run test - pnpm --filter @openuidev/assistant-ui run typecheck - pnpm --filter @openuidev/assistant-ui run test - - - name: Pack OpenUI packages - env: - PACKAGE_DIR: ${{ runner.temp }}/openui-packages - run: | - mkdir -p "${PACKAGE_DIR}" - pnpm --dir packages/react-headless pack --pack-destination "${PACKAGE_DIR}" - pnpm --dir packages/assistant-ui pack --pack-destination "${PACKAGE_DIR}" - - - name: Verify an npm consumer installation - env: - CONSUMER_DIR: ${{ runner.temp }}/ai-sdk-v7-consumer - PACKAGE_DIR: ${{ runner.temp }}/openui-packages - run: | - mkdir -p "${CONSUMER_DIR}" - cd "${CONSUMER_DIR}" - npm install --ignore-scripts --no-audit --no-fund \ - "ai@^7" \ - "${PACKAGE_DIR}"/openuidev-react-headless-*.tgz \ - "${PACKAGE_DIR}"/openuidev-assistant-ui-*.tgz - cp "${GITHUB_WORKSPACE}/.github/scripts/verify-ai-sdk-v7.mjs" ./verify-ai-sdk-v7.mjs - node ./verify-ai-sdk-v7.mjs