diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 1ea6f21384..497484ac0c 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2787,13 +2787,14 @@ def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]: return None, None pool_present, entry = _select_pool_entry("openai-codex") codex_token = _pool_runtime_api_key(entry) if pool_present else None + codex_override = os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") if codex_token: - base_url = _pool_runtime_base_url(entry, _CODEX_AUX_BASE_URL) or _CODEX_AUX_BASE_URL + base_url = codex_override or _pool_runtime_base_url(entry, _CODEX_AUX_BASE_URL) or _CODEX_AUX_BASE_URL else: codex_token = _read_codex_access_token() if not codex_token: return None, None - base_url = _CODEX_AUX_BASE_URL + base_url = codex_override or _CODEX_AUX_BASE_URL logger.debug("Auxiliary client: Codex OAuth (%s via Responses API)", model) real_client = _create_openai_client( api_key=codex_token, base_url=base_url, diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 0becf6eb69..f389f2c53a 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -449,6 +449,13 @@ def _pool_entry_mode_and_url(provider, entry, model_cfg, effective_model, base_u base_url = _config_base_url_for_provider(model_cfg, provider) or base_url if provider in _POOL_ENTRY_SIMPLE_MODES: api_mode, default_url = _POOL_ENTRY_SIMPLE_MODES[provider] + if provider == "openai-codex": + # Pool entries retain the canonical ChatGPT URL, but the profile-wide + # HERMES_CODEX_BASE_URL override must apply consistently to every + # credential source, including pooled OAuth credentials. + override_url = get_secret_str("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") + if override_url: + return api_mode, override_url return api_mode, base_url or (default_url() if callable(default_url) else default_url) if provider == "anthropic": return "anthropic_messages", _anthropic_cfg_base_url(model_cfg) or base_url or _ANTHROPIC_DEFAULT_BASE_URL diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index a8e09d8b87..c25064cd44 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -724,6 +724,26 @@ class TestBuildCodexClient: assert mock_openai.call_args.kwargs["api_key"] == "codex-auth-token" assert mock_openai.call_args.kwargs["base_url"] == "https://chatgpt.com/backend-api/codex" + def test_profile_codex_base_url_overrides_pool_endpoint(self, monkeypatch): + """Auxiliary Codex calls use the same profile endpoint override as the main client.""" + entry = SimpleNamespace( + runtime_api_key="codex-pool-token", + runtime_base_url="https://chatgpt.com/backend-api/codex", + ) + with ( + patch("agent.auxiliary_client._select_pool_entry", return_value=(True, entry)), + patch("agent.auxiliary_client.OpenAI") as mock_openai, + ): + monkeypatch.setenv("HERMES_CODEX_BASE_URL", "http://127.0.0.1:8787/v1") + mock_openai.return_value = MagicMock() + from agent.auxiliary_client import _build_codex_client + + client, model = _build_codex_client("gpt-5.4") + + assert client is not None + assert model == "gpt-5.4" + assert mock_openai.call_args.kwargs["base_url"] == "http://127.0.0.1:8787/v1" + def test_rejects_missing_model(self): """Callers must pass an explicit model; no hardcoded default.""" from agent.auxiliary_client import _build_codex_client diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index 725465382b..d841a3e6bd 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -86,6 +86,30 @@ def test_resolve_runtime_provider_uses_credential_pool(monkeypatch): assert resolved["source"] == "manual" +def test_codex_pool_honors_hermes_codex_base_url(monkeypatch): + """The profile-wide Codex endpoint override must apply to pool credentials too.""" + 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): + return _Entry() + + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openai-codex") + monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool()) + monkeypatch.setenv("HERMES_CODEX_BASE_URL", "http://127.0.0.1:8787/v1") + + resolved = rp.resolve_runtime_provider(requested="openai-codex") + + assert resolved["provider"] == "openai-codex" + assert resolved["base_url"] == "http://127.0.0.1:8787/v1" + + class TestCustomProviderPoolLoopbackNoKeyExemption: """Regression for issue #86864: legacy custom_providers configs often used short/placeholder api_keys ('123', 'm') for local no-auth