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

This commit is contained in:
Ahmed Allam
2026-07-17 18:40:23 -07:00
committed by Ahmed Allam
parent 9aa151c687
commit 3bb95ab43d
9 changed files with 77 additions and 4 deletions
+28
View File
@@ -155,3 +155,31 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
)
assert settings.tool_choice == "required"
def test_make_model_settings_sets_request_timeout() -> None:
settings = make_model_settings(
"none",
model_name="gpt-4o",
request_timeout=300.0,
)
assert settings.extra_args == {"timeout": 300.0}
def test_make_model_settings_omits_timeout_when_unset() -> None:
settings = make_model_settings("none", model_name="gpt-4o")
assert settings.extra_args is None
def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
# Reasoning is resolved via ModelSettings.resolve(); the timeout in extra_args
# must not be dropped when a reasoning override is merged in.
settings = make_model_settings(
"high",
model_name="openai/o3",
request_timeout=120.0,
)
assert settings.extra_args == {"timeout": 120.0}
+8
View File
@@ -55,6 +55,14 @@ def test_rate_limit_and_server_errors_are_retried() -> None:
assert _retries(ModelRetryNormalizedError(status_code=status)) is True
def test_timeout_error_is_retried() -> None:
# A stalled model stream trips the per-request read/inactivity timeout, which
# the SDK normalizes as a timeout. DEFAULT_MODEL_RETRY must retry it so a hung
# turn recovers instead of silently wedging the agent.
assert _retries(ModelRetryNormalizedError(is_timeout=True)) is True
assert _retries(ModelRetryNormalizedError(is_network_error=True)) is True
def test_policy_helper_matches_statusless_only() -> None:
assert _retry_statusless_provider_errors(_context(ModelRetryNormalizedError())) is True
assert (
+15 -1
View File
@@ -4,7 +4,11 @@ from __future__ import annotations
import pytest
from strix.config.models import RECOMMENDED_MODEL_NAMES, is_recommended_or_frontier_model
from strix.config.models import (
RECOMMENDED_MODEL_NAMES,
is_recommended_or_frontier_model,
request_timeout_extra_args,
)
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
@@ -12,6 +16,16 @@ def test_recommended_models_are_accepted(model_name: str) -> None:
assert is_recommended_or_frontier_model(model_name)
def test_request_timeout_extra_args_positive() -> None:
assert request_timeout_extra_args(300) == {"timeout": 300.0}
assert request_timeout_extra_args(120.5) == {"timeout": 120.5}
@pytest.mark.parametrize("value", [None, 0, -1])
def test_request_timeout_extra_args_disabled(value: float | None) -> None:
assert request_timeout_extra_args(value) is None
def test_recommended_models_are_matched_case_insensitively() -> None:
assert is_recommended_or_frontier_model("Vertex_AI/Gemini-3-Pro-Preview")
+1
View File
@@ -37,6 +37,7 @@ async def test_persistent_rate_limit_stops_gracefully(
model="openai/gpt-4o",
reasoning_effort="high",
force_required_tool_choice=False,
timeout=300,
),
runtime=types.SimpleNamespace(max_context_images=3),
)
+2 -2
View File
@@ -45,6 +45,7 @@ def _patch_engine_scaffold(
model="openai/gpt-4o",
reasoning_effort="high",
force_required_tool_choice=False,
timeout=300,
)
)
monkeypatch.setattr(runner, "load_settings", lambda: settings)
@@ -124,8 +125,7 @@ async def test_root_prompt_options_flow_into_root_agent(
assert "https://example.com" in instructions_override
assert "CUSTOM SCAN PROMPT" in instructions_override
assert (
"cannot expand, replace, or weaken authorized target constraints"
in instructions_override
"cannot expand, replace, or weaken authorized target constraints" in instructions_override
)
assert kwargs["system_prompt_context"] == {
**scope_context,