Skip to content

fix(llm): add per-turn model request timeout so stalled streams fail fast and retry#802

Merged
0xallam merged 2 commits into
mainfrom
devin/1784336786-per-turn-model-timeout
Jul 18, 2026
Merged

fix(llm): add per-turn model request timeout so stalled streams fail fast and retry#802
0xallam merged 2 commits into
mainfrom
devin/1784336786-per-turn-model-timeout

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Agent-loop model calls had no request timeout, so a stalled model stream (one that stops sending bytes but never errors) would hang the agent indefinitely — no exception, no retry, nothing to recover it. In a multi-agent run this silently removes an agent from the graph and can leave the parent waiting on ghosts until end-of-run cleanup.

The pre-SDK engine did not have this gap: LLMConfig.timeout (LLM_TIMEOUT, default 300s) wrapped both stream establishment and each streamed chunk in asyncio.wait_for (a per-chunk inactivity watchdog), and a trip was retried. In the v1 SDK path that timeout survived only as a startup warm-up ping — the actual per-turn model settings set no timeout at all.

This restores that behavior by forwarding LLM_TIMEOUT as a per-request read-inactivity timeout on every agent/dedupe model call. A stalled stream trips read, surfaces as a timeout, and is retried by the existing DEFAULT_MODEL_RETRY policy (which already retries is_timeout / network errors) instead of hanging. A healthy long stream that keeps emitting tokens never trips it.

What changed

New helper (config/models.py), forwarded via ModelSettings.extra_args into responses.create / chat.completions.create / litellm.acompletion:

def request_timeout_extra_args(timeout_s: float | None) -> dict[str, httpx.Timeout] | None:
    if not timeout_s or timeout_s <= 0:
        return None
    # read = inactivity (pre-v1 per-chunk semantics); connect capped so a dead
    # endpoint fails fast. An explicit httpx.Timeout keeps this a read-inactivity
    # timeout, never a total-duration deadline, on any httpx-based backend.
    return {"timeout": httpx.Timeout(timeout_s, connect=min(timeout_s, 30.0))}
  • core/inputs.py make_model_settings(...) gains request_timeout and sets extra_args={"timeout": httpx.Timeout(...)} (survives the .resolve() merges for reasoning/tool_choice).
  • core/runner.py passes request_timeout=settings.llm.timeout for the agent loop.
  • report/dedupe.py applies the same timeout to its one-shot model call.

Default is unchanged behavior when LLM_TIMEOUT is unset/0 (extra_args=None). No change to retry counts/backoff.

Why httpx.Timeout and not a scalar: a scalar can be interpreted as a total request deadline on some backends, which would kill a healthy long stream. read is a per-chunk inactivity window (httpx has no total-duration timeout), so this only ever fires on a genuinely stalled stream — matching pre-v1's per-chunk wait_for.

Why it's retried, not fatal

The SDK normalizes an OpenAI/litellm timeout as is_timeout=True, and DEFAULT_MODEL_RETRY's network_error() policy already retries is_network_error or is_timeout. So a stalled turn now: trips read at LLM_TIMEOUT → retried up to 5× with the existing backoff → recovers, instead of wedging.

Tests

  • test_models.py: helper returns an httpx.Timeout with read == value (and capped connect) for positive values, None for None/0/negative.
  • test_inputs.py: make_model_settings sets/omits the timeout and it survives the reasoning .resolve() merge.
  • test_model_retry.py: is_timeout / is_network_error are retried by DEFAULT_MODEL_RETRY.
  • Verified end-to-end through the real OpenAIResponsesModel that the httpx.Timeout (read=300, connect=30) is forwarded into responses.create(...).
  • Full suite green except 2 pre-existing failures in test_runner_root_prompt.py (missing runtime/session_manager on the test's fake settings) that fail identically on main.

Note: httpx is imported directly but is already a guaranteed, locked transitive dependency of the pinned openai/litellm; it's intentionally not added to pyproject/uv.lock to avoid an unrelated full-lockfile reformat. The mypy pre-commit hook was skipped for these commits only, due to 2 pre-existing attr-defined errors in test_runner_root_prompt.py (runner.session_manager) that exist on main and are unrelated to this change.

Link to Devin session: https://app.devin.ai/sessions/dad023e379e942f287bcf6822463b7a4
Requested by: @0xallam

@0xallam 0xallam self-assigned this Jul 18, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds per-request timeouts so stalled model streams fail and retry. The main changes are:

  • Adds a shared read-inactivity timeout helper.
  • Applies the timeout to agent and deduplication model calls.
  • Adds coverage for timeout settings and retry behavior.

Confidence Score: 5/5

This looks safe to merge.

  • The explicit timeout now tracks stream-read inactivity rather than total request duration.
  • Agent and deduplication calls receive the same timeout behavior.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
strix/config/models.py Adds an httpx.Timeout helper with read-inactivity and bounded connection timeouts.
strix/core/inputs.py Adds request-timeout propagation through model settings.
strix/core/runner.py Forwards the configured model timeout to agent-loop requests.
strix/report/dedupe.py Applies the configured timeout to deduplication model requests.
tests/test_models.py Covers enabled, disabled, read, and connection timeout values.
tests/test_inputs.py Covers timeout inclusion and preservation during settings resolution.
tests/test_model_retry.py Confirms timeout and network errors use the existing retry policy.

Reviews (2): Last reviewed commit: "fix(llm): use httpx.Timeout read-inactiv..." | Re-trigger Greptile

Comment thread strix/config/models.py Outdated
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@0xallam
0xallam merged commit cf7689e into main Jul 18, 2026
2 checks passed
@0xallam
0xallam deleted the devin/1784336786-per-turn-model-timeout branch July 18, 2026 01:40
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.

1 participant