feat(transcription): configurable STT model + OpenRouter provider

Add a `transcriptionModel` channel setting and an OpenRouter transcription
backend so voice messages can be transcribed through OpenRouter's
speech-to-text endpoint (e.g. nvidia/parakeet-tdt-0.6b-v3, openai/whisper-1),
alongside the existing Groq/OpenAI Whisper providers.

- schema: add channels.transcriptionModel (None = provider default)
- providers/transcription: extract a shared POST/retry skeleton; add a
  JSON+base64 OpenRouterTranscriptionProvider; make the STT model a
  constructor param on all providers instead of hardcoding it
- channels: route transcriptionProvider="openrouter" and thread the model
  through the manager to each channel
- docs + tests

Only dedicated STT models work on OpenRouter's transcription endpoint;
chat LLMs (e.g. google/gemini-3.5-flash) are rejected there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Breitburg
2026-06-09 04:01:37 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent 28f3a20d64
commit 0eb3010e40
7 changed files with 319 additions and 18 deletions
+41
View File
@@ -265,6 +265,23 @@ def test_settings_payload_includes_effective_transcription_config(
assert payload["transcription"]["language"] == "en"
def test_settings_payload_exposes_openrouter_transcription_provider(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config()
config.providers.openrouter.api_key = "sk-or-test"
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
payload = settings_payload()
providers = {provider["name"]: provider for provider in payload["transcription"]["providers"]}
assert providers["openrouter"]["label"] == "OpenRouter"
assert providers["openrouter"]["configured"] is True
def test_update_transcription_settings_writes_top_level_only(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
@@ -301,6 +318,30 @@ def test_update_transcription_settings_writes_top_level_only(
assert payload["transcription"]["provider_configured"] is True
def test_update_transcription_settings_accepts_openrouter(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path = tmp_path / "config.json"
config = Config()
config.providers.openrouter.api_key = "sk-or-test"
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
payload = update_transcription_settings(
{
"provider": ["openrouter"],
"model": ["nvidia/parakeet-tdt-0.6b-v3"],
}
)
saved = load_config(config_path)
assert saved.transcription.provider == "openrouter"
assert saved.transcription.model == "nvidia/parakeet-tdt-0.6b-v3"
assert payload["transcription"]["provider"] == "openrouter"
assert payload["transcription"]["provider_configured"] is True
def test_update_transcription_settings_validates_language(
tmp_path,
monkeypatch: pytest.MonkeyPatch,