From d5cad5502c2bea69c979eee42cc20b48e682bc0d Mon Sep 17 00:00:00 2001 From: Vorflux AI Date: Wed, 15 Apr 2026 19:12:23 +0000 Subject: [PATCH] fix: preserve prototype properties when wrapping language models for AI SDK 6 compatibility Replace object spread with Object.create + Object.defineProperties to preserve prototype getters (e.g. `provider`) that AI SDK v4/v5/v6 models define on their class prototype rather than as own properties. The spread operator `{ ...model }` only copies own enumerable properties, which causes prototype getters like `provider` to become `undefined` on the wrapped model. This triggers AI_UnsupportedModelVersionError at runtime with AI SDK 6. The fix: 1. Creates the wrapper via Object.create(Object.getPrototypeOf(model)) to inherit all prototype properties (getters, methods) 2. Copies all own property descriptors via Object.getOwnPropertyDescriptors 3. Overrides doGenerate/doStream on the instance to shadow the inherited ones Tested with real OpenAI API calls against AI SDK v4, v5, and v6. Closes #852 --- packages/tools/src/vercel/index.ts | 171 +++++++++--------- .../tools/test/with-supermemory/unit.test.ts | 95 +++++++++- 2 files changed, 181 insertions(+), 85 deletions(-) diff --git a/packages/tools/src/vercel/index.ts b/packages/tools/src/vercel/index.ts index beeef093..759d1bce 100644 --- a/packages/tools/src/vercel/index.ts +++ b/packages/tools/src/vercel/index.ts @@ -119,96 +119,99 @@ const wrapVercelLanguageModel = ( promptTemplate: options?.promptTemplate, }) - const wrappedModel = { - ...model, + // Use Object.create to preserve prototype getters (e.g. `provider` in V3 models) + // instead of object spread which only copies own enumerable properties. + const wrappedModel = Object.create( + Object.getPrototypeOf(model) ?? Object.prototype, + ) as T - doGenerate: async (params: LanguageModelCallOptions) => { - try { - const transformedParams = await transformParamsWithMemory(params, ctx) + // Copy all own property descriptors (preserves getters/setters on the instance) + const descriptors = Object.getOwnPropertyDescriptors(model) + Object.defineProperties(wrappedModel, descriptors) - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 - const result = await model.doGenerate(transformedParams as any) + // Override doGenerate and doStream with memory-aware implementations + wrappedModel.doGenerate = async (params: LanguageModelCallOptions) => { + try { + const transformedParams = await transformParamsWithMemory(params, ctx) - const userMessage = getLastUserMessage(params) - if (ctx.addMemory === "always" && userMessage && userMessage.trim()) { - const assistantResponseText = extractAssistantResponseText( - result.content as unknown[], - ) - saveMemoryAfterResponse( - ctx.client, - ctx.containerTag, - ctx.conversationId, - assistantResponseText, - params, - ctx.logger, - ctx.apiKey, - ctx.normalizedBaseUrl, - ) - } + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + const result = await model.doGenerate(transformedParams as any) - return result - } catch (error) { - ctx.logger.error("Error generating response", { - error: error instanceof Error ? error.message : "Unknown error", - }) - throw error - } - }, - - doStream: async (params: LanguageModelCallOptions) => { - let generatedText = "" - - try { - const transformedParams = await transformParamsWithMemory(params, ctx) - - const { stream, ...rest } = await model.doStream( - // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 - transformedParams as any, + const userMessage = getLastUserMessage(params) + if (ctx.addMemory === "always" && userMessage && userMessage.trim()) { + const assistantResponseText = extractAssistantResponseText( + result.content as unknown[], + ) + saveMemoryAfterResponse( + ctx.client, + ctx.containerTag, + ctx.conversationId, + assistantResponseText, + params, + ctx.logger, + ctx.apiKey, + ctx.normalizedBaseUrl, ) - - const transformStream = new TransformStream< - LanguageModelStreamPart, - LanguageModelStreamPart - >({ - transform(chunk, controller) { - if (chunk.type === "text-delta") { - generatedText += chunk.delta - } - controller.enqueue(chunk) - }, - flush: async () => { - const userMessage = getLastUserMessage(params) - if ( - ctx.addMemory === "always" && - userMessage && - userMessage.trim() - ) { - saveMemoryAfterResponse( - ctx.client, - ctx.containerTag, - ctx.conversationId, - generatedText, - params, - ctx.logger, - ctx.apiKey, - ctx.normalizedBaseUrl, - ) - } - }, - }) - - return { - stream: stream.pipeThrough(transformStream), - ...rest, - } - } catch (error) { - ctx.logger.error("Error streaming response", { - error: error instanceof Error ? error.message : "Unknown error", - }) - throw error } - }, - } as T + + return result + } catch (error) { + ctx.logger.error("Error generating response", { + error: error instanceof Error ? error.message : "Unknown error", + }) + throw error + } + } + + wrappedModel.doStream = async (params: LanguageModelCallOptions) => { + let generatedText = "" + + try { + const transformedParams = await transformParamsWithMemory(params, ctx) + + const { stream, ...rest } = await model.doStream( + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + transformedParams as any, + ) + + const transformStream = new TransformStream< + LanguageModelStreamPart, + LanguageModelStreamPart + >({ + transform(chunk, controller) { + if (chunk.type === "text-delta") { + generatedText += chunk.delta + } + controller.enqueue(chunk) + }, + flush: async () => { + const userMessage = getLastUserMessage(params) + if (ctx.addMemory === "always" && userMessage && userMessage.trim()) { + saveMemoryAfterResponse( + ctx.client, + ctx.containerTag, + ctx.conversationId, + generatedText, + params, + ctx.logger, + ctx.apiKey, + ctx.normalizedBaseUrl, + ) + } + }, + }) + + return { + stream: stream.pipeThrough(transformStream), + ...rest, + } + } catch (error) { + ctx.logger.error("Error streaming response", { + error: error instanceof Error ? error.message : "Unknown error", + }) + throw error + } + } return wrappedModel } diff --git a/packages/tools/test/with-supermemory/unit.test.ts b/packages/tools/test/with-supermemory/unit.test.ts index b20eb6f2..1bf08ee4 100644 --- a/packages/tools/test/with-supermemory/unit.test.ts +++ b/packages/tools/test/with-supermemory/unit.test.ts @@ -12,6 +12,7 @@ import type { LanguageModelV2, LanguageModelV2CallOptions, LanguageModelV2Message, + LanguageModelV3, } from "@ai-sdk/provider" import "dotenv/config" @@ -22,7 +23,7 @@ const TEST_CONFIG = { containerTag: "test-vercel-wrapper", } -// Mock language model for testing +// Mock language model for testing (V2 - plain object) const createMockLanguageModel = (): LanguageModelV2 => ({ specificationVersion: "v2", provider: "test-provider", @@ -32,6 +33,31 @@ const createMockLanguageModel = (): LanguageModelV2 => ({ doStream: vi.fn(), }) +// Mock V3 language model using a class (simulates real AI SDK 6 models +// where `provider` is a prototype getter, not an own property) +class MockLanguageModelV3 implements LanguageModelV3 { + readonly specificationVersion = "v3" as const + readonly modelId: string + supportedUrls: Record + private config: { provider: string } + + constructor(modelId: string, config: { provider: string }) { + this.modelId = modelId + this.config = config + this.supportedUrls = { "image/*": [/^https?:\/\/.*$/] } + } + + get provider(): string { + return this.config.provider + } + + doGenerate = vi.fn() + doStream = vi.fn() +} + +const createMockV3LanguageModel = (): LanguageModelV3 => + new MockLanguageModelV3("test-v3-model", { provider: "test-v3-provider" }) + // Mock profile API response const createMockProfileResponse = ( staticMemories: string[] = [], @@ -88,6 +114,73 @@ describe("Unit: withSupermemory", () => { }) }) + describe("AI SDK 6 (V3) compatibility", () => { + it("should preserve prototype getter properties on V3 models", () => { + process.env.SUPERMEMORY_API_KEY = "test-key" + + const mockModel = createMockV3LanguageModel() + // Verify the mock has `provider` as a prototype getter, not an own property + expect( + Object.getOwnPropertyDescriptor(mockModel, "provider"), + ).toBeUndefined() + expect(mockModel.provider).toBe("test-v3-provider") + + const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + + expect(wrappedModel).toBeDefined() + expect(wrappedModel.specificationVersion).toBe("v3") + expect(wrappedModel.provider).toBe("test-v3-provider") + expect(wrappedModel.modelId).toBe("test-v3-model") + expect(wrappedModel.supportedUrls).toEqual({ + "image/*": [/^https?:\/\/.*$/], + }) + }) + + it("should preserve all V3 metadata after wrapping", () => { + process.env.SUPERMEMORY_API_KEY = "test-key" + + const mockModel = createMockV3LanguageModel() + const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + + // These are the four properties that AI SDK checks at runtime + expect(wrappedModel.specificationVersion).toBe("v3") + expect(wrappedModel.provider).toBe("test-v3-provider") + expect(wrappedModel.modelId).toBe("test-v3-model") + expect(wrappedModel.supportedUrls).toBeDefined() + }) + + it("should still override doGenerate and doStream on V3 models", () => { + process.env.SUPERMEMORY_API_KEY = "test-key" + + const mockModel = createMockV3LanguageModel() + const originalDoGenerate = mockModel.doGenerate + const originalDoStream = mockModel.doStream + + const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + + // doGenerate and doStream should be overridden (not the same reference) + expect(wrappedModel.doGenerate).not.toBe(originalDoGenerate) + expect(wrappedModel.doStream).not.toBe(originalDoStream) + // But they should still be functions + expect(typeof wrappedModel.doGenerate).toBe("function") + expect(typeof wrappedModel.doStream).toBe("function") + }) + }) + + describe("V2 backwards compatibility", () => { + it("should preserve all properties on V2 plain-object models", () => { + process.env.SUPERMEMORY_API_KEY = "test-key" + + const mockModel = createMockLanguageModel() + const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + + expect(wrappedModel.specificationVersion).toBe("v2") + expect(wrappedModel.provider).toBe("test-provider") + expect(wrappedModel.modelId).toBe("test-model") + expect(wrappedModel.supportedUrls).toEqual({}) + }) + }) + describe("Memory caching", () => { let fetchMock: ReturnType