fix(openai): surface chat-completions refusal as visible text#102334
fix(openai): surface chat-completions refusal as visible text#102334xydt-tanshanshan wants to merge 2 commits into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: the same linked refusal bug now has a better canonical landing candidate at #102344, while this branch is conflicting and still misses the shared-provider Canonical path: Use #102344 as the canonical landing path for the linked refusal bug, then close the duplicate refusal-fix branches. So I’m closing this here and keeping the remaining discussion on #102344. Review detailsBest possible solution: Use #102344 as the canonical landing path for the linked refusal bug, then close the duplicate refusal-fix branches. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection gives a high-confidence path: feed either completions assembler a refusal-only delta or message chunk and current main never reads the refusal field. Is this the best way to solve the issue? No. The narrow parser-parity fix is the right shape, but this branch is not the best landing path because it leaves the shared provider's aggregated Security review: Security review cleared: No concrete security or supply-chain regression was found; the diff touches provider parsing and focused tests without dependency, workflow, secret, package, or install-script changes. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against e90bf3182927. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
OpenAI chat-completions safety/structured-output refusals arrive in a top-level refusal field with content: null. The streaming assembler never read this field, so the assistant turn resolved to empty content. - src/agents/openai-transport-stream.ts: add else-if branch for choiceDelta.refusal in processOpenAICompletionsStream delta loop, routing refusal text through appendTextDelta - packages/ai/src/providers/openai-completions.ts: add else-if for refusal in streamOpenAICompletions, routing through appendPartitionedContent - Add 2 tests (streaming delta + aggregated message) covering refusal surfacing - Sibling Responses path already handled refusal at lines 1635, 1697-1700, 1878 Fixes openclaw#102321
…efusal routing, add packages/ai test
b1c062b to
1484e4e
Compare
|
@clawsweeper re-review |
|
🦞🧹 Reason: re-review requires an open issue or PR. |
What Problem This Solves
When an OpenAI chat-completions response carries a safety or structured-output refusal, the assistant turn resolves to empty content and the user sees nothing. OpenAI returns refusals in a top-level
refusalfield withcontent: nullandfinish_reason: "stop", but the completions streaming assembler never readsrefusal, so the refusal text is silently dropped.Why This Change Was Made
Two streaming assemblers had the same blind spot:
processOpenAICompletionsStreaminsrc/agents/openai-transport-stream.ts— the delta loop readscontent, reasoning deltas, andtool_callsfromchoice.deltabut never checksrefusalstreamOpenAICompletionsinpackages/ai/src/providers/openai-completions.ts— same pattern, drops refusalThe sibling Responses path (same file, lines 1635, 1697-1700, 1878) already routes
refusalcorrectly. The fix mirrors that pattern: whencontentis null/absent, checkrefusaland route it as visible assistant text.User Impact
For users whose prompts trigger an OpenAI safety refusal on the
openai-completionsAPI, the assistant reply will now show the refusal text (e.g. "I can't help with that.") instead of a blank empty reply. Users of the Responses API are unaffected (already handled).Evidence
Real proof — streaming delta refusal (after fix):
Proof run on disposable Node.js, stubbing only the transport input as an async iterable, keeping the
processOpenAICompletionsStreamassembly logic real:Both forms now surface the refusal as visible assistant text instead of empty content.
Code change —
src/agents/openai-transport-stream.ts(afterchoiceDelta.contentblock):Code change —
packages/ai/src/providers/openai-completions.ts(afterchoice.delta.contentblock):Test coverage — 3 new tests across 2 files:
src/agents/openai-transport-stream.test.ts: streaming delta refusal + aggregated message refusalpackages/ai/src/providers/openai-completions.test.ts: streaming delta refusal in the shared assemblerAll tests feed a refusal-only chunk and assert
output.contentequals[{ type: "text", text: "I can't help with that." }].Environment: Linux x86_64, Node v24.13.1, commit 615cab6.
Fixes #102321