diff --git a/packages/tools/src/openai/middleware.ts b/packages/tools/src/openai/middleware.ts index 518db547a..4dde209fc 100644 --- a/packages/tools/src/openai/middleware.ts +++ b/packages/tools/src/openai/middleware.ts @@ -970,13 +970,22 @@ export function createOpenAIMiddleware( : "" } + // Sending `instructions` at all tells the Responses API to drop whatever a + // `previous_response_id` turn carried over, so an empty result must leave + // the caller's own field untouched rather than blank it. Matches the + // no-input early return above. + const instructionsOverride = + enhancedInstructions || typeof params.instructions === "string" + ? { instructions: enhancedInstructions } + : {} + return { request: originalResponsesCreate.call( openaiClient.responses, { ...params, input: cleanedInput, - instructions: enhancedInstructions, + ...instructionsOverride, }, requestOptions, ), diff --git a/packages/tools/test/openai-middleware.unit.test.ts b/packages/tools/test/openai-middleware.unit.test.ts index e3249c8d3..bdd882139 100644 --- a/packages/tools/test/openai-middleware.unit.test.ts +++ b/packages/tools/test/openai-middleware.unit.test.ts @@ -63,3 +63,101 @@ describe("OpenAI middleware memory context", () => { ).toHaveLength(1) }) }) + +describe("OpenAI Responses middleware instructions", () => { + const originalApiKey = process.env.SUPERMEMORY_API_KEY + + beforeEach(() => { + process.env.SUPERMEMORY_API_KEY = "sm_test_key" + }) + + afterEach(() => { + if (originalApiKey === undefined) delete process.env.SUPERMEMORY_API_KEY + else process.env.SUPERMEMORY_API_KEY = originalApiKey + vi.unstubAllGlobals() + }) + + const stubMemoryFetch = (staticMemories: string[]) => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + profile: { + static: staticMemories.map((memory) => ({ memory })), + dynamic: [], + }, + searchResults: { results: [] }, + }), + }), + ) + } + + const wrapResponsesClient = () => { + const originalCreate = vi.fn(() => + Object.assign(Promise.resolve({ output: [] }), { + asResponse: async () => new Response(), + }), + ) + const client = { + chat: { completions: { create: vi.fn() } }, + responses: { create: originalCreate }, + } as unknown as OpenAI + const wrapped = withSupermemory(client, { + containerTag: "user-a", + customId: "conversation-a", + mode: "profile", + addMemory: "never", + }) + return { originalCreate, wrapped } + } + + it("leaves instructions absent when the caller sent none and there is nothing to inject", async () => { + stubMemoryFetch([]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + previous_response_id: "resp_123", + }) + + // An empty `instructions` still counts as present, and the Responses API + // drops the previous turn's instructions whenever the field is sent. + const forwarded = originalCreate.mock.calls[0]?.[0] + expect("instructions" in forwarded).toBe(false) + expect(forwarded.previous_response_id).toBe("resp_123") + }) + + it("still injects memories as instructions when the caller sent none", async () => { + stubMemoryFetch(["Fresh profile fact"]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + }) + + const forwarded = originalCreate.mock.calls[0]?.[0] + expect(String(forwarded.instructions)).toContain("Fresh profile fact") + }) + + it("keeps caller instructions and strips stale context when there is nothing to inject", async () => { + stubMemoryFetch([]) + const { originalCreate, wrapped } = wrapResponsesClient() + + await wrapped.responses.create({ + model: "gpt-4o-mini", + input: "What do you remember?", + instructions: [ + "Be helpful.", + '', + "Stale profile fact", + "", + ].join("\n"), + }) + + const forwarded = originalCreate.mock.calls[0]?.[0] + expect(String(forwarded.instructions)).toBe("Be helpful.") + }) +})