fix: bound stream open waits (#847)

This commit is contained in:
Hunter Bown
2026-05-06 05:16:44 -05:00
committed by GitHub
parent 9b47ecfa4a
commit 67eddd6344
+65 -3
View File
@@ -17,6 +17,33 @@ use tokio::time::timeout as tokio_timeout;
/// yields a recoverable error so the caller can retry.
const DEFAULT_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
/// Default timeout for the initial streaming response headers.
///
/// `doctor` uses a bounded non-streaming request, but normal TUI turns first
/// wait for the SSE response to open. On some Windows/proxy paths that wait can
/// hang before any stream chunk exists, leaving the UI stuck at "Working...".
const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(45);
/// Reads `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` as a bounded override for the
/// response-header wait. This is intentionally shorter than the per-chunk idle
/// timeout because it only covers connection setup and upstream header return,
/// not model thinking time after streaming has started.
fn stream_open_timeout() -> Duration {
stream_open_timeout_from_env(
std::env::var("DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS")
.ok()
.as_deref(),
)
}
fn stream_open_timeout_from_env(value: Option<&str>) -> Duration {
let secs = value
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_STREAM_OPEN_TIMEOUT.as_secs())
.clamp(5, 300);
Duration::from_secs(secs)
}
/// Reads the `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var, falling back to
/// the default 300s. The parsed value is clamped to [1, 3600] seconds.
fn stream_idle_timeout() -> Duration {
@@ -75,9 +102,23 @@ impl DeepSeekClient {
);
let url = api_url(&self.base_url, "chat/completions");
let response = self
.send_with_retry(|| self.http_client.post(&url).json(&body))
.await?;
let open_timeout = stream_open_timeout();
let response = match tokio_timeout(
open_timeout,
self.send_with_retry(|| self.http_client.post(&url).json(&body)),
)
.await
{
Ok(result) => result?,
Err(_elapsed) => {
anyhow::bail!(
"SSE stream request did not receive response headers after {}s. \
`deepseek doctor` can still pass when non-streaming requests work; \
on Windows or proxy networks, try `DEEPSEEK_FORCE_HTTP1=1` and rerun `deepseek`.",
open_timeout.as_secs()
);
}
};
let status = response.status();
if !status.is_success() {
@@ -1290,6 +1331,27 @@ mod stream_diagnostics_tests {
use super::*;
use reqwest::header::{HeaderMap, HeaderValue};
#[test]
fn stream_open_timeout_defaults_and_clamps_env_values() {
assert_eq!(stream_open_timeout_from_env(None), Duration::from_secs(45));
assert_eq!(
stream_open_timeout_from_env(Some("not-a-number")),
Duration::from_secs(45)
);
assert_eq!(
stream_open_timeout_from_env(Some("1")),
Duration::from_secs(5)
);
assert_eq!(
stream_open_timeout_from_env(Some("120")),
Duration::from_secs(120)
);
assert_eq!(
stream_open_timeout_from_env(Some("999")),
Duration::from_secs(300)
);
}
#[test]
fn format_stream_headers_renders_all_fields_when_present() {
let mut headers = HeaderMap::new();