diff --git a/CHANGELOG.md b/CHANGELOG.md index 6559fe86225..23eed3c22da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -261,6 +261,7 @@ Docs: https://docs.openclaw.ai - UI/Control chat tool streaming: render tool events live in webchat without requiring refresh by enabling `tool-events` capability, fixing stream/event correlation, and resetting/reloading stream state around tool results and terminal events. (#39104) Thanks @jakepresent. - Models/provider apiKey persistence hardening: when a provider `apiKey` value equals a known provider env var value, persist the canonical env var name into `models.json` instead of resolved plaintext secrets. (#38889) Thanks @gambletan. - Discord/model picker persistence check: add a short post-dispatch settle delay before reading back session model state so picker confirmations stop reporting false mismatch warnings after successful model switches. (#39105) Thanks @akropp. +- Agents/OpenAI WS compat store flag: omit `store` from `response.create` payloads when model compat sets `supportsStore: false`, preventing strict OpenAI-compatible providers from rejecting websocket requests with unknown-field errors. (#39113) Thanks @scoootscooob. ## 2026.3.2 diff --git a/src/agents/openai-ws-stream.test.ts b/src/agents/openai-ws-stream.test.ts index b467de80262..00d0a3df64c 100644 --- a/src/agents/openai-ws-stream.test.ts +++ b/src/agents/openai-ws-stream.test.ts @@ -634,6 +634,8 @@ describe("createOpenAIWebSocketStreamFn", () => { releaseWsSession("sess-incremental"); releaseWsSession("sess-full"); releaseWsSession("sess-tools"); + releaseWsSession("sess-store-default"); + releaseWsSession("sess-store-compat"); }); it("connects to the WebSocket on first call", async () => { @@ -691,6 +693,73 @@ describe("createOpenAIWebSocketStreamFn", () => { expect(Array.isArray(sent.input)).toBe(true); }); + it("includes store:false by default", async () => { + const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-store-default"); + const stream = streamFn( + modelStub as Parameters[0], + contextStub as Parameters[1], + ); + + const completed = new Promise((res, rej) => { + queueMicrotask(async () => { + try { + await new Promise((r) => setImmediate(r)); + const manager = MockManager.lastInstance!; + manager.simulateEvent({ + type: "response.completed", + response: makeResponseObject("resp_store_default", "ok"), + }); + for await (const _ of await resolveStream(stream)) { + // consume + } + res(); + } catch (e) { + rej(e); + } + }); + }); + await completed; + + const sent = MockManager.lastInstance!.sentEvents[0] as Record; + expect(sent.store).toBe(false); + }); + + it("omits store when compat.supportsStore is false (#39086)", async () => { + releaseWsSession("sess-store-compat"); + const noStoreModel = { + ...modelStub, + compat: { supportsStore: false }, + }; + const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-store-compat"); + const stream = streamFn( + noStoreModel as Parameters[0], + contextStub as Parameters[1], + ); + + const completed = new Promise((res, rej) => { + queueMicrotask(async () => { + try { + await new Promise((r) => setImmediate(r)); + const manager = MockManager.lastInstance!; + manager.simulateEvent({ + type: "response.completed", + response: makeResponseObject("resp_no_store", "ok"), + }); + for await (const _ of await resolveStream(stream)) { + // consume + } + res(); + } catch (e) { + rej(e); + } + }); + }); + await completed; + + const sent = MockManager.lastInstance!.sentEvents[0] as Record; + expect(sent).not.toHaveProperty("store"); + }); + it("emits an AssistantMessage on response.completed", async () => { const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-2"); const stream = streamFn( diff --git a/src/agents/openai-ws-stream.ts b/src/agents/openai-ws-stream.ts index b7449f30991..d7fd1db99c6 100644 --- a/src/agents/openai-ws-stream.ts +++ b/src/agents/openai-ws-stream.ts @@ -589,10 +589,15 @@ export function createOpenAIWebSocketStreamFn( extraParams.reasoning = reasoning; } + // Respect compat.supportsStore — providers like Gemini reject unknown + // fields such as `store` with a 400 error. Fixes #39086. + const supportsStore = (model as { compat?: { supportsStore?: boolean } }).compat + ?.supportsStore; + const payload: Record = { type: "response.create", model: model.id, - store: false, + ...(supportsStore !== false ? { store: false } : {}), input: inputItems, instructions: context.systemPrompt ?? undefined, tools: tools.length > 0 ? tools : undefined,