mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-20 05:06:31 +00:00
feat(openai): add support for API key in middleware options and validation
This commit is contained in:
parent
e5e4e498f2
commit
83e1fc1034
3 changed files with 150 additions and 12 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type OpenAI from "openai"
|
||||
import { validateApiKey } from "../shared"
|
||||
import {
|
||||
createOpenAIMiddleware,
|
||||
type OpenAIMiddlewareOptions,
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
121
packages/tools/src/openai/middleware.test.ts
Normal file
121
packages/tools/src/openai/middleware.test.ts
Normal file
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type OpenAI from "openai"
|
||||
import Supermemory from "supermemory"
|
||||
import { addConversation } from "../conversations-client"
|
||||
import { validateApiKey } from "../shared"
|
||||
import { deduplicateMemories } from "../tools-shared"
|
||||
import { createLogger, type Logger } from "../vercel/logger"
|
||||
import { convertProfileToMarkdown } from "../vercel/util"
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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({
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -150,7 +158,9 @@ const supermemoryProfileSearch = async (
|
|||
* messages,
|
||||
* "user-123",
|
||||
* logger,
|
||||
* "full"
|
||||
* "full",
|
||||
* baseUrl,
|
||||
* apiKey
|
||||
* )
|
||||
* // Returns messages with system prompt containing relevant memories
|
||||
* ```
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -170,6 +181,7 @@ const addSystemPrompt = async (
|
|||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
|
|
@ -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: "never")
|
||||
* @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
|
||||
|
|
@ -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 } : {}),
|
||||
})
|
||||
|
||||
|
|
@ -457,6 +471,7 @@ export function createOpenAIMiddleware(
|
|||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
|
|
@ -615,7 +630,7 @@ export function createOpenAIMiddleware(
|
|||
memoryCustomId,
|
||||
logger,
|
||||
messages,
|
||||
process.env.SUPERMEMORY_API_KEY,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
),
|
||||
)
|
||||
|
|
@ -623,7 +638,7 @@ export function createOpenAIMiddleware(
|
|||
}
|
||||
|
||||
operations.push(
|
||||
addSystemPrompt(messages, containerTag, logger, mode, baseUrl),
|
||||
addSystemPrompt(messages, containerTag, logger, mode, baseUrl, apiKey),
|
||||
)
|
||||
|
||||
const results = await Promise.all(operations)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue