server: keep SSE streams alive while tool-call output is withheld - #1006
Open
sscarduzio wants to merge 1 commit into
Open
server: keep SSE streams alive while tool-call output is withheld#1006sscarduzio wants to merge 1 commit into
sscarduzio wants to merge 1 commit into
Conversation
sscarduzio
force-pushed
the
server-keepalive-withheld-output
branch
from
September 9, 2026 14:54
4b49fa7 to
9c2bfff
Compare
Tool-call bytes are held back from the OpenAI, Responses and Anthropic streams until the DSML parses, so a multi-thousand-token tool call sends nothing for minutes. Clients with an idle timeout abort (pi and undici both default to 300 s), retry the same prompt, and on a model whose recurrent state cannot rewind (Qwen 3.8 Flash Next) the retry re-prefills the whole context: a 33K-token agent turn cost three 9-minute prefills before giving up. The same silence covers a tool call that opens inside the think block (held until it completes, mode stays THINKING) and Responses reasoning the client did not opt into with reasoning.summary. Mirror the prefill keepalive: while a stream withholds output, write an SSE comment every 5 s. Comment lines are dropped by every SSE parser, the same assumption the existing ": prefill" keepalive relies on. "Withheld" is decided the way the parser decides it: suppress/tool mode, or a tool start after emit_pos while still thinking. Tests: no-model regressions over a socketpair for each stream path plus the two thinking-time cases: the first withheld update only arms the timer, a backdated timer emits exactly one comment, and raw tool text never reaches the wire. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sscarduzio
force-pushed
the
server-keepalive-withheld-output
branch
from
September 9, 2026 14:59
9c2bfff to
4610665
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
: generatingevery 5 s on the OpenAI chat, Responses and Anthropic paths. Withheld means what the parser holds: suppress/tool mode, a tool call that opened inside the think block (mode stays THINKING until it completes), and Responses reasoning the client did not opt into withreasoning.summary: prefillkeepalive; same assumption (SSE parsers drop comment lines), samesend_allfailure handlingWhy
A multi-thousand-token tool call sends nothing for minutes: the raw text is withheld until the call parses. Clients with an HTTP idle timeout abort at that point (pi and Node's undici both default to 300 s), then retry the identical prompt. On a model whose recurrent state cannot rewind (Qwen 3.8 Flash Next on the qwen branch) the retry is a full re-prefill. Observed on an M1 Max: one 33K-token agent turn cost three 9-minute prefills plus the abandoned generations before the client gave up. The same silence exists for DeepSeek and GLM tool calls on
main; those retries are only cheaper, not absent.Validation
Machine: MacBook Pro M1 Max 32-core GPU, 64 GB, macOS 26.6.2. Backend: Metal.
Unit tests on this branch (
main+ this commit):Live A/B. The change is server-only, so it was exercised through the same diff applied on top of
ivanfioravanti/ds4-metal@qwen3.8-flash-next(18ca8ec) servingQwen3.8-Flash-Next-IQ2XXSImatrix-Q2KDownPad768-MTP.gguf+PLE-Q4_1,--ctx 65536 --prefill-chunk 1024 --mtp. "Keepalive off" is the same build with the helper compiled to a no-op (an env toggle used only for this A/B, since removed). The client iscurl -N --speed-limit 1 --speed-time 20: it aborts when fewer than 1 byte/s arrives for 20 s, i.e. a 20-second idle timeout. Each request asks for awrite_filetool call carrying a ~900-word essay (~60 s of generation)./v1/chat/completions/v1/messages/v1/responsesSame setup with the real client that hit this: pi (
@earendil-works/pi-coding-agent0.85.1) with its idle timeout set to 20 s and itswritetool, keepalive off:terminated, no file, one retry then give-up. Keepalive on: 1,543-word file written, exit 0.Not run:
--logprob-vectors,--long-contextand--tool-call-qualityneedds4flash.gguf, which is not on this machine. The change does not touch inference, prompt rendering or the transcript; it only writes comment lines to the client socket while nothing else is being written.🤖 Generated with Claude Code