fix(stt): report unregistered configured providers

This commit is contained in:
luyifan 2026-06-30 04:39:32 +08:00 committed by Teknium
parent 3290c18247
commit a5074c0ca8
2 changed files with 53 additions and 12 deletions

View file

@ -8,7 +8,7 @@ to #30398 — STT pluggability):
built-in name (which the registry blocks), the dispatcher re-checks
defensively.
2. Unknown name with no plugin returns None (caller surfaces the
legacy "No STT provider available" error).
"provider_not_registered" error).
3. Unknown name with plugin registered dispatches, returns result.
4. Plugin exceptions are caught and converted to the standard error
envelope.
@ -235,10 +235,8 @@ class TestTranscribeAudioE2E:
assert result["transcript"] == "fake transcript"
assert result["provider"] == "openrouter"
def test_unknown_name_without_plugin_falls_to_legacy_error(self, sample_audio_file):
"""When no plugin is registered for the unknown name, the
dispatcher returns None and transcribe_audio falls through to
the legacy 'No STT provider available' error message."""
def test_unknown_name_without_plugin_returns_provider_specific_error(self, sample_audio_file):
"""Explicit unknown providers should get a named registration error."""
from unittest.mock import patch
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "openrouter"}), \
@ -247,7 +245,25 @@ class TestTranscribeAudioE2E:
result = transcription_tools.transcribe_audio(sample_audio_file)
assert result["success"] is False
assert "No STT provider" in result["error"]
assert result["provider"] == "openrouter"
assert result["error_type"] == "provider_not_registered"
assert "stt.provider='openrouter'" in result["error"]
assert "hermes plugins list" in result["error"]
assert "No STT provider available" not in result["error"]
def test_auto_detect_failure_keeps_legacy_no_provider_message(self):
"""No explicit stt.provider remains the generic setup guidance path."""
from unittest.mock import patch
with patch("tools.transcription_tools._validate_audio_file", return_value=None), \
patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("tools.transcription_tools.is_stt_enabled", return_value=True), \
patch("tools.transcription_tools._get_provider", return_value="none"):
result = transcription_tools.transcribe_audio("/tmp/audio.mp3")
assert result["success"] is False
assert result.get("error_type") is None
assert "No STT provider available" in result["error"]
def test_builtin_name_does_not_consult_plugin_registry(self, sample_audio_file):
"""Even if a plugin's name collides with a built-in (which the

View file

@ -949,6 +949,22 @@ def _get_provider(stt_config: dict) -> str:
return "none"
def _unregistered_stt_provider_error(provider: str) -> Dict[str, Any]:
key = str(provider or "").strip()
return {
"success": False,
"transcript": "",
"provider": key,
"error_type": "provider_not_registered",
"error": (
f"stt.provider='{key}' is set but no built-in, command, or plugin "
"provider registered that name. Run `hermes plugins list` to see "
"installed STT plugins, or configure a command provider under "
f"`stt.providers.{key}.command`."
),
}
# ---------------------------------------------------------------------------
# Plugin provider dispatch (issue follow-up to #30398 — STT pluggability)
# ---------------------------------------------------------------------------
@ -965,8 +981,8 @@ def _dispatch_to_plugin_provider(
"""Route the call to a plugin-registered transcription provider, or
return None.
Returns the transcribe-response dict on dispatch, or ``None`` to
fall through to the legacy "No STT provider available" error path.
Returns the transcribe-response dict on dispatch, or ``None`` when no
plugin claimed the provider name.
Resolution invariants enforced here:
@ -984,8 +1000,8 @@ def _dispatch_to_plugin_provider(
3. Plugin dispatch fires only when ``provider`` matches a
registered :class:`TranscriptionProvider` whose ``name`` equals
the configured value. Unknown names with no plugin registered
return None (caller surfaces the legacy "No STT provider"
message).
return None (caller surfaces the configured-provider error when
the name came from ``stt.provider``).
4. Availability gating: when the matched plugin reports
``is_available() == False`` (missing API key, missing optional
SDK, etc.) this returns an error envelope identifying the
@ -1983,8 +1999,8 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> Dict[str, A
# nor ``"none"`` AND there is no same-name command provider. The
# dispatcher enforces built-ins-always-win + command-wins-over-plugin
# defensively. Returns None when no plugin is registered for the
# configured name, falling through to the legacy "No STT provider"
# error message below.
# configured name; explicit configured names get a provider-specific
# error before the generic auto-detect fallback below.
#
# Plugin-scoped config namespace mirrors the built-in pattern
# (``stt.openai.model``, ``stt.mistral.model``): plugins read their
@ -2004,6 +2020,15 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> Dict[str, A
if plugin_result is not None:
return plugin_result
provider_key = str(provider or "").strip().lower()
if (
"provider" in stt_config
and provider_key
and provider_key not in BUILTIN_STT_PROVIDERS
and provider_key != "none"
):
return _unregistered_stt_provider_error(provider_key)
# No provider available
return {
"success": False,