mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(mistral): keep the deployment voice default and drop the unreachable api base fallback
Review turned up two real problems in the TTS path. Router.aspeech forwarded voice=None whenever the caller omitted it, which overwrote a voice set in the deployment's litellm_params, so a configured fallback voice was ignored on voice-less requests. It now leaves the key alone when no voice is passed. get_complete_url also fell back to MISTRAL_API_BASE, but speech() always receives a non-null api_base from get_llm_provider, whose mistral branch only reads MISTRAL_AZURE_API_BASE and otherwise hardcodes the public host. That branch could never run, and its unit test asserted a behavior the real path does not have. The working override is api_base on the deployment, now pinned by an end-to-end test
This commit is contained in:
parent
75e7f4c4a5
commit
0a2581c14c
5 changed files with 71 additions and 11 deletions
|
|
@ -115,7 +115,7 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig):
|
|||
api_base: str | None,
|
||||
litellm_params: Mapping[str, object],
|
||||
) -> str:
|
||||
configured_base: Final = (api_base or get_secret_str("MISTRAL_API_BASE") or self.TTS_BASE_URL).rstrip("/")
|
||||
configured_base: Final = (api_base or self.TTS_BASE_URL).rstrip("/")
|
||||
versioned_base: Final = configured_base if configured_base.endswith("/v1") else f"{configured_base}/v1"
|
||||
return f"{versioned_base}/audio/speech"
|
||||
|
||||
|
|
|
|||
|
|
@ -4422,7 +4422,7 @@ class Router:
|
|||
**{
|
||||
**data,
|
||||
"input": input,
|
||||
"voice": voice,
|
||||
**({"voice": voice} if voice is not None else {}),
|
||||
"client": model_client,
|
||||
**kwargs,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@ def test_transform_request_omits_voice_for_ref_audio_cloning():
|
|||
}
|
||||
|
||||
|
||||
def test_get_complete_url_default_base(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.delenv("MISTRAL_API_BASE", raising=False)
|
||||
def test_get_complete_url_default_base():
|
||||
config: Final = MistralTextToSpeechConfig()
|
||||
url: Final = config.get_complete_url(model="voxtral-mini-tts-2603", api_base=None, litellm_params={})
|
||||
assert url == SPEECH_URL
|
||||
|
|
@ -101,13 +100,6 @@ def test_get_complete_url_custom_base_always_versioned(api_base: str):
|
|||
assert url == "https://custom.api.example.com/v1/audio/speech"
|
||||
|
||||
|
||||
def test_get_complete_url_host_only_env_base_gets_v1(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("MISTRAL_API_BASE", "https://api.mistral.ai")
|
||||
config: Final = MistralTextToSpeechConfig()
|
||||
url: Final = config.get_complete_url(model="voxtral-mini-tts-2603", api_base=None, litellm_params={})
|
||||
assert url == SPEECH_URL
|
||||
|
||||
|
||||
def test_validate_environment_sets_bearer_header():
|
||||
config: Final = MistralTextToSpeechConfig()
|
||||
headers: Final = config.validate_environment(
|
||||
|
|
|
|||
|
|
@ -3287,3 +3287,21 @@ def test_speech_mistral_dispatches_and_decodes_audio(respx_mock: respx.MockRoute
|
|||
}
|
||||
assert mock_route.calls.last.request.headers["authorization"] == "Bearer sk-mistral-test"
|
||||
assert response.content == audio_bytes
|
||||
|
||||
|
||||
def test_speech_mistral_routes_to_configured_api_base(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
|
||||
audio_bytes: Final = b"ID3-gateway-bytes"
|
||||
gateway_route: Final = respx_mock.post("https://mistral.gateway.internal/v1/audio/speech").mock(
|
||||
return_value=httpx.Response(200, json={"audio_data": base64.b64encode(audio_bytes).decode()})
|
||||
)
|
||||
|
||||
response: Final = litellm.speech(
|
||||
model="mistral/voxtral-mini-tts-2603",
|
||||
input="hello from litellm",
|
||||
voice="en_paul_neutral",
|
||||
api_base="https://mistral.gateway.internal",
|
||||
)
|
||||
|
||||
assert gateway_route.called
|
||||
assert response.content == audio_bytes
|
||||
|
|
|
|||
|
|
@ -12278,6 +12278,56 @@ async def test_router_aspeech_without_voice_dispatches_ref_audio_cloning(respx_m
|
|||
assert response.content == audio_bytes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_router_aspeech_without_voice_keeps_deployment_default_voice(respx_mock, monkeypatch):
|
||||
import base64
|
||||
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
audio_bytes = b"RIFFfake-wav-bytes"
|
||||
respx_mock.post("https://api.mistral.ai/v1/audio/speech").respond(
|
||||
json={"audio_data": base64.b64encode(audio_bytes).decode()}
|
||||
)
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "voxtral-tts",
|
||||
"litellm_params": {"model": "mistral/voxtral-mini-tts-2603", "voice": "en_paul_neutral"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
await router.aspeech(model="voxtral-tts", input="use my default")
|
||||
|
||||
request_body = json.loads(respx_mock.calls.last.request.content)
|
||||
assert request_body["voice_id"] == "en_paul_neutral"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_router_aspeech_request_voice_overrides_deployment_default(respx_mock, monkeypatch):
|
||||
import base64
|
||||
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk-mistral-test")
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
audio_bytes = b"RIFFfake-wav-bytes"
|
||||
respx_mock.post("https://api.mistral.ai/v1/audio/speech").respond(
|
||||
json={"audio_data": base64.b64encode(audio_bytes).decode()}
|
||||
)
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "voxtral-tts",
|
||||
"litellm_params": {"model": "mistral/voxtral-mini-tts-2603", "voice": "en_paul_neutral"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
await router.aspeech(model="voxtral-tts", input="override me", voice="gb_oliver_neutral")
|
||||
|
||||
request_body = json.loads(respx_mock.calls.last.request.content)
|
||||
assert request_body["voice_id"] == "gb_oliver_neutral"
|
||||
|
||||
|
||||
class TestPreRoutingTierDrivesFallbacks:
|
||||
"""#38832: a complexity/auto router picks a tier behind the router name, but fallback
|
||||
lookup stayed on the router name, so the tier's configured chain never ran and a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue