Skip to content

Commit 1219cb4

Browse files
authored
fix: keep OpenAI reasoning responses stateful (#4557)
## Executive Summary Built-in OpenAI/OpenAI Codex reasoning model payloads now force `store: true` so OpenAI Responses continuations can replay reasoning/tool-call state reliably. This prevents follow-up agent steps from failing with missing `rs_*` reasoning item errors. ## Root Cause OpenAI Responses reasoning/tool-call continuations can include prior `rs_*` reasoning items in subsequent requests. The upstream Responses payload defaults to `store: false`, which means OpenAI does not persist those items server-side. When a later request references one of those non-persisted item ids, OpenAI can return: > Item with id ... not found ## Approach When applying built-in provider payload defaults for OpenAI reasoning models, keep the existing reasoning-effort normalization and also force the Responses payload to be stateful: ```ts return { ...body, store: true, reasoning: { ...existingReasoning, effort, }, } ``` The test coverage verifies that: - OpenAI reasoning payloads get `store: true`. - An incoming `store: false` is intentionally overridden. - Non-OpenAI providers, such as DeepSeek, do not receive this payload override. ## Key Invariants - Only built-in OpenAI/OpenAI Codex reasoning model payload defaults should force `store: true`. - Existing payload fields should be preserved unless intentionally overridden. - Existing reasoning effort normalization should continue to apply. ## Non-goals - This does not change non-OpenAI provider behavior. - This does not implement a stateless/ZDR-compatible encrypted reasoning replay path. - This does not patch `@mariozechner/pi-ai`; it applies the built-in agent payload default at our integration layer. ## Trade-offs The alternative is to keep `store: false` and preserve/replay full `reasoning.encrypted_content` for stateless operation. That is more complex and unnecessary for the built-in agent default path. Using `store: true` matches OpenAI's stateful Responses flow and directly fixes the observed continuation failure. ## Verification ```bash GITHUB_BASE_REF=main node scripts/check-changeset.mjs pnpm --filter @electric-ax/agents exec vitest run test/model-catalog.test.ts pnpm --filter @electric-ax/agents typecheck ``` Also attempted the prep command's suggested Vitest thread flag: ```bash pnpm --filter @electric-ax/agents exec vitest run test/model-catalog.test.ts --pool-options.threads.maxThreads=2 ``` That failed because this Vitest version rejects the flag as an unknown option (`--poolOptions`). The same targeted test passes without that unsupported flag. ## Files Changed - `.changeset/openai-reasoning-store-true.md` — patch changeset for `@electric-ax/agents`. - `packages/agents/src/model-catalog.ts` — forces `store: true` for built-in OpenAI reasoning payload defaults. - `packages/agents/test/model-catalog.test.ts` — updates expectations and adds regression coverage for OpenAI-only `store: true` behavior.
1 parent 63f4107 commit 1219cb4

3 files changed

Lines changed: 52 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@electric-ax/agents': patch
3+
---
4+
5+
Force `store: true` for built-in OpenAI reasoning model payloads so reasoning/tool-call continuations can replay `rs_*` reasoning items without follow-up requests failing due to missing persisted items.

packages/agents/src/model-catalog.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ function withProviderPayloadDefaults(
267267

268268
return {
269269
...body,
270+
// OpenAI Responses reasoning/tool-call continuations replay rs_*
271+
// reasoning items. With store:false, OpenAI does not persist those
272+
// items server-side, which can make follow-up requests fail with
273+
// "Item with id ... not found". Keep Responses stateful for built-ins.
274+
store: true,
270275
reasoning: {
271276
...existingReasoning,
272277
effort,

packages/agents/test/model-catalog.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ describe(`model catalog`, () => {
126126
)
127127

128128
expect(payload).toEqual({
129+
store: true,
129130
reasoning: { effort: `minimal` },
130131
})
131132
})
@@ -145,10 +146,51 @@ describe(`model catalog`, () => {
145146
)
146147

147148
expect(payload).toEqual({
149+
store: true,
148150
reasoning: { effort: `high` },
149151
})
150152
})
151153

154+
it(`forces store true only for OpenAI reasoning model payloads`, async () => {
155+
const openAiCatalog = await createBuiltinModelCatalog()
156+
const openAiConfig = resolveBuiltinModelConfig(openAiCatalog!, {
157+
model: `openai:gpt-5`,
158+
})
159+
160+
expect(
161+
openAiConfig.onPayload!(
162+
{ store: false, reasoning: { effort: `none` } },
163+
{} as any
164+
)
165+
).toEqual({
166+
store: true,
167+
reasoning: { effort: `minimal` },
168+
})
169+
170+
delete process.env.OPENAI_API_KEY
171+
process.env.DEEPSEEK_API_KEY = `test-deepseek-key`
172+
vi.stubGlobal(
173+
`fetch`,
174+
vi.fn(async (url: string) => {
175+
if (String(url).includes(`deepseek.com`)) {
176+
return {
177+
ok: true,
178+
status: 200,
179+
json: async () => ({ data: [{ id: `deepseek-v4-flash` }] }),
180+
}
181+
}
182+
return { ok: false, status: 401, json: async () => ({}) }
183+
})
184+
)
185+
186+
const deepseekCatalog = await createBuiltinModelCatalog()
187+
const deepseekConfig = resolveBuiltinModelConfig(deepseekCatalog!, {
188+
model: `deepseek:deepseek-v4-flash`,
189+
})
190+
191+
expect(deepseekConfig.onPayload).toBeUndefined()
192+
})
193+
152194
it(`does not expose providers whose keys are rejected`, async () => {
153195
vi.stubGlobal(
154196
`fetch`,

0 commit comments

Comments
 (0)