From 681edfa6f33b47d8bc0079e6c2d377a80cb92d39 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Wed, 15 Jul 2026 19:54:30 +0800 Subject: [PATCH] fix(providers): honor Codex proxy config consistently --- nanobot/agent/tools/image_generation.py | 1 + nanobot/cli/commands.py | 9 +++- nanobot/providers/image_generation.py | 11 ++++- tests/cli/test_commands.py | 46 +++++++++++++++++++ tests/providers/test_image_generation.py | 55 +++++++++++++++++++++++ tests/tools/test_image_generation_tool.py | 26 +++++++++++ 6 files changed, 145 insertions(+), 3 deletions(-) diff --git a/nanobot/agent/tools/image_generation.py b/nanobot/agent/tools/image_generation.py index 952a9352..f70e9429 100644 --- a/nanobot/agent/tools/image_generation.py +++ b/nanobot/agent/tools/image_generation.py @@ -129,6 +129,7 @@ class ImageGenerationTool(Tool): "api_base": provider.api_base if provider else None, "extra_headers": provider.extra_headers if provider else None, "extra_body": provider.extra_body if provider else None, + "proxy": provider.proxy if provider else None, } return cls(**kwargs) diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index c8e442e4..ae0714bf 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -2687,7 +2687,7 @@ def _set_oauth_provider_as_main( from nanobot.config.loader import get_config_path, load_config, save_config, set_config_path resolved_config_path = Path(config_path).expanduser().resolve() if config_path else None - if resolved_config_path is not None: + if resolved_config_path is not None and get_config_path() != resolved_config_path: set_config_path(resolved_config_path) console.print(f"[dim]Using config: {resolved_config_path}[/dim]") @@ -2731,6 +2731,13 @@ def provider_login( console.print(f"[red]Login not implemented for {spec.label}[/red]") raise typer.Exit(1) + if config: + from nanobot.config.loader import set_config_path + + resolved_config_path = Path(config).expanduser().resolve() + set_config_path(resolved_config_path) + console.print(f"[dim]Using config: {resolved_config_path}[/dim]") + console.print(f"{__logo__} OAuth Login - {spec.label}\n") handler() if set_main or model: diff --git a/nanobot/providers/image_generation.py b/nanobot/providers/image_generation.py index 91d08919..fe2a9f2e 100644 --- a/nanobot/providers/image_generation.py +++ b/nanobot/providers/image_generation.py @@ -187,6 +187,7 @@ class ImageGenerationProvider(ABC): api_base: str | None = None, extra_headers: dict[str, str] | None = None, extra_body: dict[str, Any] | None = None, + proxy: str | None = None, timeout: float | None = None, client: httpx.AsyncClient | None = None, ) -> None: @@ -194,6 +195,7 @@ class ImageGenerationProvider(ABC): self.api_base = self._resolve_base_url(api_base) self.extra_headers = extra_headers or {} self.extra_body = extra_body or {} + self.proxy = proxy or None self.timeout = timeout if timeout is not None else self.default_timeout self._client = client @@ -240,7 +242,11 @@ class ImageGenerationProvider(ABC): return await client.post(url, headers=headers, json=body) if self._client is not None: return await self._client.post(url, headers=headers, json=body) - async with httpx.AsyncClient(timeout=self.timeout) as c: + client_kwargs: dict[str, Any] = {"timeout": self.timeout} + if self.proxy: + client_kwargs["proxy"] = self.proxy + client_kwargs["trust_env"] = False + async with httpx.AsyncClient(**client_kwargs) as c: return await c.post(url, headers=headers, json=body) @@ -1233,7 +1239,8 @@ class CodexImageGenerationClient(ImageGenerationProvider): raise ImageGenerationError(self.missing_key_message) try: - token = await asyncio.to_thread(get_codex_token) + token_kwargs = {"proxy": self.proxy} if self.proxy else {} + token = await asyncio.to_thread(get_codex_token, **token_kwargs) except Exception as exc: raise ImageGenerationError(self.missing_key_message) from exc if not token or not token.access: diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index ee063ec5..0808dd93 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -711,6 +711,52 @@ def test_provider_login_openai_codex_passes_configured_proxy(monkeypatch): assert captured["proxy"] == proxy +def test_provider_login_openai_codex_uses_explicit_config_proxy(tmp_path, monkeypatch): + from nanobot.config import loader + + proxy = "http://127.0.0.1:23458" + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps({"providers": {"openaiCodex": {"proxy": proxy}}}), + encoding="utf-8", + ) + active_path: dict[str, Path] = {} + real_load_config = loader.load_config + + def fake_set_config_path(path: Path) -> None: + active_path["path"] = path + + def fake_load_config(config_path: Path | None = None) -> Config: + path = config_path or active_path.get("path") + if path is None: + return Config.model_validate( + {"providers": {"openaiCodex": {"proxy": "http://default-proxy:8080"}}} + ) + return real_load_config(path) + + monkeypatch.setattr(loader, "set_config_path", fake_set_config_path) + monkeypatch.setattr(loader, "load_config", fake_load_config) + + import oauth_cli_kit + + captured: dict[str, str | None] = {} + + def fake_get_token(*, proxy=None): + captured["proxy"] = proxy + return SimpleNamespace(access="access-token", account_id="acct-test") + + monkeypatch.setattr(oauth_cli_kit, "get_token", fake_get_token) + + result = runner.invoke( + app, + ["provider", "login", "openai-codex", "--config", str(config_path)], + ) + + assert result.exit_code == 0 + assert active_path["path"] == config_path.resolve() + assert captured["proxy"] == proxy + + def test_provider_login_openai_codex_resolves_proxy_env_ref(monkeypatch): proxy = "http://127.0.0.1:23458" monkeypatch.setenv("CODEX_PROXY_FOR_TEST", proxy) diff --git a/tests/providers/test_image_generation.py b/tests/providers/test_image_generation.py index 3e83b348..44b80549 100644 --- a/tests/providers/test_image_generation.py +++ b/tests/providers/test_image_generation.py @@ -1181,6 +1181,61 @@ async def test_codex_payload_and_response(monkeypatch) -> None: assert body["stream"] is True +@pytest.mark.asyncio +async def test_codex_proxy_applies_to_oauth_and_http(monkeypatch) -> None: + import sys + from types import SimpleNamespace + + proxy = "http://127.0.0.1:23458" + captured: dict[str, Any] = {} + + async def fake_to_thread(fn, *args, **kwargs): + return fn(*args, **kwargs) + + def fake_get_token(*, proxy=None): + captured["token_proxy"] = proxy + return SimpleNamespace(account_id="acct-123", access="oauth-token") + + fake_oauth = SimpleNamespace(get_token=fake_get_token) + monkeypatch.setattr("asyncio.to_thread", fake_to_thread) + monkeypatch.setitem(sys.modules, "oauth_cli_kit", fake_oauth) + + class FakeAsyncClient: + def __init__(self, **kwargs: Any) -> None: + captured["client_kwargs"] = kwargs + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb) -> None: + return None + + async def post(self, url: str, **kwargs: Any) -> FakeResponse: + captured["request"] = {"url": url, **kwargs} + return FakeResponse( + {}, + sse_lines=[ + f'data: {{"type":"response.output_item.done","item":{{"type":"image_generation_call","result":"{PNG_DATA_URL}"}}}}', + "", + "data: [DONE]", + "", + ], + ) + + monkeypatch.setattr( + "nanobot.providers.image_generation.httpx.AsyncClient", + FakeAsyncClient, + ) + client = CodexImageGenerationClient(api_key=None, proxy=proxy) + + response = await client.generate(prompt="draw", model="gpt-5.4") + + assert response.images == [PNG_DATA_URL] + assert captured["token_proxy"] == proxy + assert captured["client_kwargs"]["proxy"] == proxy + assert captured["client_kwargs"]["trust_env"] is False + + @pytest.mark.asyncio async def test_codex_stops_reading_after_completed_event(monkeypatch) -> None: import sys diff --git a/tests/tools/test_image_generation_tool.py b/tests/tools/test_image_generation_tool.py index d2ee2838..6aa06635 100644 --- a/tests/tools/test_image_generation_tool.py +++ b/tests/tools/test_image_generation_tool.py @@ -125,6 +125,32 @@ async def test_generate_image_tool_selects_aihubmix_provider( assert fake.calls[0]["aspect_ratio"] == "3:4" +def test_image_generation_tool_passes_provider_proxy_to_client( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + FakeImageClient.instances = [] + monkeypatch.setattr( + "nanobot.agent.tools.image_generation.get_image_gen_provider", + lambda name: FakeImageClient if name == "openai_codex" else None, + ) + proxy = "http://127.0.0.1:23458" + tool = ImageGenerationTool( + workspace=tmp_path, + config=ImageGenerationToolConfig( + enabled=True, + provider="openai_codex", + model="openai-codex/gpt-5.4", + ), + provider_configs={"openai_codex": ProviderConfig(proxy=proxy)}, + ) + + client = tool._provider_client() + + assert client is not None + assert FakeImageClient.instances[0].kwargs["proxy"] == proxy + + @pytest.mark.asyncio async def test_generate_image_tool_reports_missing_aihubmix_key(tmp_path: Path) -> None: tool = ImageGenerationTool(