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).
This commit is contained in:
Vorflux AI 2026-04-15 19:24:45 +00:00
parent d5cad5502c
commit 3b6fc72fb5
2 changed files with 75 additions and 27 deletions

View file

@ -119,18 +119,8 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
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 = <T extends LanguageModel>(
}
}
wrappedModel.doStream = async (params: LanguageModelCallOptions) => {
// Memory-aware doStream override
const wrappedDoStream = async (params: LanguageModelCallOptions) => {
let generatedText = ""
try {
@ -213,6 +204,28 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
}
}
// 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<string, unknown> = {
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
}

View file

@ -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<string, RegExp[]>
#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", () => {