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:
Jaaneek 2026-05-15 18:27:54 +01:00 committed by Teknium
parent 9eef53b960
commit e13c1b8060
4 changed files with 42 additions and 10 deletions

View file

@ -170,7 +170,15 @@ class TestTranscribeCallSitesReadDotenv:
assert seen_keys == ["mistral-dotenv-key"]
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 xai_http
captured: dict = {}
@ -183,15 +191,12 @@ class TestTranscribeCallSitesReadDotenv:
response.json.return_value = {"text": "hello"}
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):
if name == "XAI_API_KEY":
return "xai-dotenv-key"
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("builtins.open", MagicMock()):
result = tt._transcribe_xai("/tmp/fake.mp3", "grok-stt")

View file

@ -57,7 +57,12 @@ class TestDotenvFallbackPerProvider:
mock_import.return_value.assert_called_once_with(api_key="el-dotenv-key")
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 xai_http
captured: dict = {}
@ -69,7 +74,7 @@ class TestDotenvFallbackPerProvider:
response.raise_for_status = MagicMock()
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):
tts_tool._generate_xai_tts("hi", str(tmp_path / "out.mp3"), {})