From ac8bef76f6c5e99080e174b63f882a71cf758f39 Mon Sep 17 00:00:00 2001 From: yeounhyeok Date: Wed, 27 May 2026 19:58:30 +0900 Subject: [PATCH] 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. --- nanobot/providers/openai_codex_provider.py | 4 ++- tests/providers/test_openai_codex_provider.py | 25 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/nanobot/providers/openai_codex_provider.py b/nanobot/providers/openai_codex_provider.py index dee66c073..c0afdf572 100644 --- a/nanobot/providers/openai_codex_provider.py +++ b/nanobot/providers/openai_codex_provider.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import hashlib import json +import os from collections.abc import Awaitable, Callable from typing import Any @@ -177,7 +178,8 @@ async def _request_codex( on_content_delta: Callable[[str], Awaitable[None]] | None = None, on_tool_call_delta: Callable[[dict[str, Any]], Awaitable[None]] | None = None, ) -> 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: if response.status_code != 200: text = await response.aread() diff --git a/tests/providers/test_openai_codex_provider.py b/tests/providers/test_openai_codex_provider.py index 02e43638a..b3089e994 100644 --- a/tests/providers/test_openai_codex_provider.py +++ b/tests/providers/test_openai_codex_provider.py @@ -76,8 +76,8 @@ async def test_codex_request_non_200_populates_http_metadata(monkeypatch) -> Non request=request, ) - def fake_client(*, timeout: float, verify: bool) -> httpx.AsyncClient: - assert timeout == 60.0 + def fake_client(*, timeout: int, verify: bool) -> httpx.AsyncClient: + assert timeout == 90 assert verify is True 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 +@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 async def test_codex_prompt_cache_key_uses_stable_conversation_prefix(monkeypatch) -> None: bodies: list[dict] = []