fix(provider): honor NANOBOT_STREAM_IDLE_TIMEOUT_S in Codex provider

Every other streaming provider (anthropic, bedrock, openai_compat,
litellm) reads NANOBOT_STREAM_IDLE_TIMEOUT_S with a 90s default. The
Codex provider hardcoded 60s in _request_codex, so it could not be
tuned the same way and aborted streams sooner than its peers.

Read the same env var with the same default and pass it as the httpx
client timeout. The variable name and int parsing match anthropic /
openai_compat / bedrock verbatim.

#4009 normalized the error response when the timeout fires; this PR
fixes the timeout knob itself.
This commit is contained in:
yeounhyeok 2026-05-27 19:58:30 +09:00 committed by Xubin Ren
parent 1cfc3ef165
commit ac8bef76f6
2 changed files with 26 additions and 3 deletions

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
import hashlib import hashlib
import json import json
import os
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
@ -177,7 +178,8 @@ async def _request_codex(
on_content_delta: Callable[[str], Awaitable[None]] | None = None, on_content_delta: Callable[[str], Awaitable[None]] | None = None,
on_tool_call_delta: Callable[[dict[str, Any]], Awaitable[None]] | None = None, on_tool_call_delta: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
) -> tuple[str, list[ToolCallRequest], str]: ) -> tuple[str, list[ToolCallRequest], str]:
async with httpx.AsyncClient(timeout=60.0, verify=verify) as client: idle_timeout_s = int(os.environ.get("NANOBOT_STREAM_IDLE_TIMEOUT_S", "90"))
async with httpx.AsyncClient(timeout=idle_timeout_s, verify=verify) as client:
async with client.stream("POST", url, headers=headers, json=body) as response: async with client.stream("POST", url, headers=headers, json=body) as response:
if response.status_code != 200: if response.status_code != 200:
text = await response.aread() text = await response.aread()

View File

@ -76,8 +76,8 @@ async def test_codex_request_non_200_populates_http_metadata(monkeypatch) -> Non
request=request, request=request,
) )
def fake_client(*, timeout: float, verify: bool) -> httpx.AsyncClient: def fake_client(*, timeout: int, verify: bool) -> httpx.AsyncClient:
assert timeout == 60.0 assert timeout == 90
assert verify is True assert verify is True
return original_client(transport=httpx.MockTransport(handler), timeout=timeout) return original_client(transport=httpx.MockTransport(handler), timeout=timeout)
@ -95,6 +95,27 @@ async def test_codex_request_non_200_populates_http_metadata(monkeypatch) -> Non
assert error.should_retry is True assert error.should_retry is True
@pytest.mark.asyncio
async def test_codex_request_honors_stream_idle_timeout_env(monkeypatch) -> None:
"""NANOBOT_STREAM_IDLE_TIMEOUT_S overrides the default Codex stream timeout."""
monkeypatch.setenv("NANOBOT_STREAM_IDLE_TIMEOUT_S", "5")
original_client = httpx.AsyncClient
seen: dict[str, int] = {}
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, request=request)
def fake_client(*, timeout: int, verify: bool) -> httpx.AsyncClient:
seen["timeout"] = timeout
return original_client(transport=httpx.MockTransport(handler), timeout=timeout)
monkeypatch.setattr("nanobot.providers.openai_codex_provider.httpx.AsyncClient", fake_client)
await _request_codex("https://codex.example/responses", {}, {"input": []}, verify=True)
assert seen["timeout"] == 5
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_codex_prompt_cache_key_uses_stable_conversation_prefix(monkeypatch) -> None: async def test_codex_prompt_cache_key_uses_stable_conversation_prefix(monkeypatch) -> None:
bodies: list[dict] = [] bodies: list[dict] = []