Skip to content
Closed
10 changes: 6 additions & 4 deletions packages/tools/src/openai/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type OpenAI from "openai"
import { validateApiKey } from "../shared"
import {
createOpenAIMiddleware,
type OpenAIMiddlewareOptions,
Expand All @@ -21,6 +22,7 @@ import {
* @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.apiKey - Optional Supermemory API key to use instead of the SUPERMEMORY_API_KEY environment variable
*
* @returns An OpenAI client with SuperMemory middleware injected for both Chat Completions and Responses APIs
*
Expand Down Expand Up @@ -56,16 +58,16 @@ import {
* })
* ```
*
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
* @throws {Error} When supermemory API request fails
*/
export function withSupermemory(
openaiClient: OpenAI,
options: OpenAIMiddlewareOptions,
) {
if (!process.env.SUPERMEMORY_API_KEY) {
throw new Error("SUPERMEMORY_API_KEY is not set")
}
// Validated here (rather than only inside the middleware) so a missing key
// fails fast, before any client is wrapped.
validateApiKey(options.apiKey)

if (!options.containerTag) {
throw new Error(
Expand Down
121 changes: 121 additions & 0 deletions packages/tools/src/openai/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

// Mock the Supermemory SDK (same pattern as tool-operations.test.ts) so the
// middleware can be exercised without network access.
vi.mock("supermemory", () => {
return {
default: class MockSupermemory {
add = vi.fn()
search = { execute: vi.fn() }
documents = { add: vi.fn(), delete: vi.fn(), list: vi.fn() }
},
}
})

import { withSupermemory } from "./index"

const OPTIONS_API_KEY = "sm_key_from_options"

/** Minimal stand-in for an OpenAI client — the middleware only reassigns `create`. */
function createFakeOpenAIClient() {
const create = vi.fn().mockResolvedValue({ id: "chatcmpl_1" })
return {
chat: { completions: { create } },
} as never
}

/** Stubs `/v4/profile` and records the requests the middleware makes. */
function mockProfileFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
profile: { static: [{ memory: "User likes TypeScript" }], dynamic: [] },
searchResults: { results: [] },
}),
})
vi.stubGlobal("fetch", fetchMock)
return fetchMock
}

let originalEnvKey: string | undefined

beforeEach(() => {
originalEnvKey = process.env.SUPERMEMORY_API_KEY
delete process.env.SUPERMEMORY_API_KEY
})

afterEach(() => {
if (originalEnvKey === undefined) {
delete process.env.SUPERMEMORY_API_KEY
} else {
process.env.SUPERMEMORY_API_KEY = originalEnvKey
}
vi.unstubAllGlobals()
})

describe("withSupermemory (openai) API key", () => {
it("accepts an API key from options when the env var is not set", () => {
mockProfileFetch()

expect(() =>
withSupermemory(createFakeOpenAIClient(), {
containerTag: "user-123",
customId: "conversation-456",
apiKey: OPTIONS_API_KEY,
}),
).not.toThrow()
})

it("authenticates the profile request with the API key from options", async () => {
const fetchMock = mockProfileFetch()

const client = withSupermemory(createFakeOpenAIClient(), {
containerTag: "user-123",
customId: "conversation-456",
apiKey: OPTIONS_API_KEY,
addMemory: "never",
})

await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "what do I like?" }],
})

expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toContain("/v4/profile")
expect((init.headers as Record<string, string>).Authorization).toBe(
`Bearer ${OPTIONS_API_KEY}`,
)
})

it("falls back to the SUPERMEMORY_API_KEY env var", async () => {
const fetchMock = mockProfileFetch()
process.env.SUPERMEMORY_API_KEY = "sm_key_from_env"

const client = withSupermemory(createFakeOpenAIClient(), {
containerTag: "user-123",
customId: "conversation-456",
addMemory: "never",
})

await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: "what do I like?" }],
})

const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect((init.headers as Record<string, string>).Authorization).toBe(
"Bearer sm_key_from_env",
)
})

it("throws when neither options.apiKey nor the env var is set", () => {
expect(() =>
withSupermemory(createFakeOpenAIClient(), {
containerTag: "user-123",
customId: "conversation-456",
}),
).toThrow("SUPERMEMORY_API_KEY is not set")
})
})
31 changes: 23 additions & 8 deletions packages/tools/src/openai/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type OpenAI from "openai"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import { validateApiKey } from "../shared"
import { deduplicateMemoriesForMode } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"
Expand All @@ -20,6 +21,8 @@ export interface OpenAIMiddlewareOptions {
mode?: "profile" | "query" | "full"
addMemory?: "always" | "never"
baseUrl?: string
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
apiKey?: string
}

interface SupermemoryProfileSearch {
Expand Down Expand Up @@ -75,22 +78,25 @@ const getLastUserMessage = (
*
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID)
* @param queryText - Optional query text to search for specific memories. If empty, returns all profile memories
* @param baseUrl - The Supermemory API base URL
* @param apiKey - The Supermemory API key used to authenticate the request
* @returns Promise that resolves to the SuperMemory profile search response
* @throws {Error} When the API request fails or returns an error status
*
* @example
* ```typescript
* // Search with query
* const results = await supermemoryProfileSearch("user-123", "favorite programming language")
* const results = await supermemoryProfileSearch("user-123", "favorite programming language", baseUrl, apiKey)
*
* // Get all profile memories
* const profile = await supermemoryProfileSearch("user-123", "")
* const profile = await supermemoryProfileSearch("user-123", "", baseUrl, apiKey)
* ```
*/
const supermemoryProfileSearch = async (
containerTag: string,
queryText: string,
baseUrl: string,
apiKey: string,
): Promise<SupermemoryProfileSearch> => {
const payload = queryText
? JSON.stringify({
Expand All @@ -106,7 +112,7 @@ const supermemoryProfileSearch = async (
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
Authorization: `Bearer ${apiKey}`,
},
body: payload,
})
Expand Down Expand Up @@ -138,6 +144,8 @@ const supermemoryProfileSearch = async (
* @param containerTag - The container tag/identifier for memory search
* @param logger - Logger instance for debugging and info output
* @param mode - Memory search mode: "profile" (all memories), "query" (search-based), or "full" (both)
* @param baseUrl - The Supermemory API base URL
* @param apiKey - The Supermemory API key used to authenticate the request
* @returns Promise that resolves to enhanced messages with memory-injected system prompt
*
* @example
Expand All @@ -150,7 +158,9 @@ const supermemoryProfileSearch = async (
* messages,
* "user-123",
* logger,
* "full"
* "full",
* baseUrl,
* apiKey
* )
* // Returns messages with system prompt containing relevant memories
* ```
Expand All @@ -161,6 +171,7 @@ const addSystemPrompt = async (
logger: Logger,
mode: "profile" | "query" | "full",
baseUrl: string,
apiKey: string,
) => {
const systemPromptExists = messages.some((msg) => msg.role === "system")

Expand All @@ -170,6 +181,7 @@ const addSystemPrompt = async (
containerTag,
queryText,
baseUrl,
apiKey,
)

const memoryCountStatic = memoriesResponse.profile.static?.length || 0
Expand Down Expand Up @@ -400,8 +412,9 @@ 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: "always")
* @param options.apiKey - Supermemory API key to use instead of the SUPERMEMORY_API_KEY environment variable
* @returns Object with `wrapClient` and `createClient` methods
* @throws {Error} When SUPERMEMORY_API_KEY environment variable is not set
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
*
* @example
* ```typescript
Expand All @@ -421,8 +434,9 @@ export function createOpenAIMiddleware(
) {
const logger = createLogger(options?.verbose ?? false)
const baseUrl = normalizeBaseUrl(options?.baseUrl)
const apiKey = validateApiKey(options?.apiKey)
const client = new Supermemory({
apiKey: process.env.SUPERMEMORY_API_KEY,
apiKey,
...(baseUrl !== "https://api.supermemory.ai" ? { baseURL: baseUrl } : {}),
})

Expand Down Expand Up @@ -457,6 +471,7 @@ export function createOpenAIMiddleware(
containerTag,
queryText,
baseUrl,
apiKey,
)

const memoryCountStatic = memoriesResponse.profile.static?.length || 0
Expand Down Expand Up @@ -615,15 +630,15 @@ export function createOpenAIMiddleware(
memoryCustomId,
logger,
messages,
process.env.SUPERMEMORY_API_KEY,
apiKey,
baseUrl,
),
)
}
}

operations.push(
addSystemPrompt(messages, containerTag, logger, mode, baseUrl),
addSystemPrompt(messages, containerTag, logger, mode, baseUrl, apiKey),
)

const results = await Promise.all(operations)
Expand Down
Loading