fix(providers): honor Codex proxy config consistently
This commit is contained in:
@@ -129,6 +129,7 @@ class ImageGenerationTool(Tool):
|
|||||||
"api_base": provider.api_base if provider else None,
|
"api_base": provider.api_base if provider else None,
|
||||||
"extra_headers": provider.extra_headers if provider else None,
|
"extra_headers": provider.extra_headers if provider else None,
|
||||||
"extra_body": provider.extra_body if provider else None,
|
"extra_body": provider.extra_body if provider else None,
|
||||||
|
"proxy": provider.proxy if provider else None,
|
||||||
}
|
}
|
||||||
return cls(**kwargs)
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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
|
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)
|
set_config_path(resolved_config_path)
|
||||||
console.print(f"[dim]Using config: {resolved_config_path}[/dim]")
|
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]")
|
console.print(f"[red]Login not implemented for {spec.label}[/red]")
|
||||||
raise typer.Exit(1)
|
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")
|
console.print(f"{__logo__} OAuth Login - {spec.label}\n")
|
||||||
handler()
|
handler()
|
||||||
if set_main or model:
|
if set_main or model:
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ class ImageGenerationProvider(ABC):
|
|||||||
api_base: str | None = None,
|
api_base: str | None = None,
|
||||||
extra_headers: dict[str, str] | None = None,
|
extra_headers: dict[str, str] | None = None,
|
||||||
extra_body: dict[str, Any] | None = None,
|
extra_body: dict[str, Any] | None = None,
|
||||||
|
proxy: str | None = None,
|
||||||
timeout: float | None = None,
|
timeout: float | None = None,
|
||||||
client: httpx.AsyncClient | None = None,
|
client: httpx.AsyncClient | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -194,6 +195,7 @@ class ImageGenerationProvider(ABC):
|
|||||||
self.api_base = self._resolve_base_url(api_base)
|
self.api_base = self._resolve_base_url(api_base)
|
||||||
self.extra_headers = extra_headers or {}
|
self.extra_headers = extra_headers or {}
|
||||||
self.extra_body = extra_body 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.timeout = timeout if timeout is not None else self.default_timeout
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
@@ -240,7 +242,11 @@ class ImageGenerationProvider(ABC):
|
|||||||
return await client.post(url, headers=headers, json=body)
|
return await client.post(url, headers=headers, json=body)
|
||||||
if self._client is not None:
|
if self._client is not None:
|
||||||
return await self._client.post(url, headers=headers, json=body)
|
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)
|
return await c.post(url, headers=headers, json=body)
|
||||||
|
|
||||||
|
|
||||||
@@ -1233,7 +1239,8 @@ class CodexImageGenerationClient(ImageGenerationProvider):
|
|||||||
raise ImageGenerationError(self.missing_key_message)
|
raise ImageGenerationError(self.missing_key_message)
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as exc:
|
||||||
raise ImageGenerationError(self.missing_key_message) from exc
|
raise ImageGenerationError(self.missing_key_message) from exc
|
||||||
if not token or not token.access:
|
if not token or not token.access:
|
||||||
|
|||||||
@@ -711,6 +711,52 @@ def test_provider_login_openai_codex_passes_configured_proxy(monkeypatch):
|
|||||||
assert captured["proxy"] == proxy
|
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):
|
def test_provider_login_openai_codex_resolves_proxy_env_ref(monkeypatch):
|
||||||
proxy = "http://127.0.0.1:23458"
|
proxy = "http://127.0.0.1:23458"
|
||||||
monkeypatch.setenv("CODEX_PROXY_FOR_TEST", proxy)
|
monkeypatch.setenv("CODEX_PROXY_FOR_TEST", proxy)
|
||||||
|
|||||||
@@ -1181,6 +1181,61 @@ async def test_codex_payload_and_response(monkeypatch) -> None:
|
|||||||
assert body["stream"] is True
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_codex_stops_reading_after_completed_event(monkeypatch) -> None:
|
async def test_codex_stops_reading_after_completed_event(monkeypatch) -> None:
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -125,6 +125,32 @@ async def test_generate_image_tool_selects_aihubmix_provider(
|
|||||||
assert fake.calls[0]["aspect_ratio"] == "3:4"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_generate_image_tool_reports_missing_aihubmix_key(tmp_path: Path) -> None:
|
async def test_generate_image_tool_reports_missing_aihubmix_key(tmp_path: Path) -> None:
|
||||||
tool = ImageGenerationTool(
|
tool = ImageGenerationTool(
|
||||||
|
|||||||
Reference in New Issue
Block a user