diff --git a/tests/agent/test_credential_rotation_route_settings.py b/tests/agent/test_credential_rotation_route_settings.py index b11cf1d0ea..fd47b4ec57 100644 --- a/tests/agent/test_credential_rotation_route_settings.py +++ b/tests/agent/test_credential_rotation_route_settings.py @@ -109,3 +109,25 @@ def test_credential_rotation_does_not_carry_global_headers_across_routes(): headers = agent._client_kwargs["default_headers"] assert "Authorization" not in headers assert headers["X-Route"] == "b" + + +def test_codex_rotation_keeps_proxy_override(monkeypatch): + """#40913: a 401/429 rotation adopts the pool row, whose stored URL is the canonical ChatGPT + endpoint; with HERMES_CODEX_BASE_URL set the rotated client must keep targeting the proxy.""" + from agent.credential_pool import PooledCredential + + monkeypatch.setenv("HERMES_CODEX_BASE_URL", "http://127.0.0.1:8787/backend-api/codex/") + entry = PooledCredential(provider="openai-codex", id="second", label="second", auth_type="oauth", + priority=1, source="manual:device_code", access_token="tok-second", + base_url="https://chatgpt.com/backend-api/codex") + agent = SimpleNamespace( + api_mode="codex_responses", provider="openai-codex", model="gpt-5.3-codex", api_key="tok-first", + base_url="http://127.0.0.1:8787/backend-api/codex", + _client_kwargs={"api_key": "tok-first", "base_url": "http://127.0.0.1:8787/backend-api/codex"}, + _reapply_route_client_config=MagicMock(), _replace_primary_openai_client=MagicMock(), + ) + + assert AIAgent._swap_credential(agent, entry) is True + assert agent.base_url == "http://127.0.0.1:8787/backend-api/codex" + assert agent._client_kwargs["base_url"] == "http://127.0.0.1:8787/backend-api/codex" + assert agent.api_key == "tok-second" diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index 62afb8acd8..cc5bd78e38 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -110,6 +110,33 @@ def test_codex_pool_honors_hermes_codex_base_url(monkeypatch): assert resolved["base_url"] == "http://127.0.0.1:8787/v1" +def test_codex_pool_honors_model_base_url(monkeypatch): + """#40913: model.base_url under provider openai-codex is the secondary proxy override; the + canonical URL stored on the pool row must not shadow it.""" + class _Entry: + access_token = "pool-token" + source = "manual" + base_url = "https://chatgpt.com/backend-api/codex" + + class _Pool: + def has_credentials(self): + return True + + def select(self, **_kwargs): + return _Entry() + + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openai-codex") + monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool()) + monkeypatch.delenv("HERMES_CODEX_BASE_URL", raising=False) + monkeypatch.setattr(rp, "_get_model_config", lambda: { + "provider": "openai-codex", "default": "gpt-5.3-codex", "base_url": "http://127.0.0.1:8400/backend-api/codex/"}) + + resolved = rp.resolve_runtime_provider(requested="openai-codex") + + assert resolved["base_url"] == "http://127.0.0.1:8400/backend-api/codex" + assert resolved["api_mode"] == "codex_responses" + + class TestCustomProviderPoolLoopbackNoKeyExemption: """Regression for issue #86864: legacy custom_providers configs often used short/placeholder api_keys ('123', 'm') for local no-auth diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 997381ac21..aeb58730ba 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -23,6 +23,7 @@ Hermes reads environment variables from the process environment and, for user-ma | `AI_GATEWAY_BASE_URL` | Override AI Gateway base URL (default: `https://ai-gateway.vercel.sh/v1`) | | `OPENAI_API_KEY` | API key for custom OpenAI-compatible endpoints (used with `OPENAI_BASE_URL`) | | `OPENAI_BASE_URL` | Base URL for custom endpoint (VLLM, SGLang, etc.) | +| `HERMES_CODEX_BASE_URL` | Route the `openai-codex` (ChatGPT subscription) provider through a proxy instead of the default Codex backend. Applies everywhere the credential is used: pool resolution, auxiliary/raw clients, and 401/429 credential rotation. `model.base_url` under `model.provider: openai-codex` is the secondary override when this is unset. | | `LM_API_KEY` | API key for LM Studio (`lmstudio` provider). Often a placeholder for local servers | | `LM_BASE_URL` | LM Studio base URL (default: `http://localhost:1234/v1`) | | `COPILOT_GITHUB_TOKEN` | GitHub token for Copilot API — first priority (OAuth `gho_*` or fine-grained PAT `github_pat_*`; classic PATs `ghp_*` are **not supported**) |