From 4033c97d35efee934d74b4738be6ee7e3ff96d1d Mon Sep 17 00:00:00 2001 From: 686f6c61 <6115107+686f6c61@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:54:39 +0200 Subject: [PATCH] fix(xai): honor model.base_url when the pool row is still the registry host Env-seeded xAI credentials keep https://api.x.ai/v1, so a configured relay never receives the request. Apply model.base_url only in that case. --- hermes_cli/runtime_provider.py | 7 +++ .../test_runtime_provider_resolution.py | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index cf149c3b7f..0bf71e302d 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -534,6 +534,13 @@ def _pool_entry_mode_and_url(provider, entry, model_cfg, effective_model, base_u # only when the pool row still carries the canonical URL). if base_url in ("", default_url): base_url = _config_base_url_for_provider(model_cfg, provider) or base_url + if provider == "xai": + # Env-seeded rows keep the registry host. model.base_url is the relay + # override, and only while the row is still that host — an explicit + # per-credential endpoint stays authoritative (#121347). + canonical = (PROVIDER_REGISTRY["xai"].inference_base_url or "").rstrip("/") + if base_url.rstrip("/") in ("", canonical): + base_url = _config_base_url_for_provider(model_cfg, provider) or base_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/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index caae54a521..396defd46f 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -163,6 +163,66 @@ def test_codex_pool_honors_model_base_url(monkeypatch): assert resolved["api_mode"] == "codex_responses" +def _xai_pool(url): + class _Entry: + access_token = "pool-token" + source = "env:XAI_API_KEY" + base_url = url + + class _Pool: + def has_credentials(self): + return True + + def select(self, **_kwargs): + return _Entry() + + return _Pool() + + +def test_xai_pool_honors_model_base_url_when_row_is_registry_host(monkeypatch): + """#121347: an env-seeded xAI row keeps https://api.x.ai/v1. model.base_url is the + relay override, same as the other API-key providers, and must not be shadowed.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "xai") + monkeypatch.setattr(rp, "load_pool", lambda provider: _xai_pool("https://api.x.ai/v1")) + monkeypatch.delenv("XAI_BASE_URL", raising=False) + monkeypatch.setattr(rp, "_get_model_config", lambda: { + "provider": "xai", "default": "grok-4", "base_url": "http://127.0.0.1:8765/v1/"}) + + resolved = rp.resolve_runtime_provider(requested="xai") + + assert resolved["provider"] == "xai" + assert resolved["api_key"] == "pool-token" + assert resolved["base_url"] == "http://127.0.0.1:8765/v1" + assert resolved["api_mode"] == "codex_responses" + + +def test_xai_pool_keeps_explicit_credential_endpoint(monkeypatch): + """A pool row that is not the registry host is an explicit endpoint and wins over model.base_url.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "xai") + monkeypatch.setattr(rp, "load_pool", lambda provider: _xai_pool("https://relay.example/v1")) + monkeypatch.delenv("XAI_BASE_URL", raising=False) + monkeypatch.setattr(rp, "_get_model_config", lambda: { + "provider": "xai", "default": "grok-4", "base_url": "http://127.0.0.1:8765/v1"}) + + resolved = rp.resolve_runtime_provider(requested="xai") + + assert resolved["base_url"] == "https://relay.example/v1" + assert resolved["api_mode"] == "codex_responses" + + +def test_xai_pool_ignores_another_providers_base_url(monkeypatch): + """A stale model.base_url saved for a different provider must not receive the xAI key.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "xai") + monkeypatch.setattr(rp, "load_pool", lambda provider: _xai_pool("https://api.x.ai/v1")) + monkeypatch.delenv("XAI_BASE_URL", raising=False) + monkeypatch.setattr(rp, "_get_model_config", lambda: { + "provider": "deepseek", "default": "deepseek-v4-pro", "base_url": "http://127.0.0.1:8765/v1"}) + + resolved = rp.resolve_runtime_provider(requested="xai") + + assert resolved["base_url"] == "https://api.x.ai/v1" + + class TestCustomProviderPoolLoopbackNoKeyExemption: """Regression for issue #86864: legacy custom_providers configs often used short/placeholder api_keys ('123', 'm') for local no-auth