feat(providers): support provider-scoped proxy config
This commit is contained in:
@@ -21,9 +21,12 @@ from nanobot.providers.openai_codex_provider import (
|
||||
|
||||
|
||||
def _mock_codex_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_token(**_kwargs):
|
||||
return SimpleNamespace(account_id="acct", access="token")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"nanobot.providers.openai_codex_provider.get_codex_token",
|
||||
lambda: SimpleNamespace(account_id="acct", access="token"),
|
||||
fake_token,
|
||||
)
|
||||
|
||||
|
||||
@@ -77,7 +80,12 @@ async def test_codex_request_non_200_populates_http_metadata(monkeypatch) -> Non
|
||||
request=request,
|
||||
)
|
||||
|
||||
def fake_client(*, timeout: int, verify: bool) -> httpx.AsyncClient:
|
||||
def fake_client(
|
||||
*,
|
||||
timeout: int,
|
||||
verify: bool,
|
||||
**_kwargs: object,
|
||||
) -> httpx.AsyncClient:
|
||||
assert timeout == 90
|
||||
assert verify is True
|
||||
return original_client(transport=httpx.MockTransport(handler), timeout=timeout)
|
||||
@@ -106,7 +114,12 @@ async def test_codex_request_honors_stream_idle_timeout_env(monkeypatch) -> None
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, request=request)
|
||||
|
||||
def fake_client(*, timeout: int, verify: bool) -> httpx.AsyncClient:
|
||||
def fake_client(
|
||||
*,
|
||||
timeout: int,
|
||||
verify: bool,
|
||||
**_kwargs: object,
|
||||
) -> httpx.AsyncClient:
|
||||
seen["timeout"] = timeout
|
||||
return original_client(transport=httpx.MockTransport(handler), timeout=timeout)
|
||||
|
||||
@@ -117,6 +130,39 @@ async def test_codex_request_honors_stream_idle_timeout_env(monkeypatch) -> None
|
||||
assert seen["timeout"] == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_request_uses_configured_proxy(monkeypatch) -> None:
|
||||
original_client = httpx.AsyncClient
|
||||
seen: dict[str, object] = {}
|
||||
proxy = "http://127.0.0.1:23458"
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, request=request)
|
||||
|
||||
def fake_client(
|
||||
*,
|
||||
timeout: int,
|
||||
verify: bool,
|
||||
proxy: str | None = None,
|
||||
trust_env: bool = True,
|
||||
) -> httpx.AsyncClient:
|
||||
seen["proxy"] = proxy
|
||||
seen["trust_env"] = trust_env
|
||||
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,
|
||||
proxy=proxy,
|
||||
)
|
||||
|
||||
assert seen == {"proxy": proxy, "trust_env": False}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_prompt_cache_key_uses_stable_conversation_prefix(monkeypatch) -> None:
|
||||
bodies: list[dict] = []
|
||||
@@ -128,11 +174,12 @@ async def test_codex_prompt_cache_key_uses_stable_conversation_prefix(monkeypatc
|
||||
headers,
|
||||
body,
|
||||
verify,
|
||||
proxy=None,
|
||||
on_content_delta=None,
|
||||
on_thinking_delta=None,
|
||||
on_tool_call_delta=None,
|
||||
):
|
||||
_ = on_thinking_delta, on_tool_call_delta
|
||||
_ = proxy, on_thinking_delta, on_tool_call_delta
|
||||
bodies.append(body)
|
||||
return "ok", [], "stop", {}, None
|
||||
|
||||
@@ -186,6 +233,40 @@ async def test_codex_timeout_error_is_typed_and_retryable(monkeypatch) -> None:
|
||||
assert response.error_should_retry is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_provider_passes_proxy_to_oauth_and_response_request(monkeypatch) -> None:
|
||||
proxy = "http://127.0.0.1:23458"
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
def fake_token(*, proxy=None):
|
||||
seen["token_proxy"] = proxy
|
||||
return SimpleNamespace(account_id="acct", access="token")
|
||||
|
||||
async def fake_request(
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
verify,
|
||||
proxy=None,
|
||||
on_content_delta=None,
|
||||
on_thinking_delta=None,
|
||||
on_tool_call_delta=None,
|
||||
):
|
||||
_ = url, headers, body, verify, on_content_delta, on_thinking_delta, on_tool_call_delta
|
||||
seen["request_proxy"] = proxy
|
||||
return "ok", [], "stop", {}, None
|
||||
|
||||
monkeypatch.setattr("nanobot.providers.openai_codex_provider.get_codex_token", fake_token)
|
||||
monkeypatch.setattr("nanobot.providers.openai_codex_provider._request_codex", fake_request)
|
||||
|
||||
provider = OpenAICodexProvider(proxy=proxy)
|
||||
response = await provider.chat([{"role": "user", "content": "hello"}])
|
||||
|
||||
assert response.content == "ok"
|
||||
assert seen["token_proxy"] == proxy
|
||||
assert seen["request_proxy"] == proxy
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_timeout_error_writes_diagnostic_log(monkeypatch) -> None:
|
||||
log_capture = _capture_codex_warnings(monkeypatch)
|
||||
@@ -409,9 +490,12 @@ def test_codex_reasoning_options_request_summary_without_forcing_effort() -> Non
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_stream_surfaces_reasoning_summary(monkeypatch) -> None:
|
||||
def fake_token(**_kwargs):
|
||||
return SimpleNamespace(account_id="acct", access="token")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"nanobot.providers.openai_codex_provider.get_codex_token",
|
||||
lambda: SimpleNamespace(account_id="acct", access="token"),
|
||||
fake_token,
|
||||
)
|
||||
|
||||
async def fake_request(
|
||||
@@ -419,11 +503,12 @@ async def test_codex_stream_surfaces_reasoning_summary(monkeypatch) -> None:
|
||||
headers,
|
||||
body,
|
||||
verify,
|
||||
proxy=None,
|
||||
on_content_delta=None,
|
||||
on_thinking_delta=None,
|
||||
on_tool_call_delta=None,
|
||||
):
|
||||
_ = url, headers, verify, on_tool_call_delta
|
||||
_ = url, headers, verify, proxy, on_tool_call_delta
|
||||
assert body["reasoning"] == {"summary": "auto", "effort": "medium"}
|
||||
if on_content_delta:
|
||||
await on_content_delta("answer")
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
|
||||
import nanobot.providers.openai_compat_provider as openai_compat_provider
|
||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||
|
||||
|
||||
@@ -54,3 +55,32 @@ class TestCloudEndpointProxyEnabled:
|
||||
client = provider._client._client
|
||||
# trust_env should be True so httpx reads HTTP_PROXY etc.
|
||||
assert client._trust_env is True
|
||||
|
||||
async def test_explicit_provider_proxy_overrides_env(self, monkeypatch):
|
||||
spec = _make_spec(is_local=False)
|
||||
spec.env_key = ""
|
||||
spec.default_api_base = "https://api.openai.com/v1"
|
||||
proxy = "http://127.0.0.1:23458"
|
||||
monkeypatch.delenv("NANOBOT_OPENAI_COMPAT_TIMEOUT_S", raising=False)
|
||||
|
||||
http_client = MagicMock()
|
||||
async_client = MagicMock(return_value=http_client)
|
||||
openai_client = MagicMock(return_value=object())
|
||||
monkeypatch.setattr(httpx, "AsyncClient", async_client)
|
||||
monkeypatch.setattr(openai_compat_provider, "AsyncOpenAI", openai_client)
|
||||
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="test",
|
||||
api_base=None,
|
||||
spec=spec,
|
||||
proxy=proxy,
|
||||
)
|
||||
provider._build_client()
|
||||
|
||||
async_client.assert_called_once_with(
|
||||
timeout=120.0,
|
||||
proxy=proxy,
|
||||
trust_env=False,
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert openai_client.call_args.kwargs["http_client"] is http_client
|
||||
|
||||
Reference in New Issue
Block a user