From 3b6fc72fb514ce6e4bbca72972b8d873df91ec94 Mon Sep 17 00:00:00 2001 From: Vorflux AI Date: Wed, 15 Apr 2026 19:24:45 +0000 Subject: [PATCH] refactor: replace Object.create with Proxy for model wrapping Addresses review feedback about ES private fields breaking with Object.create + Object.defineProperties approach: Source changes (packages/tools/src/vercel/index.ts): - Replace Object.create/Object.defineProperties with a Proxy that delegates property access to the original model instance. This preserves prototype getters, ES private fields (#field), and instance brand checks since 'this' inside getters remains the real model instance. - Extract doGenerate/doStream overrides into named functions (wrappedDoGenerate/wrappedDoStream) for clarity. - Remove one-use 'descriptors' intermediate variable (inlined). Test changes (packages/tools/test/with-supermemory/unit.test.ts): - Remove duplicate test 'should preserve all V3 metadata after wrapping' (strict subset of prototype getter test). - Add MockLanguageModelV3WithPrivateFields class using true ES private fields (#config) to catch TypeError that would occur with Object.create-based wrapping. - Add test 'should handle V3 models with ES private fields' that verifies the Proxy approach works with private field access. - Clean up 'valid API key' test to be intentionally minimal (property preservation covered by dedicated V2/V3 suites). --- packages/tools/src/vercel/index.ts | 39 ++++++++---- .../tools/test/with-supermemory/unit.test.ts | 63 ++++++++++++++----- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/packages/tools/src/vercel/index.ts b/packages/tools/src/vercel/index.ts index 759d1bce..54bc5dcf 100644 --- a/packages/tools/src/vercel/index.ts +++ b/packages/tools/src/vercel/index.ts @@ -119,18 +119,8 @@ const wrapVercelLanguageModel = ( promptTemplate: options?.promptTemplate, }) - // 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 - - // Copy all own property descriptors (preserves getters/setters on the instance) - const descriptors = Object.getOwnPropertyDescriptors(model) - Object.defineProperties(wrappedModel, descriptors) - - // Override doGenerate and doStream with memory-aware implementations - wrappedModel.doGenerate = async (params: LanguageModelCallOptions) => { + // Memory-aware doGenerate override + const wrappedDoGenerate = async (params: LanguageModelCallOptions) => { try { const transformedParams = await transformParamsWithMemory(params, ctx) @@ -163,7 +153,8 @@ const wrapVercelLanguageModel = ( } } - wrappedModel.doStream = async (params: LanguageModelCallOptions) => { + // Memory-aware doStream override + const wrappedDoStream = async (params: LanguageModelCallOptions) => { let generatedText = "" try { @@ -213,6 +204,28 @@ const wrapVercelLanguageModel = ( } } + // Use a Proxy to delegate all property access to the original model while + // overriding doGenerate and doStream. This preserves prototype getters, + // ES private fields (#field), and instance brand checks — unlike + // Object.create + Object.defineProperties which creates a new object that + // prototype getters would execute against (breaking private field access). + const overrides: Record = { + doGenerate: wrappedDoGenerate, + doStream: wrappedDoStream, + } + + const wrappedModel = new Proxy(model, { + get(target, prop, _receiver) { + if (typeof prop === "string" && prop in overrides) { + return overrides[prop] + } + // Delegate to the original model so that `this` inside prototype + // getters remains the real model instance (preserving private fields). + const value = Reflect.get(target, prop, target) + return value + }, + }) + return wrappedModel } diff --git a/packages/tools/test/with-supermemory/unit.test.ts b/packages/tools/test/with-supermemory/unit.test.ts index 1bf08ee4..4a7695f4 100644 --- a/packages/tools/test/with-supermemory/unit.test.ts +++ b/packages/tools/test/with-supermemory/unit.test.ts @@ -58,6 +58,34 @@ class MockLanguageModelV3 implements LanguageModelV3 { const createMockV3LanguageModel = (): LanguageModelV3 => new MockLanguageModelV3("test-v3-model", { provider: "test-v3-provider" }) +// Mock V3 language model using true ES private fields (#config). +// Real AI SDK 6 model classes may use private fields internally; +// the wrapping approach must handle this without throwing TypeError. +class MockLanguageModelV3WithPrivateFields implements LanguageModelV3 { + readonly specificationVersion = "v3" as const + readonly modelId: string + supportedUrls: Record + #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 createMockV3WithPrivateFields = (): LanguageModelV3 => + new MockLanguageModelV3WithPrivateFields("test-v3-private", { + provider: "test-v3-private-provider", + }) + // Mock profile API response const createMockProfileResponse = ( staticMemories: string[] = [], @@ -109,8 +137,9 @@ describe("Unit: withSupermemory", () => { const mockModel = createMockLanguageModel() const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + // Intentionally minimal — just verifies wrapping succeeds with a valid key. + // Property preservation is tested in the dedicated V2/V3 compatibility suites. expect(wrappedModel).toBeDefined() - expect(wrappedModel.specificationVersion).toBe("v2") }) }) @@ -136,19 +165,6 @@ describe("Unit: withSupermemory", () => { }) }) - 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" @@ -165,6 +181,25 @@ describe("Unit: withSupermemory", () => { expect(typeof wrappedModel.doGenerate).toBe("function") expect(typeof wrappedModel.doStream).toBe("function") }) + + it("should handle V3 models with ES private fields (#config)", () => { + process.env.SUPERMEMORY_API_KEY = "test-key" + + const mockModel = createMockV3WithPrivateFields() + // Verify the original model works (private field access succeeds) + expect(mockModel.provider).toBe("test-v3-private-provider") + + const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag) + + // The wrapped model must not throw TypeError when accessing + // prototype getters that use ES private fields internally + expect(wrappedModel.specificationVersion).toBe("v3") + expect(wrappedModel.provider).toBe("test-v3-private-provider") + expect(wrappedModel.modelId).toBe("test-v3-private") + expect(wrappedModel.supportedUrls).toEqual({ + "image/*": [/^https?:\/\/.*$/], + }) + }) }) describe("V2 backwards compatibility", () => {