Skip to content
Merged
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.template
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ TAVILY_API_KEY= # 获取搜索服务的 api key 请访问 https://app.tavily.co
# DASHSCOPE_API_KEY=
# DEEPSEEK_API_KEY=
# ARK_API_KEY=
# MINIMAX_API_KEY= # MiniMax 大模型 https://platform.minimaxi.com/
# TOGETHER_API_KEY=
# # endregion model_provider

Expand Down
1 change: 1 addition & 0 deletions docs/latest/intro/model-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| [硅基流动](https://cloud.siliconflow.cn/i/Eo5yTHGJ) | `SILICONFLOW_API_KEY` | 🆓 免费额度,默认推荐 |
| OpenAI | `OPENAI_API_KEY` | GPT 系列模型 |
| DeepSeek | `DEEPSEEK_API_KEY` | 国产大模型 |
| [MiniMax](https://platform.minimaxi.com/) | `MINIMAX_API_KEY` | M2.7/M2.5 系列,百万 token 上下文 |
| OpenRouter | `OPENROUTER_API_KEY` | 多模型聚合平台 |
| 智谱清言 | `ZHIPUAI_API_KEY` | GLM 系列模型 |
| 阿里云百炼 | `DASHSCOPE_API_KEY` | 通义千问系列 |
Expand Down
13 changes: 13 additions & 0 deletions src/config/static/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ class RerankerInfo(BaseModel):
"doubao-seed-2-0-mini-260215",
],
),
"minimax": ChatModelProvider(
name="MiniMax",
url="https://platform.minimaxi.com/document/introduction",
base_url="https://api.minimax.io/v1",
default="MiniMax-M2.7",
env="MINIMAX_API_KEY",
models=[
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
],
),
"openrouter": ChatModelProvider(
name="OpenRouter",
url="https://openrouter.ai/models",
Expand Down
280 changes: 280 additions & 0 deletions test/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
"""
Unit tests for MiniMax provider configuration and model selection.

These tests validate MiniMax integration without requiring Docker services.
Run with: uv run python -m pytest test/test_minimax_provider.py -v
"""

from __future__ import annotations

import importlib.util
import os
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

# ============================================================
# Helpers: Import static models module directly (bypass src.__init__)
# ============================================================

_PROJECT_ROOT = Path(__file__).resolve().parent.parent


def _load_static_models():
"""Load src/config/static/models.py directly, bypassing src.__init__.py."""
models_path = _PROJECT_ROOT / "src" / "config" / "static" / "models.py"
spec = importlib.util.spec_from_file_location("_static_models", models_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


_models_mod = _load_static_models()
DEFAULT_CHAT_MODEL_PROVIDERS = _models_mod.DEFAULT_CHAT_MODEL_PROVIDERS
ChatModelProvider = _models_mod.ChatModelProvider


# ============================================================
# Unit Tests: Provider Configuration
# ============================================================


class TestMiniMaxProviderConfig:
"""Verify MiniMax is correctly registered in default providers."""

def test_minimax_in_default_providers(self):
assert "minimax" in DEFAULT_CHAT_MODEL_PROVIDERS

def test_minimax_provider_type(self):
assert isinstance(DEFAULT_CHAT_MODEL_PROVIDERS["minimax"], ChatModelProvider)

def test_minimax_name(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].name == "MiniMax"

def test_minimax_base_url(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].base_url == "https://api.minimax.io/v1"

def test_minimax_env_var(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].env == "MINIMAX_API_KEY"

def test_minimax_default_model(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].default == "MiniMax-M2.7"

def test_minimax_models_list(self):
expected = ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed"]
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].models == expected

def test_minimax_not_custom(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].custom is False

def test_minimax_has_documentation_url(self):
assert DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].url.startswith("https://")

def test_minimax_all_models_have_minimax_prefix(self):
for model in DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].models:
assert model.startswith("MiniMax-"), f"Model {model} should start with 'MiniMax-'"


# ============================================================
# Unit Tests: Provider Serialization
# ============================================================


class TestMiniMaxProviderSerialization:
"""Test that MiniMax provider config serializes correctly."""

def test_minimax_model_dump(self):
dumped = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].model_dump()
assert dumped["name"] == "MiniMax"
assert dumped["base_url"] == "https://api.minimax.io/v1"
assert dumped["env"] == "MINIMAX_API_KEY"
assert dumped["default"] == "MiniMax-M2.7"
assert len(dumped["models"]) == 4
assert dumped["custom"] is False

def test_minimax_model_dump_round_trip(self):
provider = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]
restored = ChatModelProvider(**provider.model_dump())
assert restored == provider

def test_minimax_model_dump_keys(self):
dumped = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"].model_dump()
assert set(dumped.keys()) == {"name", "url", "base_url", "default", "env", "models", "custom"}


# ============================================================
# Unit Tests: Model Selection (standalone, no Docker deps)
# ============================================================


class TestMiniMaxModelSelection:
"""Test model selection logic with MiniMax provider."""

def _build_select_model(self, providers_dict):
"""Build a standalone select_model function mirroring src/models/chat.py."""
from openai import AsyncOpenAI

class _Model:
def __init__(self, api_key, base_url, model_name):
self.api_key = api_key
self.base_url = base_url
self.model_name = model_name

def select_model(model_provider=None, model_name=None, model_spec=None):
if model_spec:
parts = model_spec.split("/", 1)
model_provider = model_provider or parts[0]
model_name = model_name or (parts[1] if len(parts) > 1 else "")

assert model_provider, "Model provider not specified"
info = providers_dict.get(model_provider)
if not info:
raise ValueError(f"Unknown model provider: {model_provider}")
model_name = model_name or info.default
return _Model(
api_key=os.environ.get(info.env, info.env),
base_url=info.base_url,
model_name=model_name,
)

return select_model

def test_select_model_minimax_default(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"}):
model = select_model("minimax", "MiniMax-M2.7")
assert model.model_name == "MiniMax-M2.7"
assert model.base_url == "https://api.minimax.io/v1"

def test_select_model_minimax_highspeed(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"}):
model = select_model("minimax", "MiniMax-M2.7-highspeed")
assert model.model_name == "MiniMax-M2.7-highspeed"

def test_select_model_minimax_uses_default(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"}):
model = select_model("minimax")
assert model.model_name == "MiniMax-M2.7"

def test_select_model_minimax_from_spec(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"}):
model = select_model(model_spec="minimax/MiniMax-M2.5")
assert model.model_name == "MiniMax-M2.5"
assert model.base_url == "https://api.minimax.io/v1"

def test_select_model_minimax_api_key_from_env(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with patch.dict(os.environ, {"MINIMAX_API_KEY": "my-secret-key"}):
model = select_model("minimax", "MiniMax-M2.7")
assert model.api_key == "my-secret-key"

def test_select_model_unknown_provider_raises(self):
select_model = self._build_select_model({"minimax": DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]})
with pytest.raises(ValueError, match="Unknown model provider"):
select_model("nonexistent", "some-model")


# ============================================================
# Unit Tests: LangChain OpenAI-compat Loading
# ============================================================


class TestMiniMaxLangChainLoading:
"""Test ChatOpenAI instantiation for MiniMax (OpenAI-compat)."""

def test_langchain_openai_compat_for_minimax(self):
from langchain_openai import ChatOpenAI
from pydantic import SecretStr

minimax = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]
model = ChatOpenAI(
model="MiniMax-M2.7",
api_key=SecretStr("test-key-123"),
base_url=minimax.base_url,
stream_usage=True,
)
assert isinstance(model, ChatOpenAI)
assert model.model_name == "MiniMax-M2.7"

def test_langchain_minimax_highspeed(self):
from langchain_openai import ChatOpenAI
from pydantic import SecretStr

minimax = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]
model = ChatOpenAI(
model="MiniMax-M2.5-highspeed",
api_key=SecretStr("test-key-123"),
base_url=minimax.base_url,
)
assert isinstance(model, ChatOpenAI)
assert model.model_name == "MiniMax-M2.5-highspeed"

def test_langchain_minimax_base_url_correct(self):
from langchain_openai import ChatOpenAI
from pydantic import SecretStr

minimax = DEFAULT_CHAT_MODEL_PROVIDERS["minimax"]
model = ChatOpenAI(
model="MiniMax-M2.7",
api_key=SecretStr("test-key-123"),
base_url=minimax.base_url,
)
assert str(model.openai_api_base) == "https://api.minimax.io/v1"


# ============================================================
# Integration Tests: MiniMax API Connectivity
# ============================================================


@pytest.mark.integration
class TestMiniMaxIntegration:
"""Integration tests that verify MiniMax API connectivity.

Require MINIMAX_API_KEY env var and network access to api.minimax.io.
Run with: uv run python -m pytest test/test_minimax_provider.py -m integration -v
"""

@pytest.fixture(autouse=True)
def skip_without_api_key(self):
if not os.environ.get("MINIMAX_API_KEY"):
pytest.skip("MINIMAX_API_KEY not set")

@pytest.mark.asyncio
async def test_minimax_chat_completion(self):
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ["MINIMAX_API_KEY"], base_url="https://api.minimax.io/v1")
response = await client.chat.completions.create(
model="MiniMax-M2.7", messages=[{"role": "user", "content": "Say 1."}]
)
assert response.choices[0].message.content is not None

@pytest.mark.asyncio
async def test_minimax_streaming(self):
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ["MINIMAX_API_KEY"], base_url="https://api.minimax.io/v1")
chunks = []
response = await client.chat.completions.create(
model="MiniMax-M2.7", messages=[{"role": "user", "content": "Say hi."}], stream=True
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
chunks.append(chunk.choices[0].delta.content)
assert len(chunks) > 0

@pytest.mark.asyncio
async def test_minimax_highspeed_model(self):
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ["MINIMAX_API_KEY"], base_url="https://api.minimax.io/v1")
response = await client.chat.completions.create(
model="MiniMax-M2.7-highspeed", messages=[{"role": "user", "content": "Say 1."}]
)
assert response.choices[0].message.content is not None
1 change: 1 addition & 0 deletions web/src/assets/providers/minimax-color.svg
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find the correct icon from https://icons.lobehub.com/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pointer! Updated the icon to use the official MiniMax SVG from lobehub/lobe-icons (packages/static-svg/icons/minimax-color.svg). See commit 6763ae2.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions web/src/utils/modelIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import arkIcon from '@/assets/providers/doubao-color.svg'
import openrouterIcon from '@/assets/providers/openrouter.svg'
import defaultIcon from '@/assets/providers/default.png'
import modelscopeIcon from '@/assets/providers/modelscope-color.svg'
import minimaxIcon from '@/assets/providers/minimax-color.svg'

export const modelIcons = {
openai: openaiIcon,
Expand All @@ -20,5 +21,6 @@ export const modelIcons = {
together: togetherIcon,
openrouter: openrouterIcon,
modelscope: modelscopeIcon,
minimax: minimaxIcon,
default: defaultIcon // 添加默认图标
}