From 0c338711dfc03432854c58bce776b33f316f76c2 Mon Sep 17 00:00:00 2001 From: Brett Yelverton Date: Wed, 24 Jun 2026 03:31:33 +0000 Subject: [PATCH 1/3] feat: add Neuralwatt provider support (env var mapping) --- api/providers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/providers.py b/api/providers.py index 489aedc8..5e9d09a7 100644 --- a/api/providers.py +++ b/api/providers.py @@ -701,6 +701,7 @@ _PROVIDER_ENV_VAR: dict[str, str] = { "mistralai": "MISTRAL_API_KEY", "x-ai": "XAI_API_KEY", "xiaomi": "XIAOMI_API_KEY", + "neuralwatt": "NEURALWATT_API_KEY", "opencode-zen": "OPENCODE_ZEN_API_KEY", "opencode-go": "OPENCODE_GO_API_KEY", # NOTE: bare "ollama" (local) deliberately omitted — local Ollama is keyless From 6c73e417e075bf466ec73481d4054cf230156f51 Mon Sep 17 00:00:00 2001 From: Brett Yelverton Date: Wed, 24 Jun 2026 03:45:00 +0000 Subject: [PATCH 2/3] test: add regression coverage for neuralwatt provider env-var mapping Mirrors test_issue2025_xiaomi_env_key.py pattern: - _PROVIDER_ENV_VAR['neuralwatt'] == 'NEURALWATT_API_KEY' - _provider_has_key('neuralwatt') with env var set - _provider_has_key('neuralwatt') without env var - model group appears in get_available_models() when config lists models --- tests/test_issue_neuralwatt_env_key.py | 94 ++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/test_issue_neuralwatt_env_key.py diff --git a/tests/test_issue_neuralwatt_env_key.py b/tests/test_issue_neuralwatt_env_key.py new file mode 100644 index 00000000..b1b4ced2 --- /dev/null +++ b/tests/test_issue_neuralwatt_env_key.py @@ -0,0 +1,94 @@ +"""Regression coverage for Neuralwatt provider env-var mapping. + +Mirrors test_issue2025_xiaomi_env_key.py — ensures the provider ID maps to +the correct env var and that key detection works when the env var is set. +""" + +from __future__ import annotations + +import builtins + +import api.config as config +import api.providers as providers + + +def _force_env_fallback(monkeypatch): + """Force get_available_models() down its explicit env-var fallback path.""" + real_import = builtins.__import__ + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in ("hermes_cli.models", "hermes_cli.auth"): + raise ImportError(name) + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + +def _run_available_models_with_cfg(monkeypatch, tmp_path, cfg): + old_cfg = dict(config.cfg) + old_mtime = config._cfg_mtime + monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json") + monkeypatch.setattr(config, "_get_config_path", lambda: tmp_path / "missing-config.yaml") + monkeypatch.setattr("api.profiles.get_active_hermes_home", lambda: tmp_path, raising=False) + config.cfg.clear() + config.cfg.update(cfg) + config._cfg_mtime = 0.0 + config.invalidate_models_cache() + try: + return config.get_available_models() + finally: + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + config.invalidate_models_cache() + + +def test_neuralwatt_env_var_mapping(): + """Neuralwatt maps to NEURALWATT_API_KEY in the provider env-var table.""" + assert providers._PROVIDER_ENV_VAR["neuralwatt"] == "NEURALWATT_API_KEY" + + +def test_neuralwatt_provider_has_key_when_env_set(monkeypatch, tmp_path): + """Key detection returns True when NEURALWATT_API_KEY is in the environment.""" + monkeypatch.setattr(providers, "_get_hermes_home", lambda: tmp_path) + monkeypatch.setenv("NEURALWATT_API_KEY", "test-neuralwatt-key") + + assert providers._provider_has_key("neuralwatt") is True + + +def test_neuralwatt_provider_has_key_false_without_env(monkeypatch, tmp_path): + """Key detection returns False when NEURALWATT_API_KEY is not set.""" + monkeypatch.setattr(providers, "_get_hermes_home", lambda: tmp_path) + monkeypatch.delenv("NEURALWATT_API_KEY", raising=False) + + assert providers._provider_has_key("neuralwatt") is False + + +def test_neuralwatt_model_group_appears_with_models_in_config(monkeypatch, tmp_path): + """Neuralwatt appears as a provider group when config.yaml lists its models.""" + _force_env_fallback(monkeypatch) + monkeypatch.setenv("NEURALWATT_API_KEY", "test-neuralwatt-key") + + result = _run_available_models_with_cfg( + monkeypatch, + tmp_path, + { + "model": {"default": "glm-5.2", "provider": "neuralwatt"}, + "providers": { + "neuralwatt": { + "base_url": "https://api.neuralwatt.com/v1", + "key_env": "NEURALWATT_API_KEY", + "api_mode": "chat_completions", + "default_model": "glm-5.2", + "models": ["glm-5.2", "glm-5.2-short"], + } + }, + }, + ) + + groups = {group["provider_id"]: group for group in result["groups"]} + assert "neuralwatt" in groups, f"neuralwatt missing from groups: {list(groups.keys())}" + assert groups["neuralwatt"]["provider"] == "Neuralwatt" + model_ids = {model["id"] for model in groups["neuralwatt"]["models"]} + assert "glm-5.2" in model_ids + assert "glm-5.2-short" in model_ids From cd67e67af14766a6553f3e11db8947203e3b4d73 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 29 Jun 2026 20:42:54 +0000 Subject: [PATCH 3/3] docs(changelog): add Neuralwatt provider env-var mapping (#4810) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6512d9..19bc6f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added +- **The Neuralwatt provider now picks up its API key from `NEURALWATT_API_KEY`.** Added the env-var mapping so a Neuralwatt provider configured against the generic OpenAI-compatible path resolves its key the same way the other custom providers do — no other code path changed. Thanks @b-yelverton. (#4810) - **Voice mode's silence timeout and continuous-recognition behavior are now tunable** via two `localStorage` keys: `hermes-voice-silence-ms` (pause before auto-send, default 1800 ms) and `hermes-voice-continuous` (`"true"`/`"false"`, default false — keep the mic open across natural pauses). This addresses users who pause mid-sentence getting cut off, or who want a slower/faster auto-send. No new UI (set from the dev console for now); defaults are byte-identical to before, and a missing/invalid/non-positive `silence-ms` falls back to 1800 with a 200 ms floor so a mistyped value can't trigger instant auto-send. Thanks @ChonSong. (#5176, fixes #4761) - **The composer footer now fits its controls to the available width instead of relying on fixed breakpoints.** A small fit engine measures the footer's left control group and steps it down through full → icon-only → overflow-menu as space tightens (ResizeObserver + MutationObserver, rAF-debounced), so on mid-width layouts (e.g. tablet / split panes) more controls — the workspace, model, and reasoning chips — stay directly visible as clean chips before anything moves into the overflow menu, while very narrow widths still collapse to a tidy icon row. Verified equal-or-better than the previous fixed-breakpoint layout across wide / desktop / tablet / mobile. Thanks @Paladin173. (#4657)