From c2d84c2890580b5a2e31bfd2dd0023d38d299962 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Sun, 7 Jun 2026 04:14:09 +0200 Subject: [PATCH] fix(codex): respect HERMES_CODEX_BASE_URL in all paths The openai-codex provider hardcodes the Codex base URL in three code paths, ignoring both HERMES_CODEX_BASE_URL env var and config.yaml model.base_url. This makes proxy routing impossible. Affected paths: - credential_pool._seed_from_singletons() bakes hardcoded URL into pool entries on every startup - runtime_provider._resolve_runtime_from_pool_entry() exits before reaching the config base_url override block - run_agent._swap_credential() reads pool entry base_url directly without consulting env var Centralize resolution into resolve_codex_base_url() helper in auth.py with priority: HERMES_CODEX_BASE_URL > pool entry > DEFAULT_CODEX_BASE_URL. All three sites now call this helper. Closes #40913 --- agent/credential_pool.py | 3 +- hermes_cli/auth.py | 27 ++++++++----- hermes_cli/runtime_provider.py | 3 +- run_agent.py | 3 ++ tests/agent/test_resolve_codex_base_url.py | 47 ++++++++++++++++++++++ 5 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 tests/agent/test_resolve_codex_base_url.py diff --git a/agent/credential_pool.py b/agent/credential_pool.py index e5b473ec525c6..694f4cd1c7df2 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -34,6 +34,7 @@ _save_provider_state, _store_provider_state, read_credential_pool, + resolve_codex_base_url, write_credential_pool, ) @@ -1901,7 +1902,7 @@ def _env_val(key: str) -> str: "auth_type": AUTH_TYPE_OAUTH, "access_token": tokens.get("access_token", ""), "refresh_token": tokens.get("refresh_token"), - "base_url": "https://chatgpt.com/backend-api/codex", + "base_url": resolve_codex_base_url(), "last_refresh": state.get("last_refresh"), "label": custom_label or label_from_token(tokens.get("access_token", ""), "device_code"), }, diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 021905c3ec05d..c3b4ce0b5d324 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -78,6 +78,20 @@ NOUS_INVOKE_JWT_MIN_TTL_SECONDS = ACCESS_TOKEN_REFRESH_SKEW_SECONDS DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS = 1 # poll at most every 1s DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex" + + +def resolve_codex_base_url(pool_base_url: str | None = None) -> str: + """Canonical Codex base URL resolution. + + Priority: HERMES_CODEX_BASE_URL env var > pool_base_url > DEFAULT_CODEX_BASE_URL. + + Single source of truth for every code path that needs the Codex + endpoint — credential pool seeding, pool-based runtime resolution, + credential rotation, and the legacy singleton resolver. + """ + env = os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") + pool = (pool_base_url or "").strip().rstrip("/") + return env or pool or DEFAULT_CODEX_BASE_URL DEFAULT_XAI_OAUTH_BASE_URL = "https://api.x.ai/v1" MINIMAX_OAUTH_CLIENT_ID = "78257093-7e40-4613-99e0-527b14b39113" MINIMAX_OAUTH_SCOPE = "group_id profile model.completion" @@ -3629,13 +3643,9 @@ def resolve_codex_runtime_credentials( except AuthError: pool_token = _pool_codex_access_token() if pool_token: - base_url = ( - os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") - or DEFAULT_CODEX_BASE_URL - ) return { "provider": "openai-codex", - "base_url": base_url, + "base_url": resolve_codex_base_url(), "api_key": pool_token, "source": "credential_pool", "last_refresh": None, @@ -3665,14 +3675,9 @@ def resolve_codex_runtime_credentials( tokens = _refresh_codex_auth_tokens(tokens, refresh_timeout_seconds) access_token = str(tokens.get("access_token", "") or "").strip() - base_url = ( - os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") - or DEFAULT_CODEX_BASE_URL - ) - return { "provider": "openai-codex", - "base_url": base_url, + "base_url": resolve_codex_base_url(), "api_key": access_token, "source": "hermes-auth-store", "last_refresh": data.get("last_refresh"), diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index cca80e988cebf..2334e91be1c57 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -21,6 +21,7 @@ format_auth_error, resolve_provider, resolve_nous_runtime_credentials, + resolve_codex_base_url, resolve_codex_runtime_credentials, resolve_xai_oauth_runtime_credentials, resolve_qwen_runtime_credentials, @@ -308,7 +309,7 @@ def _resolve_runtime_from_pool_entry( api_mode = "chat_completions" if provider == "openai-codex": api_mode = "codex_responses" - base_url = base_url or DEFAULT_CODEX_BASE_URL + base_url = resolve_codex_base_url(base_url) elif provider == "xai-oauth": api_mode = "codex_responses" base_url = base_url or DEFAULT_XAI_OAUTH_BASE_URL diff --git a/run_agent.py b/run_agent.py index c32218e511f92..9307b65172231 100644 --- a/run_agent.py +++ b/run_agent.py @@ -3811,6 +3811,9 @@ def _apply_client_headers_for_base_url(self, base_url: str) -> None: def _swap_credential(self, entry) -> None: runtime_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "") runtime_base = getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) or self.base_url + if self.provider == "openai-codex": + from hermes_cli.auth import resolve_codex_base_url + runtime_base = resolve_codex_base_url(runtime_base) if self.api_mode == "anthropic_messages": from agent.anthropic_adapter import build_anthropic_client, _is_oauth_token diff --git a/tests/agent/test_resolve_codex_base_url.py b/tests/agent/test_resolve_codex_base_url.py new file mode 100644 index 0000000000000..764fa66f202b3 --- /dev/null +++ b/tests/agent/test_resolve_codex_base_url.py @@ -0,0 +1,47 @@ +"""Tests for resolve_codex_base_url() — centralized Codex base URL resolution. + +Ensures HERMES_CODEX_BASE_URL env var takes priority over pool entry +base_url and DEFAULT_CODEX_BASE_URL across all code paths. + +Regression test for: https://github.com/NousResearch/hermes-agent/issues/5875 +""" + +import os +from unittest import mock + +from hermes_cli.auth import DEFAULT_CODEX_BASE_URL, resolve_codex_base_url + + +class TestResolveCodexBaseUrl: + """resolve_codex_base_url(pool_base_url=None) priority chain.""" + + def test_default_when_no_override(self): + with mock.patch.dict(os.environ, {}, clear=True): + assert resolve_codex_base_url() == DEFAULT_CODEX_BASE_URL + + def test_env_var_takes_priority_over_default(self): + with mock.patch.dict(os.environ, {"HERMES_CODEX_BASE_URL": "http://localhost:8787/v1"}): + assert resolve_codex_base_url() == "http://localhost:8787/v1" + + def test_env_var_takes_priority_over_pool(self): + with mock.patch.dict(os.environ, {"HERMES_CODEX_BASE_URL": "http://localhost:8787/v1"}): + result = resolve_codex_base_url(pool_base_url="https://chatgpt.com/backend-api/codex") + assert result == "http://localhost:8787/v1" + + def test_pool_base_url_used_when_no_env(self): + with mock.patch.dict(os.environ, {}, clear=True): + result = resolve_codex_base_url(pool_base_url="https://custom-proxy.example.com/v1") + assert result == "https://custom-proxy.example.com/v1" + + def test_env_var_stripped_and_rstripped(self): + with mock.patch.dict(os.environ, {"HERMES_CODEX_BASE_URL": " http://localhost:8787/v1/ "}): + assert resolve_codex_base_url() == "http://localhost:8787/v1" + + def test_empty_env_var_falls_through(self): + with mock.patch.dict(os.environ, {"HERMES_CODEX_BASE_URL": " "}): + assert resolve_codex_base_url() == DEFAULT_CODEX_BASE_URL + + def test_empty_pool_base_url_falls_through(self): + with mock.patch.dict(os.environ, {}, clear=True): + assert resolve_codex_base_url(pool_base_url="") == DEFAULT_CODEX_BASE_URL + assert resolve_codex_base_url(pool_base_url=None) == DEFAULT_CODEX_BASE_URL