diff --git a/src/docgen/ai_client.py b/src/docgen/ai_client.py index 65a9f62..3a4cd10 100644 --- a/src/docgen/ai_client.py +++ b/src/docgen/ai_client.py @@ -320,14 +320,27 @@ def resolve_image_model(model: str, settings: AISettings | None = None, *, cfg: return chosen +def _grok_voice_name(chosen: str) -> str: + """Map a voice onto Grok. An unknown name must not silently become ``eve``.""" + key = chosen.lower() + if not key: + return DEFAULT_GROK_TTS_VOICE + if key in _GROK_VOICES: + return key + mapped = _OPENAI_TO_GROK_VOICE.get(key) + if mapped is None: + raise AIError( + f"voice {chosen!r} is not a Grok voice and has no OpenAI alias" + ) + return mapped + + def resolve_tts_voice(voice: str, settings: AISettings | None = None, *, cfg: "Config | None" = None) -> str: chosen = (voice or "").strip() st = settings or resolve_ai_settings(cfg) if not st.is_grok: return chosen - if chosen.lower() in _GROK_VOICES: - return chosen.lower() - return _OPENAI_TO_GROK_VOICE.get(chosen.lower(), DEFAULT_GROK_TTS_VOICE) + return _grok_voice_name(chosen) def chat_completion( diff --git a/tests/test_ai_client.py b/tests/test_ai_client.py index 6049ec2..a1427a6 100644 --- a/tests/test_ai_client.py +++ b/tests/test_ai_client.py @@ -75,6 +75,16 @@ def test_xai_alias_and_openai_model_remap( assert resolve_tts_voice("eve", st) == "eve" +def test_unknown_grok_voice_is_refused( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("DOCGEN_AI_PROVIDER", "xai") + cfg = _cfg(tmp_path, {}) + st = resolve_ai_settings(cfg) + with pytest.raises(AIError, match="not a Grok voice"): + resolve_tts_voice("banana", st) + + def test_openai_client_grok_passes_base_url( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: