litellm/tests/llm_translation/test_minimax_tts.py
yuneng-jiang 6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* test: drop the cwd-relative sys.path.insert calls from the test suite

TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.

Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.

Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.

* test: drop the duplicate imports the sys.path sweep exposed to F811

* test(pre-call-utils): restore the os import the new bedrock tests need
2026-08-22 09:25:58 -07:00

366 lines
12 KiB
Python

"""
Tests for MiniMax Text-to-Speech integration
"""
import os
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
import pytest
import litellm
from litellm import speech
from litellm.llms.minimax.text_to_speech.transformation import (
MinimaxTextToSpeechConfig,
)
class TestMinimaxTextToSpeechConfig:
"""Test MiniMax TTS configuration and parameter mapping"""
def test_get_supported_openai_params(self):
"""Test that supported OpenAI params are correctly defined"""
config = MinimaxTextToSpeechConfig()
supported_params = config.get_supported_openai_params("speech-2.6-hd")
assert "voice" in supported_params
assert "response_format" in supported_params
assert "speed" in supported_params
def test_voice_mapping(self):
"""Test OpenAI voice to MiniMax voice_id mapping"""
config = MinimaxTextToSpeechConfig()
# Test OpenAI voice mappings
assert config._extract_voice_id("alloy") == "male-qn-qingse"
assert config._extract_voice_id("echo") == "male-qn-jingying"
assert config._extract_voice_id("nova") == "female-yujie"
# Test custom voice passthrough
assert config._extract_voice_id("custom-voice-id") == "custom-voice-id"
def test_format_mapping(self):
"""Test response format mapping"""
config = MinimaxTextToSpeechConfig()
assert config.FORMAT_MAPPINGS["mp3"] == "mp3"
assert config.FORMAT_MAPPINGS["pcm"] == "pcm"
assert config.FORMAT_MAPPINGS["wav"] == "wav"
assert config.FORMAT_MAPPINGS["flac"] == "flac"
def test_map_openai_params_basic(self):
"""Test basic parameter mapping from OpenAI to MiniMax format"""
config = MinimaxTextToSpeechConfig()
optional_params = {
"response_format": "mp3",
"speed": 1.5,
}
voice, mapped_params = config.map_openai_params(
model="speech-2.6-hd",
optional_params=optional_params,
voice="alloy",
)
assert voice == "male-qn-qingse"
assert mapped_params["format"] == "mp3"
assert mapped_params["speed"] == 1.5
assert mapped_params["voice_id"] == "male-qn-qingse"
def test_map_openai_params_speed_clamping(self):
"""Test that speed is clamped to MiniMax's supported range"""
config = MinimaxTextToSpeechConfig()
# Test speed too high
optional_params = {"speed": 5.0}
_, mapped_params = config.map_openai_params(
model="speech-2.6-hd",
optional_params=optional_params,
voice="alloy",
)
assert mapped_params["speed"] == 2.0 # Clamped to max
# Test speed too low
optional_params = {"speed": 0.1}
_, mapped_params = config.map_openai_params(
model="speech-2.6-hd",
optional_params=optional_params,
voice="alloy",
)
assert mapped_params["speed"] == 0.5 # Clamped to min
def test_map_openai_params_with_extra_body(self):
"""Test that extra_body parameters are passed through"""
config = MinimaxTextToSpeechConfig()
optional_params = {
"extra_body": {
"vol": 1.5,
"pitch": 2,
"sample_rate": 24000,
}
}
_, mapped_params = config.map_openai_params(
model="speech-2.6-hd",
optional_params=optional_params,
voice="alloy",
)
assert mapped_params["vol"] == 1.5
assert mapped_params["pitch"] == 2
assert mapped_params["sample_rate"] == 24000
def test_validate_environment_with_api_key(self):
"""Test environment validation with API key"""
config = MinimaxTextToSpeechConfig()
headers = {}
result_headers = config.validate_environment(
headers=headers,
model="speech-2.6-hd",
api_key="test-api-key",
)
assert "Authorization" in result_headers
assert result_headers["Authorization"] == "Bearer test-api-key"
assert result_headers["Content-Type"] == "application/json"
def test_validate_environment_missing_api_key(self):
"""Test that validation fails without API key"""
config = MinimaxTextToSpeechConfig()
headers = {}
# Mock both litellm.api_key and get_secret_str to return None
import litellm
original_api_key = litellm.api_key
try:
litellm.api_key = None
with patch(
"litellm.llms.minimax.text_to_speech.transformation.get_secret_str",
return_value=None,
):
with pytest.raises(ValueError, match="MiniMax API key is required"):
config.validate_environment(
headers=headers,
model="speech-2.6-hd",
api_key=None,
)
finally:
litellm.api_key = original_api_key
def test_transform_text_to_speech_request(self):
"""Test request transformation to MiniMax format"""
config = MinimaxTextToSpeechConfig()
optional_params = {
"voice_id": "male-qn-qingse",
"speed": 1.2,
"format": "mp3",
"vol": 1.0,
"pitch": 0,
"sample_rate": 32000,
"bitrate": 128000,
"channel": 1,
}
result = config.transform_text_to_speech_request(
model="speech-2.6-hd",
input="Hello, world!",
voice="male-qn-qingse",
optional_params=optional_params,
litellm_params={},
headers={},
)
assert "dict_body" in result
body = result["dict_body"]
assert body["model"] == "speech-2.6-hd"
assert body["text"] == "Hello, world!"
assert body["stream"] is False
assert body["voice_setting"]["voice_id"] == "male-qn-qingse"
assert body["voice_setting"]["speed"] == 1.2
assert body["audio_setting"]["format"] == "mp3"
assert body["audio_setting"]["sample_rate"] == 32000
def test_get_complete_url(self):
"""Test URL construction"""
config = MinimaxTextToSpeechConfig()
url = config.get_complete_url(
model="speech-2.6-hd",
api_base=None,
litellm_params={},
)
assert url == "https://api.minimax.io/v1/t2a_v2"
def test_get_complete_url_custom_base(self):
"""Test URL construction with custom API base"""
config = MinimaxTextToSpeechConfig()
url = config.get_complete_url(
model="speech-2.6-hd",
api_base="https://custom.api.com",
litellm_params={},
)
assert url == "https://custom.api.com/v1/t2a_v2"
class TestMinimaxSpeechIntegration:
"""Integration tests for MiniMax TTS via litellm.speech()"""
@pytest.mark.skip(reason="Requires MiniMax API key")
def test_speech_basic(self):
"""Test basic speech synthesis call"""
# This test requires a real API key
os.environ["MINIMAX_API_KEY"] = "your-api-key-here"
speech_file_path = Path(__file__).parent / "test_minimax_speech.mp3"
response = speech(
model="minimax/speech-2.6-hd",
voice="alloy",
input="Hello, this is a test of MiniMax text to speech.",
)
response.stream_to_file(speech_file_path)
# Verify file was created
assert speech_file_path.exists()
assert speech_file_path.stat().st_size > 0
# Clean up
speech_file_path.unlink()
@pytest.mark.skip(reason="Requires MiniMax API key")
def test_speech_with_custom_params(self):
"""Test speech synthesis with custom parameters"""
os.environ["MINIMAX_API_KEY"] = "your-api-key-here"
speech_file_path = Path(__file__).parent / "test_minimax_speech_custom.mp3"
response = speech(
model="minimax/speech-2.6-turbo",
voice="nova",
input="Testing custom parameters.",
speed=1.5,
response_format="mp3",
extra_body={
"vol": 1.2,
"pitch": 1,
"sample_rate": 24000,
},
)
response.stream_to_file(speech_file_path)
# Verify file was created
assert speech_file_path.exists()
assert speech_file_path.stat().st_size > 0
# Clean up
speech_file_path.unlink()
def test_speech_mock_response(self):
"""Test speech synthesis with mocked response"""
# Create mock audio data (hex-encoded as MiniMax returns)
mock_audio_bytes = b"fake audio data for testing"
mock_audio_hex = mock_audio_bytes.hex()
mock_response_json = {
"data": {"audio": mock_audio_hex, "status": 0, "ced": ""},
"extra_info": {},
}
with patch(
"litellm.llms.custom_httpx.llm_http_handler.BaseLLMHTTPHandler.text_to_speech_handler"
) as mock_tts:
# Create a mock httpx.Response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {}
mock_response.json.return_value = mock_response_json
mock_response.content = mock_audio_bytes
# Mock the response wrapper
from litellm.types.llms.openai import HttpxBinaryResponseContent
mock_binary_response = HttpxBinaryResponseContent(mock_response)
mock_tts.return_value = mock_binary_response
# This would normally make a real API call
# but we're mocking it for testing
response = speech(
model="minimax/speech-2.6-hd",
voice="alloy",
input="Test input",
api_key="test-key",
)
# Verify the mock was called
assert mock_tts.called
class TestMinimaxProviderRegistration:
"""Test that MiniMax is properly registered as a provider"""
def test_minimax_in_llm_providers(self):
"""Test that MINIMAX is in LlmProviders enum"""
from litellm.types.utils import LlmProviders
assert hasattr(LlmProviders, "MINIMAX")
assert LlmProviders.MINIMAX.value == "minimax"
def test_minimax_in_provider_list(self):
"""Test that minimax is in the provider list"""
assert litellm.LlmProviders.MINIMAX in litellm.provider_list
def test_get_provider_text_to_speech_config(self):
"""Test that MiniMax TTS config can be retrieved"""
from litellm.utils import ProviderConfigManager
config = ProviderConfigManager.get_provider_text_to_speech_config(
model="speech-2.6-hd",
provider=litellm.LlmProviders.MINIMAX,
)
assert config is not None
assert isinstance(config, MinimaxTextToSpeechConfig)
def test_get_llm_provider_minimax(self):
"""Test that get_llm_provider correctly identifies MiniMax models"""
from litellm import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="minimax/speech-2.6-hd"
)
assert model == "speech-2.6-hd"
assert provider == "minimax"
if __name__ == "__main__":
# Run basic tests
test_config = TestMinimaxTextToSpeechConfig()
test_config.test_get_supported_openai_params()
test_config.test_voice_mapping()
test_config.test_format_mapping()
test_config.test_map_openai_params_basic()
test_config.test_map_openai_params_speed_clamping()
test_config.test_transform_text_to_speech_request()
test_config.test_get_complete_url()
test_registration = TestMinimaxProviderRegistration()
test_registration.test_minimax_in_llm_providers()
test_registration.test_minimax_in_provider_list()
test_registration.test_get_provider_text_to_speech_config()
test_registration.test_get_llm_provider_minimax()
print("All basic tests passed!")