Skip to content

fix: stop applying the 45s SSE open timeout to non-streaming chat requests - #4743

Closed
vibecoding-skills wants to merge 1 commit into
Hmbown:mainfrom
vibecoding-skills:fix/nonstream-response-timeout
Closed

fix: stop applying the 45s SSE open timeout to non-streaming chat requests#4743
vibecoding-skills wants to merge 1 commit into
Hmbown:mainfrom
vibecoding-skills:fix/nonstream-response-timeout

Conversation

@vibecoding-skills

Copy link
Copy Markdown

Problem

codewhale exec (plain, non---auto) against any non-streaming backend aborts with a misleading error whenever generation takes longer than 45 seconds:

Error: SSE stream request did not receive response headers after 45s. `codewhale doctor` can still
pass when non-streaming requests work; on Windows or proxy networks, try `DEEPSEEK_FORCE_HTTP1=1`
and rerun `codewhale`.

This makes plain exec effectively unusable with local models (Ollama, llama-server, vLLM without streaming) on any real-world prompt: short prompts finish under the wire, but e.g. a "rewrite this 300-line file" task is killed mid-generation. On the server side Ollama logs a canceled request (error reading llama-server chat response: context canceled).

Reproduction

codewhale --provider ollama --model <local model> exec "<prompt whose completion takes > 45s>"

Packet capture shows the request is delivered in full immediately; the client then closes the connection 45s later while the server is still generating. DEEPSEEK_FORCE_HTTP1=1 (which the error suggests) does not help — the transport is fine.

Cause

The bounded header wait was attached to the wrong path in crates/tui/src/client/chat.rs:

  • create_message_chat — the non-streaming path (no "stream": true in the body) — wraps send_json_with_retry in tokio_timeout(stream_open_timeout(), …). But a non-streaming server only returns response headers after the entire generation completes, so the 45s "connection setup" budget races the model's full thinking + generation time. The doc comment on stream_open_timeout itself says the timeout "only covers connection setup and upstream header return, not model thinking time after streaming has started" — which is only true when streaming is actually requested.
  • handle_chat_completion_stream — the SSE path the timeout was written for — had no open timeout at all, so the Windows/proxy header hang described in that comment was still unbounded there.

Fix

  • create_message_chat now uses a non-streaming response budget: default 600s, overridable via CODEWHALE_NONSTREAM_TIMEOUT_SECS (clamped 30..3600), with an error message that explains that non-streaming servers legitimately hold the response for the whole generation.
  • handle_chat_completion_stream now applies stream_open_timeout() to its header wait, as originally intended (existing 45s default and CODEWHALE_STREAM_OPEN_TIMEOUT_SECS override unchanged).
  • Adds a clamp unit test for the new env override, mirroring stream_open_timeout_defaults_and_clamps_env_values.

Testing

  • cargo check -p codewhale-tui clean
  • cargo fmt applied; cargo clippy -p codewhale-tui --bin codewhale-tui clean
  • cargo test -p codewhale-tui --bin codewhale-tui timeout — 53 passed, 0 failed (includes the new test)
  • Manually verified against Ollama on macOS with a ~5k-token prompt whose completion takes several minutes: released 0.9.0 aborts at exactly 45s with the SSE error above (packet capture shows the client closing the connection mid-generation); a debug build of this branch runs the identical exec invocation for 2m42s and exits 0.

…uests

The bounded stream-open wait (DEFAULT_STREAM_OPEN_TIMEOUT, 45s) was wrapped
around create_message_chat — the non-streaming path — where response headers
only arrive after the server finishes the ENTIRE generation. Any non-streaming
completion slower than 45s (e.g. a local model on Ollama with a real-world
prompt) was aborted mid-generation with a misleading SSE error, while
handle_chat_completion_stream — the SSE path the timeout was written for,
per its own doc comment — had no open timeout at all and could still hang
forever on the Windows/proxy paths described there.

- create_message_chat now uses a non-streaming response budget:
  default 600s, overridable via CODEWHALE_NONSTREAM_TIMEOUT_SECS
  (clamped 30..3600), with an error message that explains the wait.
- handle_chat_completion_stream now applies stream_open_timeout() to the
  header wait, as originally intended.
- Adds a clamp unit test mirroring the existing stream_open_timeout one.

Repro: codewhale --provider ollama --model <any local model> exec '<prompt
long enough that generation exceeds 45s>' -> 'SSE stream request did not
receive response headers after 45s' and Ollama logs a canceled request.
@github-actions

Copy link
Copy Markdown
Contributor

Thanks @vibecoding-skills for taking the time to contribute.

This repository is observing a maintainer-managed PR intake gate in dry-run mode, so this pull request is staying open. This note helps maintainers prepare the allowlist before any enforcement is considered.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant recurring PR access by commenting /lgtm on a pull request.

@Hmbown

Hmbown commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Thank you for this one, @vibecoding-skills — the analysis was correct, and it deserves a straight answer about why it is closing without merging.

Your report was accurate when you filed it. Both halves are now fixed on main by a374d196e ("refactor(client): share stream opening across wire adapters"):

  1. The non-streaming path no longer carries the SSE open timeout. crates/tui/src/client/chat.rs:443, inside create_message_chat, is now a bare self.send_json_with_retry(&url, &body).await? — the tokio_timeout wrapper you identified is gone from exactly that spot.
  2. The SSE path did keep a bounded open wait, which is the right outcome: stream_open_timeout() is now used at :477 in open_chat_stream_response, threaded through StreamOpenRequest. rg -n 'tokio_timeout' crates/tui/src/client/chat.rs shows only the import and the per-chunk idle timeout at :708.

So codewhale exec against Ollama or llama-server no longer dies at 45 s — which was the user-visible cost you were chasing.

Worth being explicit: an earlier triage pass of ours had this queued as an open bug to harvest. Re-checking against main overturned that. Your PR was right, our tracking was stale, and I'd rather say so than close this as if it had never been a real finding. I'm adding you to .github/AUTHOR_MAP so any future harvest in this area attributes to you.

@Hmbown Hmbown closed this Jul 26, 2026
Hmbown added a commit that referenced this pull request Jul 26, 2026
Each of these people had a PR triaged during the v0.9.2 pass, and three of
them had work reach main without a Co-authored-by trailer because they were
not in this file. Adding them so future harvests attribute correctly and the
check-coauthor-trailers gate can resolve them.

- EvanProgramming — #4760; equivalent change landed as 0d73127 with no
  trailer.
- adity982 — #4756; the MCP call-once fix landed as cccaa4f with no trailer.
- vibecoding-skills — #4743; the SSE-timeout split landed as a374d19.
- XhesicaFrost — #4610, pending harvest for #4520.
- ffaacceelee — #4686, closed without harvest, mapped pre-emptively.

Numeric noreply ids verified against the GitHub users API rather than
inferred, since a wrong id silently drops credit out of the contributor graph
instead of failing loudly.
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.

2 participants