{"repo":"NousResearch/hermes-agent","public_id":"2544","display_label":"Cloud STT model names breaking local faster-whisper provider","summary":"A bug where cloud-only model names like 'whisper-1' cause crashes when passed to the local faster-whisper provider. Multiple fix attempts address mapping or normalizing these names to valid local model sizes.","subsystem":"gateway","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":13016},"resolution":{"state":"resolved","by":"pr-13016"},"freshness":{"state":"stale","last_verified_at":"2026-04-20T12:18:49+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":2544,"pr_number":13016,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":2544,"title":"[Bug]: Local STT provider crashes when config contains OpenAI model name (\"whisper-1\")","url":null,"state":"closed"},{"kind":"pr","number":2545,"title":"fix(stt): map cloud-only model names to valid local size for faster-whisper","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#13016","number":13016}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":2731,"title":"fix(stt): map cloud-only model names to default local model for faster-whisper","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#13016","number":13016}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11618,"title":"fix(stt): normalize cloud-only model names for local faster-whisper provider","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#13016","number":13016}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":13016,"title":"fix(stt): map cloud-only model names to valid local size for faster-whisper (#2544)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/release.py","tests/tools/test_transcription.py","tools/transcription_tools.py"],"fix_hunks":[{"file":"scripts/release.py","hunks":["@@ -80,6 +80,7 @@\n     \"[redacted:email]\": \"jplew\",\n     \"[redacted:email]\": \"nish3451\",\n     \"[redacted:email]\": \"Mibayy\",\n+    \"[redacted:email]\": \"Mibayy\",\n     \"[redacted:email]\": \"sgaofen\",\n     \"[redacted:email]\": \"nocoo\",\n     \"[redacted:email]\": \"n-WN\","]},{"file":"tests/tools/test_transcription.py","hunks":["@@ -245,3 +245,67 @@ def test_invalid_file_returns_error(self):\n         result = transcribe_audio(\"/nonexistent/file.ogg\")\n         assert result[\"success\"] is False\n         assert \"not found\" in result[\"error\"]\n+\n+\n+# ---------------------------------------------------------------------------\n+# Model name normalisation for local providers\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestNormalizeLocalModel:\n+    \"\"\"_normalize_local_model() maps cloud-only names to the local default.\"\"\"\n+\n+    def test_openai_model_name_maps_to_default(self):\n+        from tools.transcription_tools import _normalize_local_model, DEFAULT_LOCAL_MODEL\n+        assert _normalize_local_model(\"whisper-1\") == DEFAULT_LOCAL_MODEL\n+\n+    def test_groq_model_name_maps_to_default(self):\n+        from tools.transcription_tools import _normalize_local_model, DEFAULT_LOCAL_MODEL\n+        assert _normalize_local_model(\"whisper-large-v3-turbo\") == DEFAULT_LOCAL_MODEL\n+\n+    def test_valid_local_model_preserved(self):\n+        from tools.transcription_tools import _normalize_local_model\n+        for size in (\"tiny\", \"base\", \"small\", \"medium\", \"large-v3\"):\n+            assert _normalize_local_model(size) == size\n+\n+    def test_none_maps_to_default(self):\n+        from tools.transcription_tools import _normalize_local_model, DEFAULT_LOCAL_MODEL\n+        assert _normalize_local_model(None) == DEFAULT_LOCAL_MODEL\n+\n+    def test_warning_emitted_for_cloud_model(self, caplog):\n+        import logging\n+        from tools.transcription_tools import _normalize_local_model\n+        with caplog.at_level(logging.WARNING, logger=\"tools.transcription_tools\"):\n+            _normalize_local_model(\"whisper-1\")\n+        assert any(\"whisper-1\" in r.message for r in caplog.records)\n+\n+    def test_local_transcribe_normalises_model(self):\n+        \"\"\"transcribe_audio with local provider must not pass 'whisper-1' to WhisperModel.\"\"\"\n+        import tempfile, os\n+        from unittest.mock import MagicMock, patch\n+\n+        with tempfile.NamedTemporaryFile(suffix=\".ogg\", delete=False) as f:\n+            f.write(b\"x\")\n+            audio_file = f.name\n+        try:\n+            mock_model = MagicMock()\n+            mock_model.transcribe.return_value = (iter([]), MagicMock(language=\"en\", duration=1.0))\n+            with patch(\"tools.transcription_tools._HAS_FASTER_WHISPER\", True), \\\n+                 patch(\"tools.transcription_tools._load_stt_config\", return_value={\n+                     \"enabled\": True,\n+                     \"provider\": \"local\",\n+                     \"local\": {\"model\": \"whisper-1\"},\n+                 }), \\\n+                 patch(\"tools.transcription_tools._local_model\", None), \\\n+                 patch(\"tools.transcription_tools._local_model_name\", None), \\\n+                 patch(\"faster_whisper.WhisperModel\", return_value=mock_model) as mock_cls:\n+                from tools.transcription_tools import transcribe_audio\n+                transcribe_audio(audio_file)\n+                # WhisperModel must NOT have been called with \"whisper-1\"\n+                call_args = mock_cls.call_args\n+                assert call_args is not None\n+                assert call_args[0][0] != \"whisper-1\", (\n+                    \"WhisperModel was called with the cloud-only name 'whisper-1'\"\n+                )\n+        finally:\n+            os.unlink(audio_file)"]},{"file":"tools/transcription_tools.py","hunks":["@@ -154,12 +154,31 @@ def _has_local_command() -> bool:\n     return _get_local_command_template() is not None\n \n \n-def _normalize_local_command_model(model_name: Optional[str]) -> str:\n+def _normalize_local_model(model_name: Optional[str]) -> str:\n+    \"\"\"Return a valid faster-whisper model size, mapping cloud-only names to the default.\n+\n+    Cloud providers like OpenAI use names such as ``whisper-1`` which are not\n+    valid for faster-whisper (which expects ``tiny``, ``base``, ``small``,\n+    ``medium``, or ``large-v*``).  When such a name is detected we fall back to\n+    the default local model and emit a warning so the user knows what happened.\n+    \"\"\"\n     if not model_name or model_name in OPENAI_MODELS or model_name in GROQ_MODELS:\n+        if model_name and (model_name in OPENAI_MODELS or model_name in GROQ_MODELS):\n+            logger.warning(\n+                \"STT model '%s' is a cloud-only name and cannot be used with the local \"\n+                \"provider. Falling back to '%s'. Set stt.local.model to a valid \"\n+                \"faster-whisper size (tiny, base, small, medium, large-v3).\",\n+                model_name,\n+                DEFAULT_LOCAL_MODEL,\n+            )\n         return DEFAULT_LOCAL_MODEL\n     return model_name\n \n \n+def _normalize_local_command_model(model_name: Optional[str]) -> str:\n+    return _normalize_local_model(model_name)\n+\n+\n def _get_provider(stt_config: dict) -> str:\n     \"\"\"Determine which STT provider to use.\n ","@@ -596,7 +615,9 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> Dict[str, A\n \n     if provider == \"local\":\n         local_cfg = stt_config.get(\"local\", {})\n-        model_name = model or local_cfg.get(\"model\", DEFAULT_LOCAL_MODEL)\n+        model_name = _normalize_local_model(\n+            model or local_cfg.get(\"model\", DEFAULT_LOCAL_MODEL)\n+        )\n         return _transcribe_local(file_path, model_name)\n \n     if provider == \"local_command\":\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}