fix(x_search): prefer an explicit API key over subscription OAuth

When a paid XAI_API_KEY is configured alongside xAI SuperGrok OAuth,
_resolve_xai_bearer() took the OAuth path unconditionally. The OAuth
credential authorizes /v1/responses but answers in a degraded Grok
explanatory mode with no citations, while the API key returns real
posts - so every x_search query silently degraded (#88040).

Prefer the explicit API key when set (same shape as the TTS fix for
#87045 in #87081), keeping OAuth as the fallback when no API key is
configured. The shared resolver and every other xAI call site are
unchanged.
This commit is contained in:
liuhao1024
2026-08-17 09:15:12 +08:00
committed by Teknium
parent 1826310f49
commit 32170dd1a2
2 changed files with 96 additions and 1 deletions

View File

@@ -312,3 +312,84 @@ def test_x_search_not_degraded_when_no_filters_active(monkeypatch):
assert result["degraded"] is False
assert result["degraded_reason"] is None
def _xcred(prefix: str) -> str:
"""Synthesize a distinct fake credential value (never a bare literal)."""
return prefix + "-key-" + "x1"
def test_x_search_prefers_explicit_api_key_over_oauth(monkeypatch):
"""#88040: with a paid XAI_API_KEY configured alongside subscription
OAuth, x_search must use the API key — the OAuth path authorizes but
answers /v1/responses in a degraded Grok explanatory mode with no
citations. Same prefer-API-key shape as the TTS fix (#87045/#87081)."""
from tools.registry import invalidate_check_fn_cache
from tools.x_search_tool import x_search_tool
paid_key = _xcred("paid")
oauth_token = _xcred("oauth")
monkeypatch.setattr(
"hermes_cli.config.get_env_value",
lambda name, default=None: {
"XAI_API_KEY": paid_key,
"XAI_BASE_URL": None,
}.get(name, default),
)
def _fake_resolve():
return {
"provider": "xai-oauth",
"api_key": oauth_token,
"base_url": "https://api.x.ai/v1",
}
monkeypatch.setattr(
"tools.x_search_tool.resolve_xai_http_credentials", _fake_resolve
)
invalidate_check_fn_cache()
captured = {}
def _fake_post(url, headers=None, json=None, timeout=None):
captured["headers"] = headers
return _FakeResponse({"output_text": "Real posts with citations."})
monkeypatch.setattr("requests.post", _fake_post)
result = json.loads(x_search_tool(query="from:elon latest"))
assert result["success"] is True
assert result["credential_source"] == "xai"
assert captured["headers"]["Authorization"] == "Bearer " + paid_key
def test_x_search_bearer_helper_falls_back_to_oauth_without_api_key(monkeypatch):
"""No explicit XAI_API_KEY: the OAuth-first resolver path is unchanged."""
from tools.x_search_tool import _resolve_xai_bearer
oauth_token = _xcred("oauth")
monkeypatch.setattr(
"hermes_cli.config.get_env_value",
lambda name, default=None: default,
)
def _fake_resolve():
return {
"provider": "xai-oauth",
"api_key": oauth_token,
"base_url": "https://api.x.ai/v1",
}
monkeypatch.setattr(
"tools.x_search_tool.resolve_xai_http_credentials", _fake_resolve
)
api_key, base_url, source = _resolve_xai_bearer()
assert (api_key, base_url, source) == (
oauth_token,
"https://api.x.ai/v1",
"xai-oauth",
)

View File

@@ -52,7 +52,6 @@ import requests
from tools.registry import registry, tool_error
from tools.xai_http import hermes_xai_user_agent, resolve_xai_http_credentials
logger = logging.getLogger(__name__)
DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1"
@@ -128,7 +127,22 @@ def _resolve_xai_bearer() -> Tuple[str, str, str]:
gate makes that case unreachable in normal operation, but the runtime
check exists so a credential that expires between registration and
invocation produces a clean tool error instead of a 401.
x_search is API-index access: when a subscription OAuth credential is
configured alongside a paid ``XAI_API_KEY``, the OAuth path authorizes
but answers ``/v1/responses`` in a degraded Grok explanatory mode with
no citations, while the API key returns real posts (#88040). Prefer the
explicit API key — same shape as the TTS fix for #87045 (#87081) —
keeping OAuth as the fallback when no API key is configured.
"""
from hermes_cli.config import get_env_value
explicit_key = str(get_env_value("XAI_API_KEY") or "").strip()
if explicit_key:
base_url = str(
get_env_value("XAI_BASE_URL") or DEFAULT_XAI_BASE_URL
).strip().rstrip("/")
return explicit_key, base_url, "xai"
creds = resolve_xai_http_credentials()
api_key = str(creds.get("api_key") or "").strip()
if not api_key: