feat: add MiniMax provider support (Chat + TTS) (#309)

* feat: add MiniMax provider support (Chat + TTS)

- Add MiniMax LLM provider using OpenAI-compatible API
  - Models: MiniMax-M2.7, MiniMax-M2.7-highspeed
  - Default base URL: https://api.minimax.io/v1
  - Uses MINIMAX_API_KEY for authentication
- Add MiniMax TTS provider using Pipecat's MiniMaxHttpTTSService
  - Models: speech-2.8-hd (default), speech-2.8-turbo
  - 6 built-in voices
  - Requires group_id configuration
- Add unit tests for both providers

* fix(minimax): validator, temperature, session cleanup, reasoning filter
  - check_validity.py: wire MiniMax into _validator_map and enforce
    group_id at save time. Without this, saving a config with a valid
    key was rejected.
  - registry.py: surface temperature on the LLM config (gt=0; MiniMax
    rejects 0) and base_url on the TTS config
  - service_factory.py:
    * Plumb temperature through create_llm_service
    * Normalize TTS base_url to include /t2a_v2 — pipecat appends only
      ?GroupId=... to the URL.
    * Use the new MiniMaxLLMService (from pipecat) to strip
      <think>...</think> reasoning that MiniMax-M2.7 emits inline in
      delta.content (otherwise it leaks straight to TTS).
    * Use MiniMaxOwnedSessionTTSService so the per-instance aiohttp
      session gets closed in cleanup() instead of leaking sockets/FDs.
  - minimax_tts.py: small wrapper around MiniMaxHttpTTSService that owns
    the session it was handed (pipecat's caller-owns-session API
    conflicts with the ftory's per-instance pattern).
  - pipecat submodule: bumps to a commit that adds MiniMaxLLMService — a
    thin OpenAILLMService subclass with the streaming <think> filter
    (mirrors NvidiaLLMService's pattern for NIM reasoning models).
  - Tests updated/added for all of the above.

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
Octopus 2026-05-22 15:39:41 +08:00 committed by GitHub
parent 38c2003734
commit 0e0d3136ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 291 additions and 1 deletions

View file

@ -0,0 +1,23 @@
"""MiniMax TTS wrapper that closes its aiohttp session in cleanup().
Pipecat's MiniMaxHttpTTSService leaves session disposal to the caller. Our
factory creates a fresh session per service instance, so we own its close
here to avoid leaking sockets/FDs on shutdown.
"""
import aiohttp
from pipecat.services.minimax.tts import MiniMaxHttpTTSService
class MiniMaxOwnedSessionTTSService(MiniMaxHttpTTSService):
"""MiniMaxHttpTTSService variant that owns its aiohttp session lifecycle."""
def __init__(self, *args, aiohttp_session: aiohttp.ClientSession, **kwargs):
super().__init__(*args, aiohttp_session=aiohttp_session, **kwargs)
self._owned_session = aiohttp_session
async def cleanup(self):
await super().cleanup()
if not self._owned_session.closed:
await self._owned_session.close()

View file

@ -1,10 +1,12 @@
from typing import TYPE_CHECKING
import aiohttp
from fastapi import HTTPException
from loguru import logger
from api.constants import MPS_API_URL
from api.services.configuration.registry import ServiceProviders
from api.services.pipecat.minimax_tts import MiniMaxOwnedSessionTTSService
from pipecat.services.assemblyai.stt import AssemblyAISTTService, AssemblyAISTTSettings
from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings
from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings
@ -36,6 +38,8 @@ from pipecat.services.openai.stt import (
from pipecat.services.openai.tts import OpenAITTSService, OpenAITTSSettings
from pipecat.services.openrouter.llm import OpenRouterLLMService, OpenRouterLLMSettings
from pipecat.services.rime.tts import RimeTTSService, RimeTTSSettings
from pipecat.services.minimax.llm import MiniMaxLLMService
from pipecat.services.minimax.tts import MiniMaxHttpTTSService, MiniMaxTTSSettings
from pipecat.services.sarvam.stt import SarvamSTTService, SarvamSTTSettings
from pipecat.services.sarvam.tts import SarvamTTSService, SarvamTTSSettings
from pipecat.services.speaches.llm import SpeachesLLMService, SpeachesLLMSettings
@ -392,6 +396,40 @@ def create_tts_service(user_config, audio_config: "AudioConfig"):
skip_aggregator_types=["recording_router", "recording"],
silence_time_s=1.0,
)
elif user_config.tts.provider == ServiceProviders.MINIMAX.value:
group_id = getattr(user_config.tts, "group_id", None)
if not group_id:
raise HTTPException(
status_code=400,
detail="MiniMax TTS requires a group_id. Configure it in your TTS settings.",
)
voice = getattr(user_config.tts, "voice", None) or "English_Graceful_Lady"
speed = getattr(user_config.tts, "speed", None) or 1.0
# Pipecat appends "?GroupId=..." to base_url as-is, so /t2a_v2 must
# already be in the path.
base_url = (
getattr(user_config.tts, "base_url", None)
or "https://api.minimax.io/v1/t2a_v2"
).rstrip("/")
if not base_url.endswith("/t2a_v2"):
base_url = f"{base_url}/t2a_v2"
session = aiohttp.ClientSession()
return MiniMaxOwnedSessionTTSService(
api_key=user_config.tts.api_key,
group_id=group_id,
base_url=base_url,
aiohttp_session=session,
settings=MiniMaxTTSSettings(
model=user_config.tts.model,
voice=voice,
speed=speed,
),
text_filters=[xml_function_tag_filter],
skip_aggregator_types=["recording_router", "recording"],
silence_time_s=1.0,
)
else:
raise HTTPException(
status_code=400, detail=f"Invalid TTS provider {user_config.tts.provider}"
@ -408,6 +446,7 @@ def create_llm_service_from_provider(
aws_access_key: str | None = None,
aws_secret_key: str | None = None,
aws_region: str | None = None,
temperature: float | None = None,
):
"""Create an LLM service from explicit provider/model/api_key.
@ -471,6 +510,15 @@ def create_llm_service_from_provider(
api_key=api_key or "none",
settings=SpeachesLLMSettings(model=model),
)
elif provider == ServiceProviders.MINIMAX.value:
return MiniMaxLLMService(
api_key=api_key,
base_url=base_url or "https://api.minimax.io/v1",
settings=MiniMaxLLMService.Settings(
model=model,
temperature=temperature if temperature is not None else 1.0,
),
)
else:
raise HTTPException(status_code=400, detail=f"Invalid LLM provider {provider}")
@ -581,5 +629,8 @@ def create_llm_service(user_config):
kwargs["aws_access_key"] = user_config.llm.aws_access_key
kwargs["aws_secret_key"] = user_config.llm.aws_secret_key
kwargs["aws_region"] = user_config.llm.aws_region
elif provider == ServiceProviders.MINIMAX.value:
kwargs["base_url"] = user_config.llm.base_url
kwargs["temperature"] = user_config.llm.temperature
return create_llm_service_from_provider(provider, model, api_key, **kwargs)