fix(agents): respect compat.supportsStore in WebSocket stream path (#39113)

Co-authored-by: scoootscooob <zhentongfan@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-07 19:40:19 +00:00
parent 5f8f58ae25
commit 0e4603ac71
3 changed files with 76 additions and 1 deletions

View File

@@ -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

View File

@@ -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<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
);
const completed = new Promise<void>((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<string, unknown>;
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<typeof streamFn>[0],
contextStub as Parameters<typeof streamFn>[1],
);
const completed = new Promise<void>((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<string, unknown>;
expect(sent).not.toHaveProperty("store");
});
it("emits an AssistantMessage on response.completed", async () => {
const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-2");
const stream = streamFn(

View File

@@ -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<string, unknown> = {
type: "response.create",
model: model.id,
store: false,
...(supportsStore !== false ? { store: false } : {}),
input: inputItems,
instructions: context.systemPrompt ?? undefined,
tools: tools.length > 0 ? tools : undefined,