fix(models): split first-event and idle stream timeouts - #1156
fix(models): split first-event and idle stream timeouts#1156devin-ai-integration[bot] wants to merge 1 commit into
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR separates the model stream’s first-event timeout from its subsequent idle timeout and gives stream timeouts an independent retry limit.
Confidence Score: 5/5The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking issues identified. The timeout selection, configuration propagation, provider routing, cleanup behavior, and independent retry accounting are internally consistent with the documented behavior and covered by focused tests. Important Files Changed
Reviews (1): Last reviewed commit: "fix(models): split first-event and idle ..." | Re-trigger Greptile |
Summary
A provider that accepts a streaming request and then emits nothing is indistinguishable, to
_with_idle_timeout, from a stream that died mid-generation — both are "no event forLLM_STREAM_IDLE_TIMEOUT". That conflation is expensive in both directions: the idle budget has to stay generous enough to cover a slow prefill (large prompt, cold/queued upstream, reasoning before the first token), which makes real mid-stream hangs slow to detect, and a route that is silent on arrival gets the identical request replayed as a generic transient error — 5 attempts, each paying a full timeout window plus backoff, before the agent finally crashes.Two independent knobs now:
LLM_STREAM_FIRST_EVENT_TIMEOUTdefaults to0, meaning "inheritLLM_STREAM_IDLE_TIMEOUT", so an unconfigured deployment behaves exactly as before. Operators wanting the useful split set a tight idle budget and a generous first-event one:The messages stay distinguishable in logs and in the raised error (
produced no first event within Xsvs. the unchangedproduced no event for Xs), and both guards are still neutralised underLLM_DISABLE_STREAMING, where the wrapper's single event only arrives once the request is complete.The retry side changes behaviour by default, deliberately. The guard now raises
ModelStreamTimeoutError(aTimeoutErrorsubclass, so any other consumer's classification is unaffected) and_run_cyclegives it its own consecutive-retry cap of 2, counted separately from_MAX_TRANSIENT_MODEL_RETRIES = 5and reset by any other transient error. Retrying a stream timeout is not like retrying a 429: each attempt costs a full window, and a route that went silent once usually goes silent again — so a chronically silent upstream now surfaces after ~2 windows instead of burning 5.Prior art for the split: openclaw#65898 (
firstTokenTimeoutSecondsalongside the idle window), hermes-agent#32042 (TTFB watchdog while zero events have arrived), livekit/agents#6173 (bounding time-to-first-chunk so failover can happen); hermes-agent#7069 is the retry half of the problem — timeout mid-prefill, immediate replay, prefill restarts, never enough time to finish.Link to Devin session: https://app.devin.ai/sessions/5411e06b88bc4f52ac4330aa8d2a1ebe