From 4036c7d4470d335022952e67da0c56048f341d23 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Mon, 12 Oct 2026 03:47:50 -0700 Subject: [PATCH] feat(tools): support AI SDK 6 and 7 (3.1.0) (#1794) --- .github/workflows/ci.yml | 4 + .github/workflows/publish-tools.yml | 4 + bun.lock | 12 +- packages/tools/README.md | 2 + packages/tools/package.json | 19 ++- packages/tools/src/vercel/index.ts | 8 +- packages/tools/src/vercel/memory-prompt.ts | 6 +- packages/tools/src/vercel/util.ts | 9 +- packages/tools/test/ai-sdk-compat/run.ts | 153 +++++++++++++++++++++ 9 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 packages/tools/test/ai-sdk-compat/run.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 861dc706..7e93d88f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,10 @@ jobs: if: steps.sdk-changes.outputs.tools == 'true' || steps.sdk-changes.outputs.sdk_playground == 'true' run: bun run --cwd packages/tools build + - name: Check Tools against AI SDK 5, 6 and 7 + if: steps.sdk-changes.outputs.tools == 'true' + run: bun run --cwd packages/tools test:compat + - name: Run SDK Playground type checking if: steps.sdk-changes.outputs.sdk_playground == 'true' run: bun run --cwd apps/sdk-playground check-types:app diff --git a/.github/workflows/publish-tools.yml b/.github/workflows/publish-tools.yml index 8dfb8569..c78f3653 100644 --- a/.github/workflows/publish-tools.yml +++ b/.github/workflows/publish-tools.yml @@ -58,6 +58,10 @@ jobs: if: steps.version-check.outputs.changed == 'true' run: bun run build + - name: Check against AI SDK 5, 6 and 7 + if: steps.version-check.outputs.changed == 'true' + run: bun run test:compat + - name: Publish if: steps.version-check.outputs.changed == 'true' run: npm publish --access public --provenance diff --git a/bun.lock b/bun.lock index f5683eeb..3f76cc9c 100644 --- a/bun.lock +++ b/bun.lock @@ -227,34 +227,36 @@ }, "packages/tools": { "name": "@supermemory/tools", - "version": "3.0.0", + "version": "3.1.0", "dependencies": { - "@ai-sdk/anthropic": "^2.0.25", - "@ai-sdk/openai": "^2.0.23", - "ai": "^5.0.29", "lru-cache": "^11.2.6", "openai": "^4.104.0", "supermemory": "^5.0.0", "zod": "^4.1.5", }, "devDependencies": { + "@ai-sdk/anthropic": "^2.0.25", + "@ai-sdk/openai": "^2.0.23", "@ai-sdk/provider": "^3.0.0", "@anthropic-ai/sdk": "^0.65.0", "@mastra/core": "^1.0.0", "@total-typescript/tsconfig": "^1.0.4", "@types/bun": "^1.2.21", "@voltagent/core": "^2.6.12", + "ai": "^5.0.29", "dotenv": "^16.6.1", "tsdown": "^0.14.2", "typescript": "^5.9.2", "vitest": "3.2.6", }, "peerDependencies": { - "@ai-sdk/provider": "^2.0.0 || ^3.0.0", + "@ai-sdk/provider": "^2.0.0 || ^3.0.0 || ^4.0.0", "@voltagent/core": "^2.6.12", + "ai": "^5.0.0 || ^6.0.0 || ^7.0.0", }, "optionalPeers": [ "@voltagent/core", + "ai", ], }, "packages/ui": { diff --git a/packages/tools/README.md b/packages/tools/README.md index 87a9dab2..f6596746 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -22,6 +22,8 @@ npm install @supermemory/tools Requires Node 18 or later and a `SUPERMEMORY_API_KEY` from [console.supermemory.ai](https://console.supermemory.ai). The `supermemory@5` SDK comes along as a dependency. Against a self-hosted server, pass `baseUrl` and run `supermemory-server` v0.0.9 or later, which is the first version that serves the v5 routes. +The `ai-sdk` entry point works with AI SDK 5, 6 and 7. It uses the `ai` package your app already installs, so install `ai` yourself if you use that entry point. + ## Migrating from 2.x 3.0 moves every call to the Supermemory v5 API (`supermemory@5`). The full guide is at [supermemory.ai/docs/migration/tools-v3-upgrade](https://supermemory.ai/docs/migration/tools-v3-upgrade). diff --git a/packages/tools/package.json b/packages/tools/package.json index ec8aa63a..081e50d1 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,7 +1,7 @@ { "name": "@supermemory/tools", "type": "module", - "version": "3.0.0", + "version": "3.1.0", "description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory", "scripts": { "build": "tsdown", @@ -9,36 +9,41 @@ "check-types": "tsc --noEmit", "test": "vitest --testTimeout 100000", "test:unit": "vitest run --testTimeout 100000 src/tools-shared.test.ts src/tool-operations.test.ts src/claude-memory.test.ts test/with-supermemory/unit.test.ts test/with-supermemory/conversation-conversion.test.ts test/openai-middleware.unit.test.ts test/mastra/unit.test.ts test/voltagent.unit.test.ts", - "test:watch": "vitest --watch --testTimeout 100000" + "test:watch": "vitest --watch --testTimeout 100000", + "test:compat": "bun test/ai-sdk-compat/run.ts" }, "dependencies": { - "@ai-sdk/anthropic": "^2.0.25", - "@ai-sdk/openai": "^2.0.23", - "ai": "^5.0.29", "lru-cache": "^11.2.6", "openai": "^4.104.0", "supermemory": "^5.0.0", "zod": "^4.1.5" }, "devDependencies": { + "@ai-sdk/anthropic": "^2.0.25", + "@ai-sdk/openai": "^2.0.23", "@ai-sdk/provider": "^3.0.0", "@anthropic-ai/sdk": "^0.65.0", "@voltagent/core": "^2.6.12", "@mastra/core": "^1.0.0", "@total-typescript/tsconfig": "^1.0.4", "@types/bun": "^1.2.21", + "ai": "^5.0.29", "dotenv": "^16.6.1", "tsdown": "^0.14.2", "typescript": "^5.9.2", "vitest": "3.2.6" }, "peerDependencies": { - "@ai-sdk/provider": "^2.0.0 || ^3.0.0", - "@voltagent/core": "^2.6.12" + "@ai-sdk/provider": "^2.0.0 || ^3.0.0 || ^4.0.0", + "@voltagent/core": "^2.6.12", + "ai": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { "@voltagent/core": { "optional": true + }, + "ai": { + "optional": true } }, "main": "./dist/index.js", diff --git a/packages/tools/src/vercel/index.ts b/packages/tools/src/vercel/index.ts index fe999c10..9e88889b 100644 --- a/packages/tools/src/vercel/index.ts +++ b/packages/tools/src/vercel/index.ts @@ -78,10 +78,10 @@ interface WrapVercelLanguageModelOptions { * a new system prompt with the memories. Pre-LLM profile retrieval uses a fixed internal * time budget and cannot be configured via options. * - * Supports both Vercel AI SDK 5 (LanguageModelV2) and SDK 6 (LanguageModelV3) via runtime + * Supports Vercel AI SDK 5 (LanguageModelV2), 6 (LanguageModelV3) and 7 (LanguageModelV4) via runtime * detection of `model.specificationVersion`. * - * @param model - The language model to wrap with supermemory capabilities (V2 or V3) + * @param model - The language model to wrap with supermemory capabilities (V2, V3 or V4) * @param options - Configuration options for Supermemory integration * @param options.namespace - Required. The namespace for memory search (e.g., user ID, project ID) * @param options.id - Required. ID that groups messages into a single document for contextual memory generation @@ -180,7 +180,7 @@ const wrapVercelLanguageModel = ( } try { - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility across V2, V3 and V4 const result = await target.doGenerate(modelParams as any) if ( @@ -245,7 +245,7 @@ const wrapVercelLanguageModel = ( try { const { stream, ...rest } = await target.doStream( - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility across V2, V3 and V4 modelParams as any, ) diff --git a/packages/tools/src/vercel/memory-prompt.ts b/packages/tools/src/vercel/memory-prompt.ts index e4664b22..4c80e27c 100644 --- a/packages/tools/src/vercel/memory-prompt.ts +++ b/packages/tools/src/vercel/memory-prompt.ts @@ -46,7 +46,7 @@ export const extractQueryText = ( return content } - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility across V2, V3 and V4 return (content as any[]) .filter((part) => part.type === "text") .map((part) => part.text || "") @@ -74,7 +74,7 @@ export const injectMemoriesIntoParams = ( if (systemPromptExists) { logger.debug("Replaced Supermemory context in existing system prompt") let injected = false - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 prompt types + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility across V2, V3 and V4 prompt types const newPrompt = params.prompt.map((prompt: any) => { if (prompt.role !== "system") return prompt const content = String(prompt.content ?? "") @@ -95,7 +95,7 @@ export const injectMemoriesIntoParams = ( const newPrompt = [ { role: "system" as const, content: memoryContext }, ...params.prompt, - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 prompt types + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility across V2, V3 and V4 prompt types ] as any return { ...params, prompt: newPrompt } as LanguageModelCallOptions } diff --git a/packages/tools/src/vercel/util.ts b/packages/tools/src/vercel/util.ts index 57d16037..640eee11 100644 --- a/packages/tools/src/vercel/util.ts +++ b/packages/tools/src/vercel/util.ts @@ -12,20 +12,19 @@ export type { ProfileMarkdownData, } from "../shared" -// Provider v2 does not export V3 names, so keep the public declaration on the -// common V2 surface and structurally accept V3 models at the wrapper boundary. -type LanguageModelV3Compat = Omit< +// Provider v2 has no V3/V4 names, so newer models (AI SDK 6 = v3, AI SDK 7 = v4) are matched by shape. +type LanguageModelNextCompat = Omit< LanguageModelV2, "specificationVersion" | "doGenerate" | "doStream" > & { - readonly specificationVersion: "v3" + readonly specificationVersion: "v3" | "v4" // biome-ignore lint/suspicious/noExplicitAny: Bridges mutually exclusive provider major declarations. doGenerate(...args: any[]): PromiseLike // biome-ignore lint/suspicious/noExplicitAny: Bridges mutually exclusive provider major declarations. doStream(...args: any[]): PromiseLike } -export type LanguageModel = LanguageModelV2 | LanguageModelV3Compat +export type LanguageModel = LanguageModelV2 | LanguageModelNextCompat export type LanguageModelCallOptions = LanguageModelV2CallOptions export type LanguageModelMessage = LanguageModelV2Message export type LanguageModelStreamPart = LanguageModelV2StreamPart diff --git a/packages/tools/test/ai-sdk-compat/run.ts b/packages/tools/test/ai-sdk-compat/run.ts new file mode 100644 index 00000000..b46108a0 --- /dev/null +++ b/packages/tools/test/ai-sdk-compat/run.ts @@ -0,0 +1,153 @@ +// Packs the built package and installs it next to each supported AI SDK major, then typechecks and runs a consumer app. +import { mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { $ } from "bun" + +const ROOT = path.resolve(import.meta.dir, "../..") +const FINISHED = { unified: "stop", raw: "stop" } +const TOOL_CALLS = { unified: "tool-calls", raw: "tool_calls" } +const USAGE = { + inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 }, + outputTokens: { total: 1, text: 1, reasoning: 0 }, +} + +const VERSIONS = [ + { + ai: "5", + mock: "MockLanguageModelV2", + stop: "stop", + toolCalls: "tool-calls", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }, + { + ai: "6", + mock: "MockLanguageModelV3", + stop: FINISHED, + toolCalls: TOOL_CALLS, + usage: USAGE, + }, + { + ai: "7", + mock: "MockLanguageModelV4", + stop: FINISHED, + toolCalls: TOOL_CALLS, + usage: USAGE, + }, +] + +const fixture = (v: (typeof VERSIONS)[number]) => ` +import { generateText, stepCountIs, streamText } from "ai" +import { ${v.mock} } from "ai/test" +import { supermemoryTools, withSupermemory } from "@supermemory/tools/ai-sdk" + +let searchCalls = 0 +const server = Bun.serve({ + port: 0, + fetch(req) { + const route = new URL(req.url).pathname + if (route.endsWith("/profile")) + return Response.json({ profile: { static: [{ id: "s0", memory: "User prefers dark mode" }], dynamic: [], buckets: {} } }) + if (route.endsWith("/search")) { + searchCalls++ + return Response.json({ results: [], searchTime: 1 }) + } + return Response.json({ id: "doc_1", status: "queued" }) + }, +}) +const baseUrl = \`http://127.0.0.1:\${server.port}\` + +const prompts: string[] = [] +let calls = 0 +const model = new ${v.mock}({ + doGenerate: async (options) => { + prompts.push(JSON.stringify(options.prompt)) + const callTool = calls++ === 0 && (options.tools?.length ?? 0) > 0 + return { + content: callTool + ? [{ type: "tool-call", toolCallId: "c1", toolName: "searchMemories", input: JSON.stringify({ informationToGet: "theme" }) }] + : [{ type: "text", text: "ok" }], + finishReason: callTool ? ${JSON.stringify(v.toolCalls)} : ${JSON.stringify(v.stop)}, + usage: ${JSON.stringify(v.usage)}, + warnings: [], + } as never + }, +}) + +const tools = supermemoryTools("sm_test", { baseUrl }) +const withTools = await generateText({ model, tools, prompt: "what theme?", stopWhen: stepCountIs(3) }) +if (withTools.text !== "ok" || searchCalls !== 1) throw new Error(\`tools: text=\${withTools.text} searchCalls=\${searchCalls}\`) + +const wrapped = withSupermemory(model, { apiKey: "sm_test", namespace: "user_1", id: "conv_1", baseUrl }) +const withMemory = await generateText({ model: wrapped, prompt: "hi" }) +if (withMemory.text !== "ok" || !prompts.at(-1)?.includes("User prefers dark mode")) + throw new Error("withSupermemory: memory was not injected into the prompt") + +// Type-only: the wrapped model and tools must also be accepted by streamText. +const _typecheckStream = () => streamText({ model: wrapped, tools, prompt: "hi" }) + +server.stop(true) +console.log("ai@${v.ai}: tools and withSupermemory OK") +` + +const work = await mkdtemp(path.join(tmpdir(), "sm-tools-compat-")) +try { + await $`bun run build`.cwd(ROOT).quiet() + const packed = await $`npm pack --silent --pack-destination ${work}` + .cwd(ROOT) + .text() + const tarball = path.join(work, packed.trim().split("\n").at(-1) ?? "") + + for (const v of VERSIONS) { + const app = path.join(work, `ai-v${v.ai}`) + await Bun.write( + path.join(app, "package.json"), + JSON.stringify({ + type: "module", + dependencies: { + "@supermemory/tools": `file:${tarball}`, + ai: `^${v.ai}`, + typescript: "^5.9.2", + zod: "^4.1.5", + "@types/bun": "^1.2.21", + }, + }), + ) + await Bun.write( + path.join(app, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + target: "ES2022", + module: "ESNext", + moduleResolution: "bundler", + strict: true, + noEmit: true, + skipLibCheck: true, + types: ["bun"], + }, + files: ["app.ts"], + }), + ) + await Bun.write(path.join(app, "app.ts"), fixture(v)) + await $`npm install --silent --no-audit --no-fund`.cwd(app).quiet() + const installed = ( + await Bun.file(path.join(app, "node_modules/ai/package.json")).json() + ).version + const nested = await Bun.file( + path.join( + app, + "node_modules/@supermemory/tools/node_modules/ai/package.json", + ), + ).exists() + if (nested) + throw new Error( + `ai@${v.ai}: a second copy of ai was installed under @supermemory/tools`, + ) + const tsc = await $`npx tsc -p .`.cwd(app).nothrow().quiet() + if (tsc.exitCode !== 0) + throw new Error(`ai@${installed}: typecheck failed\n${tsc.stdout}`) + await $`bun app.ts`.cwd(app) + } +} finally { + await rm(work, { recursive: true, force: true }) +}