Skip to content

feat(ai): preserve streamed refusals as text - #43343

Merged
rekram1-node merged 4 commits into
v2from
refusal-streams
Aug 20, 2026
Merged

rekram1-node merged 4 commits into
v2from
refusal-streams

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • preserve OpenAI Chat delta.refusal as visible ordinary assistant text
  • preserve Open Responses and OpenAI Responses response.refusal.delta / response.refusal.done as visible ordinary assistant text
  • use typed refusal event variants while accepting compatible providers that omit unused standard coordinates
  • retain Responses message phase metadata through the existing message text lifecycle
  • keep provider terminal semantics unchanged: a refusal ending with stop remains a successful assistant answer
  • replay persisted refusal text as ordinary Chat content or Responses output_text, never as native refusal state

Follow-up to #43332. Part of #41932.

Testing

  • focused Chat refusal, reasoning-order, and mixed-delta tests
  • full OpenAI Responses and Open Responses-compatible provider test files
  • bun typecheck and bun run build in packages/ai

The complete AI suite previously had unrelated failures on v2: an Azure Chat URL expectation, stale OpenRouter recorded requests, and existing OpenRouter reasoning-lowering expectations.

@rekram1-node rekram1-node changed the title feat(ai): preserve streamed refusals feat(ai): preserve streamed refusals as text Aug 19, 2026
@rekram1-node
rekram1-node merged commit 5b1e845 into v2 Aug 20, 2026
10 checks passed
@rekram1-node
rekram1-node deleted the refusal-streams branch August 20, 2026 05:47
teknium1 added a commit to NousResearch/hermes-agent that referenced this pull request Sep 15, 2026
…ncode#43343)

A model that declines mid-stream delivers the explanation on the
structured refusal channel (chat_completions delta.refusal; Responses
response.refusal.delta / refusal content parts) and leaves content
empty. The streaming accumulators dropped that channel entirely, so a
streamed refusal assembled into an empty message and fell into the
empty/invalid-response retry loops - burning paid retries reproducing a
deterministic refusal - while the non-streaming path had already fixed
this class in #46013.

- chat_completions streaming: accumulate delta.refusal (incl.
  model_extra), expose message.refusal on the assembled mock response so
  ChatCompletionsTransport.normalize_response applies the existing
  sole-payload -> content_filter promotion; count refusal deltas in the
  zero-chunk guard; carry refusal in the Relay final-response dict.
- Codex Responses stream consumer: collect response.refusal.delta as
  answer text so a refusal-only stream no longer raises 'did not emit a
  terminal response' with zero usable content.
- Responses normalizer: read type=refusal content parts in
  _extract_responses_message_text (attr and dict shapes).

Sabotage-verified: each new test fails with its wiring line disabled.
E2E: refusal-only stream -> terminal content_filter with explanation;
refusal-alongside-content stays a normal usable turn; plain-text
streams unchanged.
teknium1 added a commit to NousResearch/hermes-agent that referenced this pull request Sep 15, 2026
…ncode#43343)

A model that declines mid-stream delivers the explanation on the
structured refusal channel (chat_completions delta.refusal; Responses
response.refusal.delta / refusal content parts) and leaves content
empty. The streaming accumulators dropped that channel entirely, so a
streamed refusal assembled into an empty message and fell into the
empty/invalid-response retry loops - burning paid retries reproducing a
deterministic refusal - while the non-streaming path had already fixed
this class in #46013.

- chat_completions streaming: accumulate delta.refusal (incl.
  model_extra), expose message.refusal on the assembled mock response so
  ChatCompletionsTransport.normalize_response applies the existing
  sole-payload -> content_filter promotion; count refusal deltas in the
  zero-chunk guard; carry refusal in the Relay final-response dict.
- Codex Responses stream consumer: collect response.refusal.delta as
  answer text so a refusal-only stream no longer raises 'did not emit a
  terminal response' with zero usable content.
- Responses normalizer: read type=refusal content parts in
  _extract_responses_message_text (attr and dict shapes).

Sabotage-verified: each new test fails with its wiring line disabled.
E2E: refusal-only stream -> terminal content_filter with explanation;
refusal-alongside-content stays a normal usable turn; plain-text
streams unchanged.
QuixThe2nd pushed a commit to QuixThe2nd/hermes-ide that referenced this pull request Sep 15, 2026
jinhuang712 pushed a commit to jinhuang712/opencode that referenced this pull request Sep 26, 2026
kanekoshoyu pushed a commit to kanekoshoyu/hermes-agent-1 that referenced this pull request Oct 15, 2026
kanekoshoyu pushed a commit to kanekoshoyu/hermes-agent-1 that referenced this pull request Oct 29, 2026
kanekoshoyu pushed a commit to kanekoshoyu/hermes-agent-1 that referenced this pull request Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant