Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/tools/src/openai/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
98 changes: 98 additions & 0 deletions packages/tools/test/openai-middleware.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
'<supermemory context="user-memories" readonly>',
"Stale profile fact",
"</supermemory>",
].join("\n"),
})

const forwarded = originalCreate.mock.calls[0]?.[0]
expect(String(forwarded.instructions)).toBe("Be helpful.")
})
})
Loading