Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent/credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
_save_provider_state,
_store_provider_state,
read_credential_pool,
resolve_codex_base_url,
write_credential_pool,
)

Expand Down Expand Up @@ -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"),
},
Expand Down
27 changes: 16 additions & 11 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
3 changes: 2 additions & 1 deletion hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -308,7 +309,7 @@ def _resolve_runtime_from_pool_entry(
api_mode = "chat_completions"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies the environment override, but resolve_codex_base_url(base_url) cannot honor model.base_url because it receives no model configuration. Please add the matching-provider config fallback for an empty/default pool URL (while preserving a non-default pool endpoint), as the PR description and #60198 discussion require.

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
Expand Down
3 changes: 3 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions tests/agent/test_resolve_codex_base_url.py
Original file line number Diff line number Diff line change
@@ -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