Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ GOOGLE_API_KEY= # Google Imagen images, Google Cloud TTS (700+ voic
ELEVENLABS_API_KEY= # TTS narration, music generation, sound effects
OPENAI_API_KEY= # OpenAI TTS fallback and DALL-E image generation
XAI_API_KEY= # Grok image generation/editing and Grok video generation
MINIMAX_API_KEY= # MiniMax chat (MiniMax-M2.7) + TTS (speech-2.8-hd / turbo) — get one at https://platform.minimax.io/
# Piper local voices do not require env vars; install `piper-tts` via pip

# --- Music ---
Expand Down
91 changes: 91 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ GOOGLE_API_KEY= # Google TTS + Google Imagen
ELEVENLABS_API_KEY= # TTS, music, sound effects (10K chars/month free)
OPENAI_API_KEY= # OpenAI TTS + DALL-E 3 images
XAI_API_KEY= # xAI Grok image generation/editing + Grok video generation
MINIMAX_API_KEY= # MiniMax chat (MiniMax-M2.7) + TTS (speech-2.8-hd / turbo)

# MULTI-MODEL GATEWAY (one key, 6+ tools)
FAL_KEY= # FLUX, Recraft, Kling, Veo, MiniMax video
Expand Down Expand Up @@ -262,6 +263,96 @@ Google TTS offers 700+ voices across 50+ languages. Voice names follow the patte

---

### MiniMax — Chat + TTS

> **Cost-effective chat and TTS under one API key.** MiniMax-M2.7 is a frontier-class model accessible via an OpenAI-compatible API. The same key also unlocks the TTS provider. No subscription required.

**Tools unlocked:** `minimax_tts`; LLM provider via `lib/providers` (set `llm.provider: minimax` in `config.yaml`)
**Env var:** `MINIMAX_API_KEY`

#### Chat Models

| Model | Notes |
|-------|-------|
| `MiniMax-M2.7` | Peak performance. Ultimate value. (default) |
| `MiniMax-M2.7-highspeed` | Same capability, faster and more agile |

**API reference:** <https://platform.minimax.io/docs/api-reference/text-openai-api>

#### Chat Usage

Set `llm.provider: minimax` in `config.yaml` (or override per-run) to route
OpenMontage's LLM calls through MiniMax:

```yaml
# config.yaml
llm:
provider: minimax
model: MiniMax-M2.7 # optional — MiniMax-M2.7 is the default
temperature: 1.0 # MiniMax range: (0.0, 1.0] — 0 is not accepted
max_tokens: 4096
```

Then in Python:

```python
from lib.config_model import OpenMontageConfig
from lib.providers import build_provider

config = OpenMontageConfig.load()
provider = build_provider(config.llm)
response = provider.chat([{"role": "user", "content": "Hello!"}])
```

**Env var:** `MINIMAX_API_KEY` — get one at <https://platform.minimax.io/>

---

### MiniMax — TTS

> **Cost-effective TTS with a broad voice catalogue.** The same `MINIMAX_API_KEY` covers TTS. No subscription required.

**Tools unlocked:** `minimax_tts`
**Env var:** `MINIMAX_API_KEY`

#### Setup

1. Go to [platform.minimax.io](https://platform.minimax.io/) and create an account
2. Navigate to **API Keys** in your account settings
3. Create a key and copy it
4. Add to `.env`: `MINIMAX_API_KEY=your-key-here`

#### TTS Pricing

| Model | Price per 1M characters |
|-------|------------------------|
| `speech-2.8-hd` | ~$100.00 |
| `speech-2.8-turbo` | ~$50.00 |

**Free tier:** MiniMax offers a free trial quota for new accounts. Check [platform.minimax.io/docs/guides/pricing-paygo](https://platform.minimax.io/docs/guides/pricing-paygo) for current rates.

#### Supported Voices (English)

| Voice ID | Style |
|----------|-------|
| `English_expressive_narrator` | Expressive narration (default) |
| `English_Graceful_Lady` | Elegant female |
| `English_Insightful_Speaker` | Calm male |
| `English_radiant_girl` | Upbeat female |
| `English_Persuasive_Man` | Authoritative male |
| `English_Lucky_Robot` | Sci-fi robot |

Full voice list: [platform.minimax.io/faq/system-voice-id](https://platform.minimax.io/faq/system-voice-id)

#### TTS Models

| Model | Speed | Quality |
|-------|-------|---------|
| `speech-2.8-hd` | Standard | Highest similarity (recommended default) |
| `speech-2.8-turbo` | Fast | High quality, lower latency |

---

### Runway — Gen-3/Gen-4 Video

> **Highest-rated AI video quality.** #1 on Elo rankings. Professional-grade video generation with Gen-3 Alpha Turbo, Gen-4 Turbo, and Gen-4 Aleph models.
Expand Down
71 changes: 71 additions & 0 deletions lib/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""LLM provider registry for OpenMontage.

Usage
-----
Import :func:`build_provider` to get a provider instance from a
:class:`~lib.config_model.LLMConfig`::

from lib.config_model import OpenMontageConfig
from lib.providers import build_provider

config = OpenMontageConfig.load()
provider = build_provider(config.llm)
response = provider.chat([{"role": "user", "content": "Hello!"}])

Supported ``llm.provider`` values
----------------------------------
* ``minimax`` — MiniMax-M2.7 via OpenAI-compatible API

More providers can be registered here as the project grows.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from lib.providers.base import BaseLLMProvider
from lib.providers.minimax import MiniMaxProvider

if TYPE_CHECKING:
from lib.config_model import LLMConfig

__all__ = [
"BaseLLMProvider",
"MiniMaxProvider",
"build_provider",
]

_REGISTRY: dict[str, type[BaseLLMProvider]] = {
"minimax": MiniMaxProvider,
}


def build_provider(llm_config: "LLMConfig") -> BaseLLMProvider:
"""Instantiate the LLM provider described by *llm_config*.

Parameters
----------
llm_config:
The ``llm`` section of :class:`~lib.config_model.OpenMontageConfig`.

Returns
-------
BaseLLMProvider
A ready-to-use provider instance.

Raises
------
ValueError
When ``llm_config.provider`` names an unsupported provider.
"""
key = llm_config.provider.lower()
cls = _REGISTRY.get(key)
if cls is None:
supported = ", ".join(sorted(_REGISTRY))
raise ValueError(
f"Unsupported LLM provider {key!r}. "
f"Supported values: {supported}. "
"For other providers (anthropic, openai, gemini, …) use the "
"native SDK of the coding assistant driving OpenMontage."
)
return cls()
34 changes: 34 additions & 0 deletions lib/providers/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Abstract base class for LLM provider clients.

Each provider wraps an HTTP API and exposes a unified chat() interface
that mirrors the OpenAI chat completions contract.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any


class BaseLLMProvider(ABC):
"""Minimal contract every LLM provider must satisfy."""

#: Short machine-readable provider name (e.g. "minimax", "openai")
name: str = ""

@abstractmethod
def chat(self, messages: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]:
"""Send a chat completion request and return the raw API response.

Args:
messages: List of role/content message dicts (OpenAI format).
**kwargs: Extra parameters forwarded to the underlying API
(model, temperature, max_tokens, …).

Returns:
The raw response body as a Python dict.
"""

def get_info(self) -> dict[str, Any]:
"""Return provider metadata."""
return {"name": self.name}
146 changes: 146 additions & 0 deletions lib/providers/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""MiniMax chat-model provider (OpenAI-compatible interface).

Wraps the MiniMax /v1/chat/completions endpoint so that any part of
OpenMontage that needs a general-purpose LLM client can route to MiniMax
by setting ``llm.provider: minimax`` in *config.yaml*.

Models supported
----------------
* ``MiniMax-M2.7`` – Peak performance. Ultimate value.
* ``MiniMax-M2.7-highspeed`` – Same capability, faster and more agile.

API reference
-------------
https://platform.minimax.io/docs/api-reference/text-openai-api

Environment variable
--------------------
``MINIMAX_API_KEY`` — obtain a key at https://platform.minimax.io/
"""

from __future__ import annotations

import os
from typing import Any

import requests

from lib.providers.base import BaseLLMProvider

_CHAT_ENDPOINT = "https://api.minimax.io/v1/chat/completions"

MINIMAX_MODELS = [
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
]

_DEFAULT_MODEL = "MiniMax-M2.7"
_DEFAULT_TEMPERATURE = 1.0 # MiniMax range is (0.0, 1.0]; 0 is not accepted


class MiniMaxProvider(BaseLLMProvider):
"""MiniMax chat provider using the OpenAI-compatible API.

Parameters
----------
api_key:
MiniMax API key. Falls back to the ``MINIMAX_API_KEY`` environment
variable when not supplied explicitly.
base_url:
Override the default endpoint root (useful for testing or proxies).
"""

name = "minimax"

def __init__(
self,
api_key: str | None = None,
base_url: str = "https://api.minimax.io",
) -> None:
self._api_key = api_key or os.environ.get("MINIMAX_API_KEY", "")
self._base_url = base_url.rstrip("/")

# ------------------------------------------------------------------
# Public interface
# ------------------------------------------------------------------

@property
def is_available(self) -> bool:
"""Return True if an API key is configured."""
return bool(self._api_key)

def chat(
self,
messages: list[dict[str, Any]],
*,
model: str = _DEFAULT_MODEL,
temperature: float = _DEFAULT_TEMPERATURE,
max_tokens: int = 4096,
**kwargs: Any,
) -> dict[str, Any]:
"""Send a chat completion request to MiniMax.

Parameters
----------
messages:
Conversation history in OpenAI format
(``[{"role": "user", "content": "..."}]``).
model:
One of :data:`MINIMAX_MODELS`. Defaults to ``MiniMax-M2.7``.
temperature:
Sampling temperature in the range ``(0.0, 1.0]``.
Values of 0 are not accepted by the MiniMax API.
max_tokens:
Maximum tokens in the completion.
**kwargs:
Additional fields forwarded verbatim to the API payload.

Returns
-------
dict
Raw response body from the MiniMax API (OpenAI-compatible shape).

Raises
------
EnvironmentError
When no API key is configured.
requests.HTTPError
On non-2xx responses from the API.
"""
if not self._api_key:
raise EnvironmentError(
"MINIMAX_API_KEY is not set. "
"Get a key at https://platform.minimax.io/"
)

# Clamp temperature: MiniMax rejects 0; cap at 1.0
temperature = max(0.01, min(float(temperature), 1.0))

payload: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
**kwargs,
}

response = requests.post(
f"{self._base_url}/v1/chat/completions",
headers={
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json",
},
json=payload,
timeout=120,
)
response.raise_for_status()
return response.json()

def get_info(self) -> dict[str, Any]:
return {
"name": self.name,
"models": MINIMAX_MODELS,
"default_model": _DEFAULT_MODEL,
"base_url": self._base_url,
"available": self.is_available,
}
Loading