From 82ef13e720dd78f9b16ff0dad612477f3bc430a4 Mon Sep 17 00:00:00 2001 From: abhay-codes07 Date: Mon, 13 Jul 2026 15:43:06 +0530 Subject: [PATCH] fix(tools): route Responses API memory saves through /v4/conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a customId is configured, the chat-completions path hands addMemoryTool the messages plus API credentials, so the save routes through /v4/conversations with the internal "conversation:" prefix stripped back off. The Responses path built the same prefixed memoryCustomId but passed neither messages nor credentials, so the conversation branch never ran and the save fell through to client.add() with "conversation:" as the stored customId — a value customId validation rejects (alphanumeric/hyphen/underscore only). The error is swallowed by design (memory saves must not break the user's API call), which means every Responses API call with a customId silently saved nothing. Hand the input over as a single user message together with the credentials, mirroring the chat path, so conversation grouping works for the Responses API too. Also strip the internal prefix in the client.add fallback so the routing marker can never leak into a stored customId on any path (previously the chat path had the same leak whenever SUPERMEMORY_API_KEY was unset). Covered with mocked-SDK middleware tests: a customId save routes through addConversation with the un-prefixed conversation id and never touches client.add; the fallback stores the user's configured id, not the prefixed marker; and the no-customId path is unchanged. Two of the three tests fail against the previous implementation. --- packages/tools/src/openai/middleware.test.ts | 121 +++++++++++++++++++ packages/tools/src/openai/middleware.ts | 25 +++- 2 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 packages/tools/src/openai/middleware.test.ts diff --git a/packages/tools/src/openai/middleware.test.ts b/packages/tools/src/openai/middleware.test.ts new file mode 100644 index 000000000..6efbd8524 --- /dev/null +++ b/packages/tools/src/openai/middleware.test.ts @@ -0,0 +1,121 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +// Mock the Supermemory SDK and the conversations client so the middleware's +// memory-save routing can be asserted without network access. +const { clientAddMock, addConversationMock } = vi.hoisted(() => ({ + clientAddMock: vi.fn(), + addConversationMock: vi.fn(), +})) + +vi.mock("supermemory", () => ({ + default: class MockSupermemory { + add = clientAddMock + }, +})) + +vi.mock("../conversations-client", () => ({ + addConversation: addConversationMock, +})) + +import { createOpenAIMiddleware } from "./middleware" + +const EMPTY_PROFILE = { + profile: { static: [], dynamic: [] }, + searchResults: { results: [] }, +} + +function makeFakeOpenAI() { + return { + chat: { + completions: { + create: vi.fn().mockResolvedValue({ id: "chatcmpl-1" }), + }, + }, + responses: { + create: vi.fn().mockResolvedValue({ id: "resp-1" }), + }, + } +} + +describe("createOpenAIMiddleware Responses API memory save", () => { + beforeEach(() => { + clientAddMock.mockReset().mockResolvedValue({ id: "mem-1" }) + addConversationMock.mockReset().mockResolvedValue({ id: "conv-doc-1" }) + process.env.SUPERMEMORY_API_KEY = "sm_test_key" + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + json: async () => EMPTY_PROFILE, + })), + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it("routes a customId save through /v4/conversations like the chat path", async () => { + const openai = makeFakeOpenAI() + // biome-ignore lint/suspicious/noExplicitAny: minimal OpenAI stub for the wrapped surface + createOpenAIMiddleware(openai as any, "user-1", { + containerTag: "user-1", + customId: "conv-1", + addMemory: "always", + mode: "query", + }) + + await openai.responses.create({ input: "I love hiking", instructions: "" }) + + expect(addConversationMock).toHaveBeenCalledTimes(1) + const payload = addConversationMock.mock.calls[0]?.[0] + expect(payload.conversationId).toBe("conv-1") + expect(payload.messages).toEqual([ + { role: "user", content: "I love hiking" }, + ]) + // The raw client.add fallback (which would have stored the internal + // "conversation:" prefix as the customId) must not fire. + expect(clientAddMock).not.toHaveBeenCalled() + }) + + it("never stores the internal conversation: prefix in the add fallback", async () => { + const openai = makeFakeOpenAI() + // biome-ignore lint/suspicious/noExplicitAny: minimal OpenAI stub for the wrapped surface + createOpenAIMiddleware(openai as any, "user-1", { + containerTag: "user-1", + customId: "conv-1", + addMemory: "always", + mode: "query", + }) + + // Without an API key the conversation branch cannot run, so the save + // falls through to client.add — the stored customId must be the + // user's configured id, not the prefixed routing marker. + delete process.env.SUPERMEMORY_API_KEY + + await openai.responses.create({ input: "I love hiking", instructions: "" }) + + expect(addConversationMock).not.toHaveBeenCalled() + expect(clientAddMock).toHaveBeenCalledTimes(1) + expect(clientAddMock.mock.calls[0]?.[0]?.customId).toBe("conv-1") + }) + + it("saves without a customId via plain client.add", async () => { + const openai = makeFakeOpenAI() + // biome-ignore lint/suspicious/noExplicitAny: minimal OpenAI stub for the wrapped surface + createOpenAIMiddleware(openai as any, "user-1", { + containerTag: "user-1", + addMemory: "always", + mode: "query", + // biome-ignore lint/suspicious/noExplicitAny: exercising the optional-customId path + } as any) + + await openai.responses.create({ input: "I love hiking", instructions: "" }) + + expect(addConversationMock).not.toHaveBeenCalled() + expect(clientAddMock).toHaveBeenCalledTimes(1) + const arg = clientAddMock.mock.calls[0]?.[0] + expect(arg.content).toBe("I love hiking") + expect(arg.customId).toBeUndefined() + }) +}) diff --git a/packages/tools/src/openai/middleware.ts b/packages/tools/src/openai/middleware.ts index 7fc4267e7..78df52da4 100644 --- a/packages/tools/src/openai/middleware.ts +++ b/packages/tools/src/openai/middleware.ts @@ -367,11 +367,13 @@ const addMemoryTool = async ( return } - // Fallback to old behavior for non-conversation memories + // Fallback to old behavior for non-conversation memories. The + // "conversation:" prefix is an internal routing marker — never let + // it reach the stored customId (colons fail customId validation). const response = await client.add({ content, containerTags: [containerTag], - customId, + customId: customId?.replace(/^conversation:/, ""), }) logger.info("Memory saved successfully", { @@ -548,9 +550,26 @@ export function createOpenAIMiddleware( if (addMemory === "always" && input?.trim()) { const content = customId ? `Input: ${input}` : input const memoryCustomId = customId ? `conversation:${customId}` : undefined + // Mirror the chat path: hand the input over as messages together + // with the credentials so a configured customId routes through + // /v4/conversations. Without these, addMemoryTool fell through to + // client.add() with the internal "conversation:" prefix baked + // into the stored customId — which the API rejects, so Responses + // API memory saves silently failed whenever customId was set. + const inputAsMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + [{ role: "user", content: input }] operations.push( - addMemoryTool(client, containerTag, content, memoryCustomId, logger), + addMemoryTool( + client, + containerTag, + content, + memoryCustomId, + logger, + customId ? inputAsMessages : undefined, + process.env.SUPERMEMORY_API_KEY, + baseUrl, + ), ) }