fix(tools): repair openai middleware drift

This commit is contained in:
Vorflux AI 2026-07-18 04:26:21 +00:00
parent 86854efee6
commit d159beb72c
6 changed files with 374 additions and 78 deletions

View 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.

View file

@ -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

View file

@ -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",

View file

@ -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,

View file

@ -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,

View 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")
})
})