feat: add stateless mode configuration for OpenAI Native provider

- Add openAiNativeStatelessMode boolean option to provider settings schema
- Update OpenAI Native handler to respect stateless mode configuration
- When enabled, forces store: false for all Responses API requests
- Add comprehensive tests for stateless mode behavior

Fixes #7789
This commit is contained in:
Roo Code 2025-09-08 18:40:16 +00:00
parent 0ce4e891fd
commit 753a66f171
3 changed files with 189 additions and 2 deletions

View file

@ -228,6 +228,9 @@ 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(),
// When true, forces the OpenAI Responses API to run in stateless mode (store: false)
// This prevents responses from being stored for 30 days in OpenAI's Responses API
openAiNativeStatelessMode: z.boolean().optional(),
})
const mistralSchema = apiModelIdProviderModelSchema.extend({

View file

@ -1732,5 +1732,187 @@ describe("GPT-5 streaming event coverage (additional)", () => {
expect(bodyStr).not.toContain('"verbosity"')
})
})
describe("Stateless mode configuration", () => {
it("should use stateless mode when openAiNativeStatelessMode is true", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
;(global as any).fetch = mockFetch as any
// Force SDK path to fail so we use fetch fallback
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
openAiNativeStatelessMode: true, // Enable stateless mode
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
const stream = handler.createMessage(systemPrompt, messages)
for await (const _ of stream) {
// drain
}
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(bodyStr)
expect(parsedBody.store).toBe(false) // Should be false when stateless mode is enabled
})
it("should default to store: true when openAiNativeStatelessMode is false", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
;(global as any).fetch = mockFetch as any
// Force SDK path to fail so we use fetch fallback
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
openAiNativeStatelessMode: false, // Explicitly disable stateless mode
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
const stream = handler.createMessage(systemPrompt, messages)
for await (const _ of stream) {
// drain
}
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(bodyStr)
expect(parsedBody.store).toBe(true) // Should be true when stateless mode is disabled
})
it("should default to store: true when openAiNativeStatelessMode is not set", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
;(global as any).fetch = mockFetch as any
// Force SDK path to fail so we use fetch fallback
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
// openAiNativeStatelessMode not set
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
const stream = handler.createMessage(systemPrompt, messages)
for await (const _ of stream) {
// drain
}
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(bodyStr)
expect(parsedBody.store).toBe(true) // Should default to true
})
it("should override metadata.store when openAiNativeStatelessMode is true", async () => {
const mockFetch = vitest.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
;(global as any).fetch = mockFetch as any
// Force SDK path to fail so we use fetch fallback
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
openAiNativeStatelessMode: true, // Enable stateless mode
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
// Even if metadata.store is true, stateless mode should override it
const stream = handler.createMessage(systemPrompt, messages, { taskId: "test", store: true })
for await (const _ of stream) {
// drain
}
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
const parsedBody = JSON.parse(bodyStr)
expect(parsedBody.store).toBe(false) // Should be false even when metadata.store is true
})
it("should use stateless mode in completePrompt when openAiNativeStatelessMode is true", async () => {
// Mock the responses.create method
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "Test response",
},
],
},
],
})
const handler = new OpenAiNativeHandler({
apiModelId: "gpt-5-2025-08-07",
openAiNativeApiKey: "test-api-key",
openAiNativeStatelessMode: true, // Enable stateless mode
})
await handler.completePrompt("Test prompt")
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({
store: false, // Should always be false in completePrompt with stateless mode
}),
)
})
})
})
})

View file

@ -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
// Use stateless mode if configured, otherwise respect metadata.store (default true)
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