e27f16365d
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.