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] = []