mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
fix(tools): repair openai middleware drift
This commit is contained in:
parent
86854efee6
commit
d159beb72c
6 changed files with 374 additions and 78 deletions
9
packages/tools/CHANGELOG.md
Normal file
9
packages/tools/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# @supermemory/tools changelog
|
||||
|
||||
## 2.1.2 - 2026-07-18
|
||||
|
||||
- Fix OpenAI middleware to accept programmatic `apiKey` and `baseUrl` options.
|
||||
- Make OpenAI middleware fail open when Supermemory memory retrieval fails for Chat Completions or Responses API calls.
|
||||
- Align OpenAI middleware `addMemory` default to `never` and document explicit `always` opt-in for auto-save.
|
||||
- Preserve query-mode search results that overlap profile memories.
|
||||
- Add focused OpenAI middleware regression coverage.
|
||||
|
|
@ -272,7 +272,7 @@ const openaiWithSupermemory = withSupermemory(openai, {
|
|||
containerTag: "user-123", // Required: identifies the user/container
|
||||
customId: "conversation-456", // Required: groups messages into the same document
|
||||
mode: "full",
|
||||
addMemory: "always", // Default: "always"
|
||||
addMemory: "always", // Default: "never"; set "always" to auto-save
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ const openaiWithSupermemory = withSupermemory(openai, {
|
|||
containerTag: "user-123", // Required: identifies the user/container
|
||||
customId: "conversation-456", // Required: groups messages for contextual memory
|
||||
mode: "full", // "profile" | "query" | "full"
|
||||
addMemory: "always", // "always" (default) | "never"
|
||||
addMemory: "always", // "always" | "never" (default)
|
||||
verbose: true, // Enable detailed logging
|
||||
})
|
||||
```
|
||||
|
|
@ -651,7 +651,7 @@ interface WithSupermemoryOptions {
|
|||
customId: string // Required: groups messages into the same document
|
||||
verbose?: boolean
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never" // Default: "always"
|
||||
addMemory?: "always" | "never" // OpenAI default: "never"; AI SDK default: "always"
|
||||
/** Optional Supermemory API key. Use this in browser environments. */
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
|
|
@ -664,7 +664,7 @@ interface WithSupermemoryOptions {
|
|||
- **customId**: Required. Custom ID to group messages into a single document for contextual memory generation
|
||||
- **verbose**: Enable detailed logging of memory search and injection process (default: false)
|
||||
- **mode**: Memory search mode - "profile" (default), "query", or "full"
|
||||
- **addMemory**: Automatic memory storage mode - "always" (default) or "never"
|
||||
- **addMemory**: Automatic memory storage mode - "always" or "never". OpenAI middleware defaults to "never"; AI SDK middleware defaults to "always".
|
||||
- **skipMemoryOnError**: If memory retrieval fails or hits the internal timeout, continue with the original prompt (default: true)
|
||||
|
||||
## Available Tools
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "Memory tools for AI SDK, OpenAI, Voltagent and Mastra with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import {
|
|||
* @param options.customId - Required. Custom ID to group messages into a single document for contextual memory generation
|
||||
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
|
||||
* @param options.mode - Optional mode for memory search: "profile" (default), "query", or "full"
|
||||
* @param options.addMemory - Optional mode for memory addition: "always" (default), "never"
|
||||
* @param options.addMemory - Optional mode for memory addition: "always", "never" (default)
|
||||
* @param options.apiKey - Optional Supermemory API key to use instead of the environment variable
|
||||
* @param options.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
|
||||
*
|
||||
* @returns An OpenAI client with SuperMemory middleware injected for both Chat Completions and Responses APIs
|
||||
*
|
||||
|
|
@ -56,15 +58,18 @@ import {
|
|||
* })
|
||||
* ```
|
||||
*
|
||||
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
|
||||
* @throws {Error} When supermemory API request fails
|
||||
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
|
||||
*/
|
||||
export function withSupermemory(
|
||||
openaiClient: OpenAI,
|
||||
options: OpenAIMiddlewareOptions,
|
||||
) {
|
||||
if (!process.env.SUPERMEMORY_API_KEY) {
|
||||
throw new Error("SUPERMEMORY_API_KEY is not set")
|
||||
const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
|
||||
)
|
||||
}
|
||||
|
||||
if (!options.containerTag) {
|
||||
|
|
@ -82,13 +87,14 @@ export function withSupermemory(
|
|||
const { containerTag } = options
|
||||
const verbose = options.verbose ?? false
|
||||
const mode = options.mode ?? "profile"
|
||||
const addMemory = options.addMemory ?? "always"
|
||||
const addMemory = options.addMemory ?? "never"
|
||||
|
||||
const openaiWithSupermemory = createOpenAIMiddleware(
|
||||
openaiClient,
|
||||
containerTag,
|
||||
{
|
||||
...options,
|
||||
apiKey,
|
||||
verbose,
|
||||
mode,
|
||||
addMemory,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface OpenAIMiddlewareOptions {
|
|||
verbose?: boolean
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never"
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +33,48 @@ interface SupermemoryProfileSearch {
|
|||
}
|
||||
}
|
||||
|
||||
const formatMemoriesForInjection = (
|
||||
memoriesResponse: SupermemoryProfileSearch,
|
||||
mode: "profile" | "query" | "full",
|
||||
context: "chat" | "responses",
|
||||
) => {
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults:
|
||||
mode === "query" ? [] : memoriesResponse.searchResults?.results,
|
||||
})
|
||||
const searchResultsForPrompt =
|
||||
mode === "query"
|
||||
? deduplicateMemories({
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
}).searchResults
|
||||
: deduplicated.searchResults
|
||||
|
||||
const profileData =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const searchResultsMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's ${context === "chat" ? "recent message" : "input"}: \n${searchResultsForPrompt
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
return {
|
||||
memories: `${profileData}\n${searchResultsMemories}`.trim(),
|
||||
deduplicated,
|
||||
searchResultsForPrompt,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the last user message from an array of chat completion messages.
|
||||
*
|
||||
|
|
@ -91,6 +134,7 @@ const supermemoryProfileSearch = async (
|
|||
containerTag: string,
|
||||
queryText: string,
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<SupermemoryProfileSearch> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
|
|
@ -106,7 +150,7 @@ const supermemoryProfileSearch = async (
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: payload,
|
||||
})
|
||||
|
|
@ -161,6 +205,7 @@ const addSystemPrompt = async (
|
|||
logger: Logger,
|
||||
mode: "profile" | "query" | "full",
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
) => {
|
||||
const systemPromptExists = messages.some((msg) => msg.role === "system")
|
||||
|
||||
|
|
@ -170,6 +215,7 @@ const addSystemPrompt = async (
|
|||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
|
|
@ -184,11 +230,8 @@ const addSystemPrompt = async (
|
|||
mode,
|
||||
})
|
||||
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
})
|
||||
const { memories, deduplicated, searchResultsForPrompt } =
|
||||
formatMemoriesForInjection(memoriesResponse, mode, "chat")
|
||||
|
||||
logger.debug("Memory deduplication completed for chat API", {
|
||||
static: {
|
||||
|
|
@ -201,29 +244,10 @@ const addSystemPrompt = async (
|
|||
},
|
||||
searchResults: {
|
||||
original: memoriesResponse.searchResults?.results?.length,
|
||||
deduplicated: deduplicated.searchResults.length,
|
||||
deduplicated: searchResultsForPrompt.length,
|
||||
},
|
||||
})
|
||||
|
||||
const profileData =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const searchResultsMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's recent message: \n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
const memories = `${profileData}\n${searchResultsMemories}`.trim()
|
||||
|
||||
if (memories) {
|
||||
logger.debug("Memory content preview for chat API", {
|
||||
content: memories,
|
||||
|
|
@ -400,6 +424,8 @@ const addMemoryTool = async (
|
|||
* @param options.verbose - Enable detailed logging of memory operations (default: false)
|
||||
* @param options.mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both) (default: "profile")
|
||||
* @param options.addMemory - Automatic memory storage mode: "always" or "never" (default: "never")
|
||||
* @param options.apiKey - Optional Supermemory API key to use instead of the environment variable
|
||||
* @param options.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
|
||||
* @returns Object with `wrapClient` and `createClient` methods
|
||||
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
|
||||
*
|
||||
|
|
@ -420,15 +446,23 @@ export function createOpenAIMiddleware(
|
|||
options?: OpenAIMiddlewareOptions,
|
||||
) {
|
||||
const logger = createLogger(options?.verbose ?? false)
|
||||
const apiKey = options?.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
|
||||
)
|
||||
}
|
||||
|
||||
const baseUrl = normalizeBaseUrl(options?.baseUrl)
|
||||
const client = new Supermemory({
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY,
|
||||
apiKey,
|
||||
...(baseUrl !== "https://api.supermemory.ai" ? { baseURL: baseUrl } : {}),
|
||||
})
|
||||
|
||||
const customId = options?.customId
|
||||
const mode = options?.mode ?? "profile"
|
||||
const addMemory = options?.addMemory ?? "always"
|
||||
const addMemory = options?.addMemory ?? "never"
|
||||
|
||||
const originalCreate = openaiClient.chat.completions.create
|
||||
const originalResponsesCreate = openaiClient.responses?.create
|
||||
|
|
@ -457,6 +491,7 @@ export function createOpenAIMiddleware(
|
|||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
|
|
@ -471,11 +506,8 @@ export function createOpenAIMiddleware(
|
|||
mode,
|
||||
})
|
||||
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
})
|
||||
const { memories, deduplicated, searchResultsForPrompt } =
|
||||
formatMemoriesForInjection(memoriesResponse, mode, context)
|
||||
|
||||
logger.debug(`Memory deduplication completed for ${context} API`, {
|
||||
static: {
|
||||
|
|
@ -488,29 +520,10 @@ export function createOpenAIMiddleware(
|
|||
},
|
||||
searchResults: {
|
||||
original: memoriesResponse.searchResults?.results?.length,
|
||||
deduplicated: deduplicated.searchResults.length,
|
||||
deduplicated: searchResultsForPrompt.length,
|
||||
},
|
||||
})
|
||||
|
||||
const profileData =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const searchResultsMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's ${context === "chat" ? "recent message" : "input"}: \n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
const memories = `${profileData}\n${searchResultsMemories}`.trim()
|
||||
|
||||
if (memories) {
|
||||
logger.debug(`Memory content preview for ${context} API`, {
|
||||
content: memories,
|
||||
|
|
@ -565,17 +578,25 @@ export function createOpenAIMiddleware(
|
|||
),
|
||||
)
|
||||
|
||||
const results = await Promise.all(operations)
|
||||
const memories = results[results.length - 1] // Memory search result is always last
|
||||
try {
|
||||
const results = await Promise.all(operations)
|
||||
const memories = results[results.length - 1]
|
||||
if (memories) {
|
||||
return originalResponsesCreate.call(openaiClient.responses, {
|
||||
...params,
|
||||
instructions: `${params.instructions || ""}\n\n${memories}`.trim(),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"Supermemory retrieval failed; continuing without injected memories",
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const enhancedInstructions = memories
|
||||
? `${params.instructions || ""}\n\n${memories}`.trim()
|
||||
: params.instructions
|
||||
|
||||
return originalResponsesCreate.call(openaiClient.responses, {
|
||||
...params,
|
||||
instructions: enhancedInstructions,
|
||||
})
|
||||
return originalResponsesCreate.call(openaiClient.responses, params)
|
||||
}
|
||||
|
||||
const createWithMemory = async (
|
||||
|
|
@ -615,7 +636,7 @@ export function createOpenAIMiddleware(
|
|||
memoryCustomId,
|
||||
logger,
|
||||
messages,
|
||||
process.env.SUPERMEMORY_API_KEY,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
),
|
||||
)
|
||||
|
|
@ -623,11 +644,23 @@ export function createOpenAIMiddleware(
|
|||
}
|
||||
|
||||
operations.push(
|
||||
addSystemPrompt(messages, containerTag, logger, mode, baseUrl),
|
||||
addSystemPrompt(messages, containerTag, logger, mode, baseUrl, apiKey),
|
||||
)
|
||||
|
||||
const results = await Promise.all(operations)
|
||||
const enhancedMessages = results[results.length - 1] // Enhanced messages result is always last
|
||||
let enhancedMessages = messages
|
||||
try {
|
||||
const results = await Promise.all(operations)
|
||||
enhancedMessages = results[
|
||||
results.length - 1
|
||||
] as OpenAI.Chat.Completions.ChatCompletionMessageParam[]
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"Supermemory retrieval failed; continuing without injected memories",
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return originalCreate.call(openaiClient.chat.completions, {
|
||||
...params,
|
||||
|
|
|
|||
248
packages/tools/test/openai-middleware.unit.test.ts
Normal file
248
packages/tools/test/openai-middleware.unit.test.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import type OpenAI from "openai"
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
type Mock,
|
||||
} from "vitest"
|
||||
import { withSupermemory } from "../src/openai"
|
||||
|
||||
const originalEnv = process.env.SUPERMEMORY_API_KEY
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
const createMockProfileResponse = ({
|
||||
staticMemories = [],
|
||||
dynamicMemories = [],
|
||||
searchResults = [],
|
||||
}: {
|
||||
staticMemories?: string[]
|
||||
dynamicMemories?: string[]
|
||||
searchResults?: string[]
|
||||
} = {}) => ({
|
||||
profile: {
|
||||
static: staticMemories.map((memory) => ({ memory })),
|
||||
dynamic: dynamicMemories.map((memory) => ({ memory })),
|
||||
},
|
||||
searchResults: {
|
||||
results: searchResults.map((memory) => ({ memory })),
|
||||
},
|
||||
})
|
||||
|
||||
const createMockOpenAIClient = () =>
|
||||
({
|
||||
chat: {
|
||||
completions: {
|
||||
create: vi.fn(async (params) => ({ params })),
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
create: vi.fn(async (params) => ({ params })),
|
||||
},
|
||||
}) as unknown as OpenAI & {
|
||||
chat: { completions: { create: Mock } }
|
||||
responses: { create: Mock }
|
||||
}
|
||||
|
||||
describe("OpenAI withSupermemory middleware", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.SUPERMEMORY_API_KEY = originalEnv
|
||||
} else {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it("uses programmatic apiKey and baseUrl without requiring env auth", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
Response.json(
|
||||
createMockProfileResponse({ searchResults: ["Custom API memory"] }),
|
||||
),
|
||||
)
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
const originalCreate = client.chat.completions.create
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
baseUrl: "https://api.example.com/",
|
||||
})
|
||||
|
||||
await wrapped.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "what do I prefer?" }],
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, init] = fetchMock.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
]
|
||||
expect(url).toBe("https://api.example.com/v4/profile")
|
||||
expect(init?.headers).toMatchObject({
|
||||
Authorization: "Bearer programmatic-key",
|
||||
})
|
||||
expect(originalCreate).toHaveBeenCalledTimes(1)
|
||||
const enhancedParams = originalCreate.mock.calls[0]?.[0]
|
||||
expect(enhancedParams.messages[0]).toMatchObject({
|
||||
role: "system",
|
||||
content: expect.stringContaining("Custom API memory"),
|
||||
})
|
||||
})
|
||||
|
||||
it("uses programmatic apiKey and baseUrl for Responses API", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
Response.json(
|
||||
createMockProfileResponse({ searchResults: ["Responses API memory"] }),
|
||||
),
|
||||
)
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
const originalResponsesCreate = client.responses.create
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
baseUrl: "https://api.example.com/",
|
||||
})
|
||||
|
||||
await wrapped.responses.create({
|
||||
model: "gpt-4o-mini",
|
||||
instructions: "Be helpful",
|
||||
input: "what do I prefer?",
|
||||
})
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
]
|
||||
expect(url).toBe("https://api.example.com/v4/profile")
|
||||
expect(init?.headers).toMatchObject({
|
||||
Authorization: "Bearer programmatic-key",
|
||||
})
|
||||
expect(originalResponsesCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
instructions: expect.stringContaining("Responses API memory"),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("defaults addMemory to never", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
Response.json(
|
||||
createMockProfileResponse({ searchResults: ["Retrieved only"] }),
|
||||
),
|
||||
)
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
})
|
||||
|
||||
await wrapped.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const firstFetchCall = fetchMock.mock.calls[0] as unknown[] | undefined
|
||||
expect(String(firstFetchCall?.[0])).toContain("/v4/profile")
|
||||
})
|
||||
|
||||
it("fails open when profile retrieval fails", async () => {
|
||||
const fetchMock = vi.fn(async () => new Response("down", { status: 503 }))
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
const originalCreate = client.chat.completions.create
|
||||
const originalParams = {
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user" as const, content: "hello" }],
|
||||
}
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
})
|
||||
|
||||
await expect(
|
||||
wrapped.chat.completions.create(originalParams),
|
||||
).resolves.toEqual({
|
||||
params: originalParams,
|
||||
})
|
||||
expect(originalCreate).toHaveBeenCalledWith(originalParams)
|
||||
})
|
||||
|
||||
it("fails open for Responses API when profile retrieval fails", async () => {
|
||||
globalThis.fetch = vi.fn(
|
||||
async () => new Response("down", { status: 503 }),
|
||||
) as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
const originalResponsesCreate = client.responses.create
|
||||
const originalParams = {
|
||||
model: "gpt-4o-mini",
|
||||
instructions: "Be helpful",
|
||||
input: "hello",
|
||||
}
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
})
|
||||
|
||||
await expect(wrapped.responses.create(originalParams)).resolves.toEqual({
|
||||
params: originalParams,
|
||||
})
|
||||
expect(originalResponsesCreate).toHaveBeenCalledWith(originalParams)
|
||||
})
|
||||
|
||||
it("keeps query search hits that overlap profile memories", async () => {
|
||||
globalThis.fetch = vi.fn(async () =>
|
||||
Response.json(
|
||||
createMockProfileResponse({
|
||||
staticMemories: ["User likes TypeScript"],
|
||||
searchResults: ["User likes TypeScript", "User uses Bun"],
|
||||
}),
|
||||
),
|
||||
) as unknown as typeof fetch
|
||||
const client = createMockOpenAIClient()
|
||||
const originalCreate = client.chat.completions.create
|
||||
|
||||
const wrapped = withSupermemory(client, {
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "query",
|
||||
apiKey: "programmatic-key",
|
||||
})
|
||||
|
||||
await wrapped.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: "what language do I like?" }],
|
||||
})
|
||||
|
||||
const enhancedParams = originalCreate.mock.calls[0]?.[0]
|
||||
const systemPrompt = enhancedParams.messages[0].content
|
||||
expect(systemPrompt).toContain("- User likes TypeScript")
|
||||
expect(systemPrompt).toContain("- User uses Bun")
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue