diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4dfeacbf07..044b48f790 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -228,6 +228,10 @@ const openAiNativeSchema = apiModelIdProviderModelSchema.extend({ // OpenAI Responses API service tier for openai-native provider only. // UI should only expose this when the selected model supports flex/priority. openAiNativeServiceTier: serviceTierSchema.optional(), + // Enable stateless mode for OpenAI Responses API (sets store: false) + // When enabled, responses won't be stored for 30 days and can't be referenced + // in future requests using previous_response_id + openAiNativeStatelessMode: z.boolean().optional(), }) const mistralSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 618cdeac65..582a2c19a9 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -859,6 +859,109 @@ describe("OpenAiNativeHandler", () => { expect(secondCallBody.previous_response_id).toBe("resp_789") }) + it("should respect openAiNativeStatelessMode configuration", async () => { + // Test with stateless mode enabled + const statelessHandler = new OpenAiNativeHandler({ + ...mockOptions, + openAiNativeStatelessMode: true, + }) + + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Test"}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + // Mock SDK to fail + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + + const stream = statelessHandler.createMessage(systemPrompt, messages, { taskId: "test-task" }) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that store is set to false when stateless mode is enabled + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(requestBody.store).toBe(false) + }) + + it("should use metadata.store when stateless mode is disabled", async () => { + // Test with stateless mode disabled (default) + const handler = new OpenAiNativeHandler(mockOptions) + + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Test"}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + // Mock SDK to fail + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + + // Test with metadata.store = false + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", store: false }) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that store is set to false when metadata.store is false + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(requestBody.store).toBe(false) + }) + + it("should default to store:true when stateless mode is disabled and metadata.store is not set", async () => { + // Test with stateless mode disabled and no metadata.store + const handler = new OpenAiNativeHandler(mockOptions) + + // Mock fetch for Responses API + const mockFetch = vitest.fn().mockResolvedValue({ + ok: true, + body: new ReadableStream({ + start(controller) { + controller.enqueue( + new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Test"}\n\n'), + ) + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + }, + }), + }) + global.fetch = mockFetch as any + + // Mock SDK to fail + mockResponsesCreate.mockRejectedValue(new Error("SDK not available")) + + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task" }) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify that store defaults to true + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body) + expect(requestBody.store).toBe(true) + }) + it("should retry with full conversation when previous_response_id fails", async () => { // This test verifies the fix for context loss bug when previous_response_id becomes invalid const mockFetch = vitest diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 8a205a06b4..a5c3e3d19d 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -255,7 +255,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio model: model.id, input: formattedInput, stream: true, - store: metadata?.store !== false, // Default to true unless explicitly set to false + // Check if stateless mode is enabled in configuration, otherwise use metadata.store + store: this.options.openAiNativeStatelessMode ? false : metadata?.store !== false, // Always include instructions (system prompt) for Responses API. // Unlike Chat Completions, system/developer roles in input have no special semantics here. // The official way to set system behavior is the top-level `instructions` field. @@ -1286,7 +1287,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio }, ], stream: false, // Non-streaming for completePrompt - store: false, // Don't store prompt completions + // Use stateless mode if configured, otherwise don't store prompt completions + store: this.options.openAiNativeStatelessMode ? false : false, } // Include service tier if selected and supported