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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,10 @@ api_providers:
api_key: your_deepseek_key
base_url: https://api.deepseek.com/v1
enable_proxy: false
minimax:
api_key: your_minimax_key
base_url: https://api.minimax.io/v1
enable_proxy: false
dashscope:
api_key: your_dashscope_key
base_url: https://dashscope.aliyuncs.com/api/v1
Expand Down Expand Up @@ -509,6 +513,7 @@ generation:
| **OpenAI** | `api_providers.openai.api_key` / `base_url` | GPT 文本、视觉模型和 OpenAI 图像模型 |
| **Gemini** | `api_providers.gemini.api_key` / `base_url` | Gemini 文本、视觉模型 |
| **DeepSeek** | `api_providers.deepseek.api_key` / `base_url` | DeepSeek 文本模型 |
| **MiniMax** | `api_providers.minimax.api_key` / `base_url` | MiniMax 文本模型 |
| **DashScope** | `api_providers.dashscope.api_key` / `base_url` | 通义千问、通义万相、Wan 图像/视频等 |
| **火山方舟 ARK** | `api_providers.ark.api_key` / `base_url` | Seedream 图像、Seedance 视频等 |
| **Kling** | `api_providers.kling.access_key` / `secret_key` / `base_url` | 可灵视频生成 |
Expand All @@ -519,7 +524,7 @@ generation:

| 类型 | 模型 |
| :----------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **LLM** | qwen3.6-max-preview, qwen3-max, deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro, gpt-4o, gpt-5, gpt-5.4, gemini-2.5-flash, gemini-2.0-flash, kimi-k2.6 |
| **LLM** | qwen3.6-max-preview, qwen3-max, deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro, MiniMax-M3, MiniMax-M2.7, gpt-4o, gpt-5, gpt-5.4, gemini-2.5-flash, gemini-2.0-flash, kimi-k2.6 |
| **VLM** | qwen3.6-plus, qwen3.6-flash, kimi-k2.6, gpt-5.4, gemini-2.5-flash-image, gemini-2.0-flash |
| **文生图** | wan2.7-image, wan2.7-image-pro, wan2.6-t2i, doubao-seedream-5.0/4.5/4.0, gpt-image-2 |
| **图生图** | wan2.7-image, wan2.7-image-pro, doubao-seedream-5.0/4.5/4.0, gpt-image-2 |
Expand Down
7 changes: 6 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ api_providers:
api_key: your_deepseek_key
base_url: https://api.deepseek.com/v1
enable_proxy: false
minimax:
api_key: your_minimax_key
base_url: https://api.minimax.io/v1
enable_proxy: false
dashscope:
api_key: your_dashscope_key
base_url: https://dashscope.aliyuncs.com/api/v1
Expand Down Expand Up @@ -488,6 +492,7 @@ models:
| **OpenAI** | `api_providers.openai.api_key` / `base_url` | GPT text/vision models and OpenAI image models |
| **Gemini** | `api_providers.gemini.api_key` / `base_url` | Gemini text and vision models |
| **DeepSeek** | `api_providers.deepseek.api_key` / `base_url` | DeepSeek text models |
| **MiniMax** | `api_providers.minimax.api_key` / `base_url` | MiniMax text models |
| **DashScope** | `api_providers.dashscope.api_key` / `base_url` | Qwen, Wan image/video models, and related Alibaba Cloud services |
| **Volcengine ARK** | `api_providers.ark.api_key` / `base_url` | Seedream image models and Seedance video models |
| **Kling** | `api_providers.kling.access_key` / `secret_key` / `base_url` | Kling video generation |
Expand All @@ -498,7 +503,7 @@ You only need to fill in the provider keys required by the models you choose. Fo

| Type | Models |
|:---:|:---|
| **LLM** | qwen3.6-max-preview, qwen3-max, deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro, gpt-4o, gpt-5, gpt-5.4, gemini-2.5-flash, gemini-2.0-flash, kimi-k2.6 |
| **LLM** | qwen3.6-max-preview, qwen3-max, deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro, MiniMax-M3, MiniMax-M2.7, gpt-4o, gpt-5, gpt-5.4, gemini-2.5-flash, gemini-2.0-flash, kimi-k2.6 |
| **VLM** | qwen3.6-plus, qwen3.6-flash, kimi-k2.6, gpt-5.4, gemini-2.5-flash-image, gemini-2.0-flash |
| **Text-to-Image** | wan2.7-image, wan2.7-image-pro, wan2.6-t2i, doubao-seedream-5.0/4.5/4.0, gpt-image-2 |
| **Image-to-Image** | wan2.7-image, wan2.7-image-pro, doubao-seedream-5.0/4.5/4.0, gpt-image-2 |
Expand Down
1 change: 1 addition & 0 deletions video-claw/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ sed -n '1,120p' video-claw/backend/config.yaml
| 平台 | config.yaml 字段 | 获取链接 |
|------|--------------|----------|
| DeepSeek | `api_providers.deepseek.api_key` | https://platform.deepseek.com/api_keys |
| MiniMax | `api_providers.minimax.api_key` | https://platform.minimax.io |
| 阿里云 DashScope | `api_providers.dashscope.api_key` 或 `models.llm.api_key` | https://bailian.console.aliyun.com/cn-beijing/?tab=home#/home |
| 字节火山方舟 | `api_providers.ark.api_key` | https://www.volcengine.com/product/ark |
| 快手可灵 Kling | `api_providers.kling.access_key` / `api_providers.kling.secret_key` | https://klingai.com/cn/dev |
Expand Down
11 changes: 11 additions & 0 deletions video-claw/video-claw/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"base_url": "https://api.deepseek.com/v1",
"enable_proxy": False,
},
"minimax": {
"api_key": "",
"base_url": "https://api.minimax.io/v1",
"enable_proxy": False,
},
"dashscope": {
"api_key": "",
"base_url": "https://dashscope.aliyuncs.com/api/v1",
Expand Down Expand Up @@ -220,6 +225,9 @@ class Config:
DEEPSEEK_API_KEY = _get(CONFIG, "api_providers.deepseek.api_key")
DEEPSEEK_BASE_URL = _get(CONFIG, "api_providers.deepseek.base_url")
DEEPSEEK_ENABLE_PROXY = _get(CONFIG, "api_providers.deepseek.enable_proxy")
MINIMAX_API_KEY = _get(CONFIG, "api_providers.minimax.api_key")
MINIMAX_BASE_URL = _get(CONFIG, "api_providers.minimax.base_url")
MINIMAX_ENABLE_PROXY = _get(CONFIG, "api_providers.minimax.enable_proxy")
DASHSCOPE_API_KEY = _get(CONFIG, "api_providers.dashscope.api_key")
DASHSCOPE_BASE_URL = _get(CONFIG, "api_providers.dashscope.base_url")
DASHSCOPE_ENABLE_PROXY = _get(CONFIG, "api_providers.dashscope.enable_proxy")
Expand Down Expand Up @@ -294,6 +302,9 @@ def update_config(cls, values: Dict[str, Any]) -> Dict[str, Any]:
cls.DEEPSEEK_API_KEY = _get(clean, "api_providers.deepseek.api_key")
cls.DEEPSEEK_BASE_URL = _get(clean, "api_providers.deepseek.base_url")
cls.DEEPSEEK_ENABLE_PROXY = _get(clean, "api_providers.deepseek.enable_proxy")
cls.MINIMAX_API_KEY = _get(clean, "api_providers.minimax.api_key")
cls.MINIMAX_BASE_URL = _get(clean, "api_providers.minimax.base_url")
cls.MINIMAX_ENABLE_PROXY = _get(clean, "api_providers.minimax.enable_proxy")
cls.DASHSCOPE_API_KEY = _get(clean, "api_providers.dashscope.api_key")
cls.DASHSCOPE_BASE_URL = _get(clean, "api_providers.dashscope.base_url")
cls.DASHSCOPE_ENABLE_PROXY = _get(clean, "api_providers.dashscope.enable_proxy")
Expand Down
4 changes: 4 additions & 0 deletions video-claw/video-claw/backend/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ api_providers:
api_key: ''
base_url: https://api.deepseek.com/v1
enable_proxy: false
minimax:
api_key: ''
base_url: https://api.minimax.io/v1
enable_proxy: false
dashscope:
api_key: ''
base_url: https://dashscope.aliyuncs.com/api/v1
Expand Down
14 changes: 14 additions & 0 deletions video-claw/video-claw/backend/models/config_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
'concurrency': 10,
'price_per_1k_input_token': 0.002,
'price_per_1k_output_token': 0.008},
'MiniMax-M3': {'name': 'MiniMax M3',
'provider': 'minimax',
'family': 'minimax',
'type': ['llm'],
'concurrency': 10,
'price_per_1k_input_token': 0.0003,
'price_per_1k_output_token': 0.0012},
'MiniMax-M2.7': {'name': 'MiniMax M2.7',
'provider': 'minimax',
'family': 'minimax',
'type': ['llm'],
'concurrency': 10,
'price_per_1k_input_token': 0.0003,
'price_per_1k_output_token': 0.0012},
'gpt-4o': {'name': 'GPT-4o',
'provider': 'openai',
'type': ['llm'],
Expand Down
19 changes: 18 additions & 1 deletion video-claw/video-claw/backend/models/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
from models.llm_gpt import GPT
from models.llm_gemini import Gemini
from models.llm_deepseek import DeepSeek
from models.llm_minimax import MiniMax
from models.llm_dashscope import QwenLLM
from models.vlm_dashscope import QwenVLClient
except ImportError:
from llm_gpt import GPT
from llm_gemini import Gemini
from llm_deepseek import DeepSeek
from llm_minimax import MiniMax
from llm_dashscope import QwenLLM
from vlm_dashscope import QwenVLClient

Expand All @@ -26,18 +28,21 @@
logger = logging.getLogger(__name__)

class LLM:
def __init__(self, gemini_base_url="", gemini_api_key="", gpt_base_url="", gpt_api_key="", deepseek_base_url="", deepseek_api_key="", dashscope_api_key=""):
def __init__(self, gemini_base_url="", gemini_api_key="", gpt_base_url="", gpt_api_key="", deepseek_base_url="", deepseek_api_key="", minimax_base_url="", minimax_api_key="", dashscope_api_key=""):
self._gemini_base_url = gemini_base_url or Config.GOOGLE_GEMINI_BASE_URL
self._gemini_api_key = gemini_api_key or Config.GEMINI_API_KEY
self._gpt_base_url = gpt_base_url or Config.OPENAI_BASE_URL
self._gpt_api_key = gpt_api_key or Config.OPENAI_API_KEY
self._deepseek_base_url = deepseek_base_url or Config.DEEPSEEK_BASE_URL
self._deepseek_api_key = deepseek_api_key or Config.DEEPSEEK_API_KEY
self._minimax_base_url = minimax_base_url or Config.MINIMAX_BASE_URL
self._minimax_api_key = minimax_api_key or Config.MINIMAX_API_KEY
self._dashscope_api_key = dashscope_api_key or Config.DASHSCOPE_API_KEY

self._gemini_client = None
self._gpt_client = None
self._deepseek_client = None
self._minimax_client = None
self._dashscope_client = None
self._dashscope_vl_client = None

Expand Down Expand Up @@ -69,6 +74,15 @@ def deepseek_client(self):
)
return self._deepseek_client

@property
def minimax_client(self):
if self._minimax_client is None:
self._minimax_client = MiniMax(
base_url=self._minimax_base_url,
api_key=self._minimax_api_key,
)
return self._minimax_client

@property
def dashscope_client(self):
if self._dashscope_client is None:
Expand Down Expand Up @@ -128,6 +142,9 @@ def query(self, prompt, image_urls=[], model="qwen3.6-max-preview", safe_content
elif "gpt" in model_lower:
# OpenAI series models
result = self.gpt_client.query(prompt, image_urls=image_urls, model=model, web_search=web_search)
elif model_lower.startswith("minimax"):
# MiniMax series models (OpenAI-compatible interface)
result = self.minimax_client.query(prompt, image_urls=image_urls, model=model, web_search=web_search)
elif "kimi" in model_lower or "qwen3.6-plus" in model_lower or "qwen3.6-flash" in model_lower or "vl" in model_lower:
# DashScope VLM models (using MultiModalConversation API)
result = self.dashscope_vl_client.chat(text=prompt, images=image_urls, model=model, stream=False)
Expand Down
134 changes: 134 additions & 0 deletions video-claw/video-claw/backend/models/llm_minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import os
import sys

models_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.dirname(models_dir)
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)

import time
import logging
from openai import OpenAI
from config import Config

logger = logging.getLogger(__name__)

DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1"
DEFAULT_MINIMAX_TIMEOUT = 300
DEFAULT_MINIMAX_MAX_ATTEMPTS = 5
DEFAULT_MINIMAX_MAX_TOKENS = 8000


class MiniMax:
"""
MiniMax LLM 客户端(OpenAI 兼容接口)

MiniMax-M3: 最新旗舰模型,1M 上下文,最大输出 128K,支持图片输入
MiniMax-M2.7: 上一代模型,作为兼容选项保留

注意:
- Base URL 使用海外版 https://api.minimax.io/v1
- temperature 取值范围为 (0.0, 1.0],默认 1.0,不能传 0
"""
def __init__(self, base_url="", api_key="", timeout=None, max_attempts=None):
import httpx
self.base_url = base_url or Config.MINIMAX_BASE_URL or DEFAULT_MINIMAX_BASE_URL
self.api_key = api_key or Config.MINIMAX_API_KEY
self.timeout = self._as_int(timeout, DEFAULT_MINIMAX_TIMEOUT)
self.max_attempts = self._as_int(max_attempts, DEFAULT_MINIMAX_MAX_ATTEMPTS)

if not self.api_key:
logger.warning("MINIMAX_API_KEY is not set")

kwargs = {"api_key": self.api_key, "base_url": self.base_url, "timeout": self.timeout}
proxy = Config.provider_proxy("minimax")
if proxy:
kwargs["http_client"] = httpx.Client(proxy=proxy, timeout=self.timeout)
self.client = OpenAI(**kwargs)
self.max_tokens = DEFAULT_MINIMAX_MAX_TOKENS

@staticmethod
def _as_int(value, default: int) -> int:
try:
return int(value)
except (TypeError, ValueError):
return default

def query(self, prompt, image_urls=[], model="MiniMax-M3", web_search=False):
"""
Query MiniMax model via the OpenAI-compatible Chat Completions API.

:param prompt: Text prompt
:param image_urls: Optional image URLs for multimodal input
:param model: Model name (e.g., MiniMax-M3, MiniMax-M2.7)
:param web_search: Unused for MiniMax, kept for interface consistency
"""
if not model:
model = "MiniMax-M3"

messages = [{"role": "system", "content": "You are a helpful assistant."}]
content = [{"type": "text", "text": prompt}]
if image_urls:
content.extend([{"type": "image_url", "image_url": {"url": url}} for url in image_urls])
messages.append({"role": "user", "content": content})

attempts = 0
while attempts < self.max_attempts:
try:
response = self.client.chat.completions.create(
model=model,
messages=messages,
max_tokens=self.max_tokens,
temperature=1.0, # MiniMax requires temperature in (0.0, 1.0]
)

if response.choices and response.choices[0].message.content:
return response.choices[0].message.content
logger.warning("MiniMax returned an empty response; retrying")
time.sleep(2)
except Exception as e:
logger.warning(
"MiniMax request failed; retrying. model=%s attempt=%s/%s timeout=%ss error=%s",
model,
attempts + 1,
self.max_attempts,
self.timeout,
e,
)
time.sleep(5)
attempts += 1

raise Exception("Max attempts reached, failed to get a response from MiniMax.")


if __name__ == "__main__":
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import Config

# 支持的模型列表
MODELS = ["MiniMax-M3", "MiniMax-M2.7"]

print("=== MiniMax 文本生成可用性测试 ===")
api_key = Config.MINIMAX_API_KEY
base_url = Config.MINIMAX_BASE_URL
if not api_key:
print("✗ MINIMAX_API_KEY 未设置,跳过")
sys.exit(1)
print(f" API Key: {api_key[:6]}***{api_key[-4:]}")
if base_url:
print(f" Base URL: {base_url}")

client = MiniMax(api_key=api_key, base_url=base_url)
prompt = "用一句话介绍你自己。"
print(f" Prompt: {prompt}")

for model in MODELS:
print(f"\n--- 测试模型: {model} ---")
t0 = time.time()
try:
resp = client.query(prompt, model=model)
elapsed = time.time() - t0
print(f"✓ 响应 ({elapsed:.1f}s): {resp.strip()[:200]}")
except Exception as e:
print(f"✗ 失败: {e}")
59 changes: 59 additions & 0 deletions video-claw/video-claw/backend/tests/test_llm_minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Unit tests for the MiniMax LLM provider integration."""

import os
import sys

BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR)


def test_minimax_provider_in_config_defaults():
"""MiniMax should be registered as an api_provider with the overseas base URL."""
from config import Config

config = Config.as_dict()
providers = config["api_providers"]
assert "minimax" in providers
assert providers["minimax"]["base_url"] == "https://api.minimax.io/v1"
assert "api_key" in providers["minimax"]
assert providers["minimax"]["enable_proxy"] is False


def test_minimax_models_registered():
"""MiniMax-M3 (new default) and MiniMax-M2.7 (compat) should be llm models."""
from models.config_model import get_model_config, get_models_by_type

llm_ids = {model["id"] for model in get_models_by_type("llm")}
assert "MiniMax-M3" in llm_ids
assert "MiniMax-M2.7" in llm_ids

m3 = get_model_config("MiniMax-M3")
assert m3["provider"] == "minimax"
assert "llm" in m3["type"]


def test_minimax_client_default_base_url():
"""Client should fall back to the overseas base URL and default model M3."""
from models.llm_minimax import DEFAULT_MINIMAX_BASE_URL, MiniMax

client = MiniMax(api_key="test-key", base_url="")
assert client.base_url == DEFAULT_MINIMAX_BASE_URL
assert client.base_url.endswith("/v1")


def test_minimax_client_custom_base_url():
from models.llm_minimax import MiniMax

client = MiniMax(api_key="test-key", base_url="https://example.com/v1")
assert client.base_url == "https://example.com/v1"


def test_minimax_client_uses_openai_sdk():
"""The provider should be implemented on top of the OpenAI-compatible SDK."""
from models.llm_minimax import MiniMax

client = MiniMax(api_key="test-key")
# OpenAI client exposes chat.completions.create
assert hasattr(client.client, "chat")
assert hasattr(client.client.chat, "completions")
Loading