Skip to content

Commit 753a66f

Browse files
committed
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
1 parent 0ce4e89 commit 753a66f

File tree

3 files changed

+189
-2
lines changed

3 files changed

+189
-2
lines changed

packages/types/src/provider-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ const openAiNativeSchema = apiModelIdProviderModelSchema.extend({
228228
// OpenAI Responses API service tier for openai-native provider only.
229229
// UI should only expose this when the selected model supports flex/priority.
230230
openAiNativeServiceTier: serviceTierSchema.optional(),
231+
// When true, forces the OpenAI Responses API to run in stateless mode (store: false)
232+
// This prevents responses from being stored for 30 days in OpenAI's Responses API
233+
openAiNativeStatelessMode: z.boolean().optional(),
231234
})
232235

233236
const mistralSchema = apiModelIdProviderModelSchema.extend({

src/api/providers/__tests__/openai-native.spec.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,5 +1732,187 @@ describe("GPT-5 streaming event coverage (additional)", () => {
17321732
expect(bodyStr).not.toContain('"verbosity"')
17331733
})
17341734
})
1735+
1736+
describe("Stateless mode configuration", () => {
1737+
it("should use stateless mode when openAiNativeStatelessMode is true", async () => {
1738+
const mockFetch = vitest.fn().mockResolvedValue({
1739+
ok: true,
1740+
body: new ReadableStream({
1741+
start(controller) {
1742+
controller.enqueue(
1743+
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
1744+
)
1745+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1746+
controller.close()
1747+
},
1748+
}),
1749+
})
1750+
;(global as any).fetch = mockFetch as any
1751+
1752+
// Force SDK path to fail so we use fetch fallback
1753+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1754+
1755+
const handler = new OpenAiNativeHandler({
1756+
apiModelId: "gpt-5-2025-08-07",
1757+
openAiNativeApiKey: "test-api-key",
1758+
openAiNativeStatelessMode: true, // Enable stateless mode
1759+
})
1760+
1761+
const systemPrompt = "You are a helpful assistant."
1762+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1763+
const stream = handler.createMessage(systemPrompt, messages)
1764+
1765+
for await (const _ of stream) {
1766+
// drain
1767+
}
1768+
1769+
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
1770+
const parsedBody = JSON.parse(bodyStr)
1771+
expect(parsedBody.store).toBe(false) // Should be false when stateless mode is enabled
1772+
})
1773+
1774+
it("should default to store: true when openAiNativeStatelessMode is false", async () => {
1775+
const mockFetch = vitest.fn().mockResolvedValue({
1776+
ok: true,
1777+
body: new ReadableStream({
1778+
start(controller) {
1779+
controller.enqueue(
1780+
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
1781+
)
1782+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1783+
controller.close()
1784+
},
1785+
}),
1786+
})
1787+
;(global as any).fetch = mockFetch as any
1788+
1789+
// Force SDK path to fail so we use fetch fallback
1790+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1791+
1792+
const handler = new OpenAiNativeHandler({
1793+
apiModelId: "gpt-5-2025-08-07",
1794+
openAiNativeApiKey: "test-api-key",
1795+
openAiNativeStatelessMode: false, // Explicitly disable stateless mode
1796+
})
1797+
1798+
const systemPrompt = "You are a helpful assistant."
1799+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1800+
const stream = handler.createMessage(systemPrompt, messages)
1801+
1802+
for await (const _ of stream) {
1803+
// drain
1804+
}
1805+
1806+
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
1807+
const parsedBody = JSON.parse(bodyStr)
1808+
expect(parsedBody.store).toBe(true) // Should be true when stateless mode is disabled
1809+
})
1810+
1811+
it("should default to store: true when openAiNativeStatelessMode is not set", async () => {
1812+
const mockFetch = vitest.fn().mockResolvedValue({
1813+
ok: true,
1814+
body: new ReadableStream({
1815+
start(controller) {
1816+
controller.enqueue(
1817+
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
1818+
)
1819+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1820+
controller.close()
1821+
},
1822+
}),
1823+
})
1824+
;(global as any).fetch = mockFetch as any
1825+
1826+
// Force SDK path to fail so we use fetch fallback
1827+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1828+
1829+
const handler = new OpenAiNativeHandler({
1830+
apiModelId: "gpt-5-2025-08-07",
1831+
openAiNativeApiKey: "test-api-key",
1832+
// openAiNativeStatelessMode not set
1833+
})
1834+
1835+
const systemPrompt = "You are a helpful assistant."
1836+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1837+
const stream = handler.createMessage(systemPrompt, messages)
1838+
1839+
for await (const _ of stream) {
1840+
// drain
1841+
}
1842+
1843+
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
1844+
const parsedBody = JSON.parse(bodyStr)
1845+
expect(parsedBody.store).toBe(true) // Should default to true
1846+
})
1847+
1848+
it("should override metadata.store when openAiNativeStatelessMode is true", async () => {
1849+
const mockFetch = vitest.fn().mockResolvedValue({
1850+
ok: true,
1851+
body: new ReadableStream({
1852+
start(controller) {
1853+
controller.enqueue(
1854+
new TextEncoder().encode('data: {"type":"response.done","response":{}}\n\n'),
1855+
)
1856+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
1857+
controller.close()
1858+
},
1859+
}),
1860+
})
1861+
;(global as any).fetch = mockFetch as any
1862+
1863+
// Force SDK path to fail so we use fetch fallback
1864+
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
1865+
1866+
const handler = new OpenAiNativeHandler({
1867+
apiModelId: "gpt-5-2025-08-07",
1868+
openAiNativeApiKey: "test-api-key",
1869+
openAiNativeStatelessMode: true, // Enable stateless mode
1870+
})
1871+
1872+
const systemPrompt = "You are a helpful assistant."
1873+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1874+
// Even if metadata.store is true, stateless mode should override it
1875+
const stream = handler.createMessage(systemPrompt, messages, { taskId: "test", store: true })
1876+
1877+
for await (const _ of stream) {
1878+
// drain
1879+
}
1880+
1881+
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
1882+
const parsedBody = JSON.parse(bodyStr)
1883+
expect(parsedBody.store).toBe(false) // Should be false even when metadata.store is true
1884+
})
1885+
1886+
it("should use stateless mode in completePrompt when openAiNativeStatelessMode is true", async () => {
1887+
// Mock the responses.create method
1888+
mockResponsesCreate.mockResolvedValue({
1889+
output: [
1890+
{
1891+
type: "message",
1892+
content: [
1893+
{
1894+
type: "output_text",
1895+
text: "Test response",
1896+
},
1897+
],
1898+
},
1899+
],
1900+
})
1901+
1902+
const handler = new OpenAiNativeHandler({
1903+
apiModelId: "gpt-5-2025-08-07",
1904+
openAiNativeApiKey: "test-api-key",
1905+
openAiNativeStatelessMode: true, // Enable stateless mode
1906+
})
1907+
1908+
await handler.completePrompt("Test prompt")
1909+
1910+
expect(mockResponsesCreate).toHaveBeenCalledWith(
1911+
expect.objectContaining({
1912+
store: false, // Should always be false in completePrompt with stateless mode
1913+
}),
1914+
)
1915+
})
1916+
})
17351917
})
17361918
})

src/api/providers/openai-native.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
255255
model: model.id,
256256
input: formattedInput,
257257
stream: true,
258-
store: metadata?.store !== false, // Default to true unless explicitly set to false
258+
// Use stateless mode if configured, otherwise respect metadata.store (default true)
259+
store: this.options.openAiNativeStatelessMode ? false : metadata?.store !== false,
259260
// Always include instructions (system prompt) for Responses API.
260261
// Unlike Chat Completions, system/developer roles in input have no special semantics here.
261262
// The official way to set system behavior is the top-level `instructions` field.
@@ -1286,7 +1287,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
12861287
},
12871288
],
12881289
stream: false, // Non-streaming for completePrompt
1289-
store: false, // Don't store prompt completions
1290+
// Use stateless mode if configured, otherwise don't store prompt completions
1291+
store: this.options.openAiNativeStatelessMode ? false : false,
12901292
}
12911293

12921294
// Include service tier if selected and supported

0 commit comments

Comments
 (0)