Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions tests/tools/test_transcription_plugin_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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.
Expand Down Expand Up @@ -229,10 +229,8 @@ def test_unknown_name_with_plugin_dispatches(self):
assert result["transcript"] == "fake transcript"
assert result["provider"] == "openrouter"

def test_unknown_name_without_plugin_falls_to_legacy_error(self):
"""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):
"""Explicit unknown providers should get a named registration error."""
from unittest.mock import patch

with patch("tools.transcription_tools._validate_audio_file", return_value=None), \
Expand All @@ -242,7 +240,25 @@ def test_unknown_name_without_plugin_falls_to_legacy_error(self):
result = transcription_tools.transcribe_audio("/tmp/audio.mp3")

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):
"""Even if a plugin's name collides with a built-in (which the
Expand Down
37 changes: 31 additions & 6 deletions tools/transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,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)
# ---------------------------------------------------------------------------
Expand All @@ -882,8 +898,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:

Expand All @@ -901,8 +917,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
Expand Down Expand Up @@ -1715,8 +1731,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
Expand All @@ -1736,6 +1752,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,
Expand Down