mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-25 05:52:34 +00:00
fix(xai-http): preserve ~/.hermes/.env fallback and XAI_STT_BASE_URL precedence
The new resolve_xai_http_credentials() resolver was using os.getenv() for the XAI_API_KEY/XAI_BASE_URL fallback path, which dropped the ~/.hermes/.env contract guarded by PR #17140 / #17163. Users with XAI_API_KEY in dotenv only would see "No xAI credentials found" even though the key was configured. Separately, _transcribe_xai started consulting creds["base_url"] (which always returns at least the default https://api.x.ai/v1) ahead of the public XAI_STT_BASE_URL env override, so the per-tool override stopped working. - tools/xai_http.py: add module-level get_env_value() wrapper that reads ~/.hermes/.env first (via hermes_cli.config.get_env_value), then os.environ. Resolver uses it for the API-key/base-url fallback. - tools/transcription_tools.py: restore precedence so XAI_STT_BASE_URL wins over creds["base_url"]. - tests/tools/test_transcription_dotenv_fallback.py + tests/tools/test_tts_dotenv_fallback.py: repoint the per-call-site patches at the new resolution point (tools.xai_http.get_env_value). The end-to-end regression-guard test (which patches load_env) is unchanged and still passes.
This commit is contained in:
parent
9eef53b960
commit
e13c1b8060
4 changed files with 42 additions and 10 deletions
|
|
@ -170,7 +170,15 @@ class TestTranscribeCallSitesReadDotenv:
|
||||||
assert seen_keys == ["mistral-dotenv-key"]
|
assert seen_keys == ["mistral-dotenv-key"]
|
||||||
|
|
||||||
def test_transcribe_xai_forwards_dotenv_key(self):
|
def test_transcribe_xai_forwards_dotenv_key(self):
|
||||||
|
"""xAI STT now resolves credentials through ``tools.xai_http`` so the
|
||||||
|
OAuth bearer wins when present and ``XAI_API_KEY`` is the fallback.
|
||||||
|
Patch the resolver's ``get_env_value`` to simulate a dotenv-only key
|
||||||
|
and confirm it reaches the HTTP call. The per-call-site
|
||||||
|
``transcription_tools.get_env_value`` is still consulted for the
|
||||||
|
``XAI_STT_BASE_URL`` override (covered by ``test_custom_base_url``).
|
||||||
|
"""
|
||||||
from tools import transcription_tools as tt
|
from tools import transcription_tools as tt
|
||||||
|
from tools import xai_http
|
||||||
|
|
||||||
captured: dict = {}
|
captured: dict = {}
|
||||||
|
|
||||||
|
|
@ -183,15 +191,12 @@ class TestTranscribeCallSitesReadDotenv:
|
||||||
response.json.return_value = {"text": "hello"}
|
response.json.return_value = {"text": "hello"}
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# get_env_value is consulted for both XAI_API_KEY and XAI_STT_BASE_URL.
|
|
||||||
# Return the key for the first call, None for base-url override
|
|
||||||
# (so it defaults to the module-level XAI_STT_BASE_URL).
|
|
||||||
def fake_get_env_value(name, default=None):
|
def fake_get_env_value(name, default=None):
|
||||||
if name == "XAI_API_KEY":
|
if name == "XAI_API_KEY":
|
||||||
return "xai-dotenv-key"
|
return "xai-dotenv-key"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with patch.object(tt, "get_env_value", side_effect=fake_get_env_value), \
|
with patch.object(xai_http, "get_env_value", side_effect=fake_get_env_value), \
|
||||||
patch("requests.post", side_effect=fake_post), \
|
patch("requests.post", side_effect=fake_post), \
|
||||||
patch("builtins.open", MagicMock()):
|
patch("builtins.open", MagicMock()):
|
||||||
result = tt._transcribe_xai("/tmp/fake.mp3", "grok-stt")
|
result = tt._transcribe_xai("/tmp/fake.mp3", "grok-stt")
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,12 @@ class TestDotenvFallbackPerProvider:
|
||||||
mock_import.return_value.assert_called_once_with(api_key="el-dotenv-key")
|
mock_import.return_value.assert_called_once_with(api_key="el-dotenv-key")
|
||||||
|
|
||||||
def test_xai_reads_dotenv_key(self, tmp_path):
|
def test_xai_reads_dotenv_key(self, tmp_path):
|
||||||
|
"""xAI TTS now resolves credentials through ``tools.xai_http``; the
|
||||||
|
dotenv fallback contract from #17140 is preserved by patching the
|
||||||
|
resolver's ``get_env_value`` rather than ``tts_tool.get_env_value``.
|
||||||
|
"""
|
||||||
from tools import tts_tool
|
from tools import tts_tool
|
||||||
|
from tools import xai_http
|
||||||
|
|
||||||
captured: dict = {}
|
captured: dict = {}
|
||||||
|
|
||||||
|
|
@ -69,7 +74,7 @@ class TestDotenvFallbackPerProvider:
|
||||||
response.raise_for_status = MagicMock()
|
response.raise_for_status = MagicMock()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
with patch.object(tts_tool, "get_env_value", return_value="xai-dotenv-key"), \
|
with patch.object(xai_http, "get_env_value", return_value="xai-dotenv-key"), \
|
||||||
patch("requests.post", side_effect=fake_post):
|
patch("requests.post", side_effect=fake_post):
|
||||||
tts_tool._generate_xai_tts("hi", str(tmp_path / "out.mp3"), {})
|
tts_tool._generate_xai_tts("hi", str(tmp_path / "out.mp3"), {})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -726,8 +726,8 @@ def _transcribe_xai(file_path: str, model_name: str) -> Dict[str, Any]:
|
||||||
xai_config = stt_config.get("xai", {})
|
xai_config = stt_config.get("xai", {})
|
||||||
base_url = str(
|
base_url = str(
|
||||||
xai_config.get("base_url")
|
xai_config.get("base_url")
|
||||||
or creds.get("base_url")
|
|
||||||
or get_env_value("XAI_STT_BASE_URL")
|
or get_env_value("XAI_STT_BASE_URL")
|
||||||
|
or creds.get("base_url")
|
||||||
or XAI_STT_BASE_URL
|
or XAI_STT_BASE_URL
|
||||||
).strip().rstrip("/")
|
).strip().rstrip("/")
|
||||||
language = str(
|
language = str(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,25 @@ from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hermes_cli.config import get_env_value as _hermes_get_env_value
|
||||||
|
except Exception:
|
||||||
|
_hermes_get_env_value = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_value(name: str, default=None):
|
||||||
|
"""Read ``name`` from ``~/.hermes/.env`` first, then ``os.environ``.
|
||||||
|
|
||||||
|
Wraps :func:`hermes_cli.config.get_env_value` so tests can patch
|
||||||
|
``tools.xai_http.get_env_value`` to inject dotenv-only secrets into the
|
||||||
|
xAI credential resolver.
|
||||||
|
"""
|
||||||
|
if _hermes_get_env_value is not None:
|
||||||
|
value = _hermes_get_env_value(name)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
return os.environ.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
def hermes_xai_user_agent() -> str:
|
def hermes_xai_user_agent() -> str:
|
||||||
"""Return a stable Hermes-specific User-Agent for xAI HTTP calls."""
|
"""Return a stable Hermes-specific User-Agent for xAI HTTP calls."""
|
||||||
|
|
@ -19,8 +38,11 @@ def resolve_xai_http_credentials() -> Dict[str, str]:
|
||||||
"""Resolve bearer credentials for direct xAI HTTP endpoints.
|
"""Resolve bearer credentials for direct xAI HTTP endpoints.
|
||||||
|
|
||||||
Prefers Hermes-managed xAI OAuth credentials when available, then falls back
|
Prefers Hermes-managed xAI OAuth credentials when available, then falls back
|
||||||
to ``XAI_API_KEY`` from the environment. This keeps direct xAI endpoints
|
to ``XAI_API_KEY`` resolved via ``hermes_cli.config.get_env_value`` so keys
|
||||||
(images, TTS, STT, etc.) aligned with the main runtime auth model.
|
stored in ``~/.hermes/.env`` (the standard Hermes location) are honored —
|
||||||
|
not just ones already exported into ``os.environ``. This keeps direct xAI
|
||||||
|
endpoints (images, TTS, STT, etc.) aligned with the main runtime auth model
|
||||||
|
and preserves the regression contract from PR #17140 / #17163.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||||
|
|
@ -52,8 +74,8 @@ def resolve_xai_http_credentials() -> Dict[str, str]:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
api_key = os.getenv("XAI_API_KEY", "").strip()
|
api_key = str(get_env_value("XAI_API_KEY") or "").strip()
|
||||||
base_url = (os.getenv("XAI_BASE_URL") or "https://api.x.ai/v1").strip().rstrip("/")
|
base_url = str(get_env_value("XAI_BASE_URL") or "https://api.x.ai/v1").strip().rstrip("/")
|
||||||
return {
|
return {
|
||||||
"provider": "xai",
|
"provider": "xai",
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue