mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 16:38:49 +00:00
fix(agent): add wall-clock timeout for streaming LLM requests
This commit is contained in:
parent
6c9e3a2cc3
commit
11eb9d8cc8
@ -806,11 +806,17 @@ class AgentRunner:
|
|||||||
else:
|
else:
|
||||||
coro = spec.runtime.provider.chat_with_retry(**kwargs)
|
coro = spec.runtime.provider.chat_with_retry(**kwargs)
|
||||||
|
|
||||||
# Streaming requests already have provider-level idle timeouts
|
# Streaming requests also have provider-level idle timeouts
|
||||||
# (NANOBOT_STREAM_IDLE_TIMEOUT_S). Do not also apply the outer wall-clock
|
# (NANOBOT_STREAM_IDLE_TIMEOUT_S), but a stream that keeps producing
|
||||||
# LLM timeout here, or healthy long reasoning streams can be killed just
|
# very slow deltas can still run forever. Use a more generous wall-clock
|
||||||
# because total elapsed time exceeded NANOBOT_LLM_TIMEOUT_S.
|
# timeout for streaming while preserving NANOBOT_LLM_TIMEOUT_S=0 as an
|
||||||
outer_timeout_s = None if (wants_streaming or wants_progress_streaming) else timeout_s
|
# opt-out for all LLM wall-clock timeouts.
|
||||||
|
is_streaming_request = wants_streaming or wants_progress_streaming
|
||||||
|
outer_timeout_s = (
|
||||||
|
max(300.0, timeout_s * 2)
|
||||||
|
if is_streaming_request and timeout_s is not None
|
||||||
|
else timeout_s
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
response = (
|
response = (
|
||||||
await coro if outer_timeout_s is None
|
await coro if outer_timeout_s is None
|
||||||
|
|||||||
@ -189,7 +189,7 @@ async def test_runner_times_out_hung_llm_request():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
|
async def test_runner_applies_outer_wall_timeout_to_streaming_requests():
|
||||||
from nanobot.agent.hook import AgentHook, AgentHookContext
|
from nanobot.agent.hook import AgentHook, AgentHookContext
|
||||||
from nanobot.agent.runner import AgentRunner
|
from nanobot.agent.runner import AgentRunner
|
||||||
|
|
||||||
@ -216,8 +216,13 @@ async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
|
|||||||
streamed.append(delta)
|
streamed.append(delta)
|
||||||
|
|
||||||
runner = AgentRunner()
|
runner = AgentRunner()
|
||||||
wait_for = AsyncMock(side_effect=AssertionError("streaming path must not use wait_for"))
|
wait_for_calls: list[float] = []
|
||||||
with patch("nanobot.agent.runner.asyncio.wait_for", wait_for):
|
|
||||||
|
async def fake_wait_for(coro, *, timeout):
|
||||||
|
wait_for_calls.append(timeout)
|
||||||
|
return await coro
|
||||||
|
|
||||||
|
with patch("nanobot.agent.runner.asyncio.wait_for", fake_wait_for):
|
||||||
result = await runner.run(make_run_spec(provider,
|
result = await runner.run(make_run_spec(provider,
|
||||||
initial_messages=[{"role": "user", "content": "think for a while"}],
|
initial_messages=[{"role": "user", "content": "think for a while"}],
|
||||||
tools=tools,
|
tools=tools,
|
||||||
@ -232,7 +237,47 @@ async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
|
|||||||
assert result.final_content == "still alive"
|
assert result.final_content == "still alive"
|
||||||
assert streamed == ["still ", "alive"]
|
assert streamed == ["still ", "alive"]
|
||||||
provider.chat_with_retry.assert_not_awaited()
|
provider.chat_with_retry.assert_not_awaited()
|
||||||
wait_for.assert_not_awaited()
|
assert wait_for_calls == [300.0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_runner_times_out_never_ending_streaming_request():
|
||||||
|
from nanobot.agent.hook import AgentHook
|
||||||
|
from nanobot.agent.runner import AgentRunner
|
||||||
|
|
||||||
|
provider = MagicMock(spec=LLMProvider)
|
||||||
|
|
||||||
|
async def chat_stream_with_retry(*, on_content_delta, **kwargs):
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
|
||||||
|
provider.chat_stream_with_retry = chat_stream_with_retry
|
||||||
|
provider.chat_with_retry = AsyncMock()
|
||||||
|
tools = MagicMock()
|
||||||
|
tools.get_definitions.return_value = []
|
||||||
|
|
||||||
|
class StreamingHook(AgentHook):
|
||||||
|
def wants_streaming(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def fake_wait_for(coro, *, timeout):
|
||||||
|
coro.close()
|
||||||
|
raise asyncio.TimeoutError
|
||||||
|
|
||||||
|
runner = AgentRunner()
|
||||||
|
with patch("nanobot.agent.runner.asyncio.wait_for", fake_wait_for):
|
||||||
|
result = await runner.run(make_run_spec(provider,
|
||||||
|
initial_messages=[{"role": "user", "content": "think forever"}],
|
||||||
|
tools=tools,
|
||||||
|
model="test-model",
|
||||||
|
max_iterations=1,
|
||||||
|
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||||
|
hook=StreamingHook(),
|
||||||
|
llm_timeout_s=200,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert result.stop_reason == "error"
|
||||||
|
assert result.final_content == "Error calling LLM: timed out after 400s"
|
||||||
|
provider.chat_with_retry.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user