fix: continue recovered streams in a new segment

maintainer edit: streamed timeout recovery was returning the retried response internally while the channel still treated the final outbound as already streamed. End the current stream segment before retry/fallback recovery so subsequent deltas are delivered in a new segment.
This commit is contained in:
chengyongru
2026-06-10 18:10:44 +08:00
committed by Xubin Ren
parent 2c5a4e0703
commit bc4bb508a1
6 changed files with 162 additions and 13 deletions
+43
View File
@@ -199,6 +199,49 @@ async def test_chat_stream_with_retry_retries_timeout_after_emitting_content(mon
assert provider.last_kwargs.get("on_content_delta") is None
@pytest.mark.asyncio
async def test_chat_stream_with_retry_retries_timeout_in_new_stream_segment(
monkeypatch,
) -> None:
first = LLMResponse(
content="Error calling LLM: stream stalled for more than 30 seconds",
finish_reason="error",
error_kind="timeout",
)
first._test_stream_delta = "partial" # type: ignore[attr-defined]
second = LLMResponse(content="full retry response")
second._test_stream_delta = "full retry response" # type: ignore[attr-defined]
provider = ScriptedProvider([first, second])
deltas: list[str] = []
recoveries: list[str] = []
delays: list[int] = []
async def _fake_sleep(delay: int) -> None:
delays.append(delay)
async def _on_delta(delta: str) -> None:
deltas.append(delta)
async def _on_stream_recover() -> None:
recoveries.append("recover")
monkeypatch.setattr("nanobot.providers.base.asyncio.sleep", _fake_sleep)
response = await provider.chat_stream_with_retry(
messages=[{"role": "user", "content": "hello"}],
on_content_delta=_on_delta,
on_stream_recover=_on_stream_recover,
)
assert response.content == "full retry response"
assert response.finish_reason == "stop"
assert provider.calls == 2
assert deltas == ["partial", "full retry response"]
assert recoveries == ["recover"]
assert delays == [1]
assert provider.last_kwargs.get("on_content_delta") is not None
@pytest.mark.asyncio
async def test_chat_with_retry_uses_provider_generation_defaults() -> None:
"""When callers omit generation params, provider.generation defaults are used."""