mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-22 03:01:07 +00:00
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:
parent
d5cad5502c
commit
3b6fc72fb5
2 changed files with 75 additions and 27 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue