From f2a92e7c8427d2afb209841030f6ccc9b85bf3da Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 20:51:26 +0000 Subject: [PATCH] fix(agents): forward websocket maxTokens=0 correctly Landed from #39148 by @scoootscooob. Co-authored-by: scoootscooob --- CHANGELOG.md | 1 + src/agents/openai-ws-stream.test.ts | 31 +++++++++++++++++++++++++++++ src/agents/openai-ws-stream.ts | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b76306f5e..c9dfb00ca35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -276,6 +276,7 @@ Docs: https://docs.openclaw.ai - Browser/dispatcher error clarity: preserve dispatcher-side failure context in browser fetch errors while still appending operator guidance and explicit no-retry model hints, preventing misleading `"Can't reach service"` wrapping and avoiding LLM retry loops. (#39090) Thanks @NewdlDewdl. - Telegram/polling offset safety: confirm persisted offsets before polling startup while validating stored `lastUpdateId` values as non-negative safe integers (with overflow guards) so malformed offset state cannot cause update skipping/dropping. (#39111) Thanks @MumuTW. - Telegram/status SecretRef read-only resolution: resolve env-backed bot-token SecretRefs in config-only/status inspection while respecting provider source/defaults and env allowlists, so status no longer crashes or reports false-ready tokens for disallowed providers. (#39130) Thanks @neocody. +- Agents/OpenAI WS max-token zero forwarding: treat `maxTokens: 0` as an explicit value in websocket `response.create` payloads (instead of dropping it as falsy), with regression coverage for zero-token forwarding. (#39148) Thanks @scoootscooob. ## 2026.3.2 diff --git a/src/agents/openai-ws-stream.test.ts b/src/agents/openai-ws-stream.test.ts index 00d0a3df64c..a9c3679f561 100644 --- a/src/agents/openai-ws-stream.test.ts +++ b/src/agents/openai-ws-stream.test.ts @@ -636,6 +636,7 @@ describe("createOpenAIWebSocketStreamFn", () => { releaseWsSession("sess-tools"); releaseWsSession("sess-store-default"); releaseWsSession("sess-store-compat"); + releaseWsSession("sess-max-tokens-zero"); }); it("connects to the WebSocket on first call", async () => { @@ -1008,6 +1009,36 @@ describe("createOpenAIWebSocketStreamFn", () => { expect(sent.max_output_tokens).toBe(256); }); + it("forwards maxTokens: 0 to response.create as max_output_tokens", async () => { + const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-max-tokens-zero"); + const opts = { maxTokens: 0 }; + const stream = streamFn( + modelStub as Parameters[0], + contextStub as Parameters[1], + opts as Parameters[2], + ); + await new Promise((resolve, reject) => { + queueMicrotask(async () => { + try { + await new Promise((r) => setImmediate(r)); + MockManager.lastInstance!.simulateEvent({ + type: "response.completed", + response: makeResponseObject("resp-max-zero", "Done"), + }); + for await (const _ of await resolveStream(stream)) { + /* consume */ + } + resolve(); + } catch (e) { + reject(e); + } + }); + }); + const sent = MockManager.lastInstance!.sentEvents[0] as Record; + expect(sent.type).toBe("response.create"); + expect(sent.max_output_tokens).toBe(0); + }); + it("forwards reasoningEffort/reasoningSummary to response.create reasoning block", async () => { const streamFn = createOpenAIWebSocketStreamFn("sk-test", "sess-reason"); const opts = { reasoningEffort: "high", reasoningSummary: "auto" }; diff --git a/src/agents/openai-ws-stream.ts b/src/agents/openai-ws-stream.ts index d7fd1db99c6..9228fd92d46 100644 --- a/src/agents/openai-ws-stream.ts +++ b/src/agents/openai-ws-stream.ts @@ -569,7 +569,7 @@ export function createOpenAIWebSocketStreamFn( if (streamOpts?.temperature !== undefined) { extraParams.temperature = streamOpts.temperature; } - if (streamOpts?.maxTokens) { + if (streamOpts?.maxTokens !== undefined) { extraParams.max_output_tokens = streamOpts.maxTokens; } if (streamOpts?.topP !== undefined) {