Merge branch 'main' into feat/sandbox-provisioner-shared-storage
This commit is contained in:
commit
4cc795da9c
@ -49,6 +49,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
|
||||
|
||||
|
||||
@ -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` | 通义千问系列 |
|
||||
|
||||
@ -231,13 +231,14 @@ async def test_mcp_server(
|
||||
await get_server_or_404(db, name)
|
||||
|
||||
try:
|
||||
tools = await get_all_mcp_tools(name)
|
||||
tools = await get_all_mcp_tools(name, raise_on_error=True)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"连接成功,共发现 {len(tools)} 个工具",
|
||||
"tool_count": len(tools),
|
||||
}
|
||||
except Exception as test_error:
|
||||
logger.warning(f"MCP server test failed for '{name}': {test_error}")
|
||||
raise HTTPException(status_code=500, detail=f"连接失败: {str(test_error)}")
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@ -115,6 +115,19 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
|
||||
"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",
|
||||
|
||||
@ -210,6 +210,7 @@ async def get_mcp_tools(
|
||||
disabled_tools: list[str] = None,
|
||||
cache: bool = True,
|
||||
force_refresh: bool = False,
|
||||
raise_on_error: bool = False,
|
||||
) -> list[Callable[..., Any]]:
|
||||
"""Get MCP tools for a specific server.
|
||||
|
||||
@ -288,11 +289,15 @@ async def get_mcp_tools(
|
||||
|
||||
except AssertionError as e:
|
||||
logger.warning(f"[assert] Failed to load tools from MCP server '{server_name}': {e}")
|
||||
if raise_on_error:
|
||||
raise
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to load tools from MCP server '{server_name}': {e}, traceback: {traceback.format_exc()}"
|
||||
)
|
||||
if raise_on_error:
|
||||
raise
|
||||
return []
|
||||
|
||||
# 3. Filtering (Apply to Return Value Only)
|
||||
@ -602,7 +607,7 @@ async def get_servers_config(names: list[str]) -> dict[str, dict[str, Any]]:
|
||||
return {name: MCP_SERVERS[name] for name in names if name in MCP_SERVERS}
|
||||
|
||||
|
||||
async def get_all_mcp_tools(server_name: str) -> list:
|
||||
async def get_all_mcp_tools(server_name: str, raise_on_error: bool = False) -> list:
|
||||
"""Get all tools of an MCP server (no filtering).
|
||||
|
||||
For management UI to display tool list, supports viewing all tools and their enabled status.
|
||||
@ -610,6 +615,7 @@ async def get_all_mcp_tools(server_name: str) -> list:
|
||||
|
||||
Args:
|
||||
server_name: Server name
|
||||
raise_on_error: Whether to raise an exception on error instead of returning empty list
|
||||
|
||||
Returns:
|
||||
List of all tools (unfiltered)
|
||||
@ -617,7 +623,15 @@ async def get_all_mcp_tools(server_name: str) -> list:
|
||||
config = MCP_SERVERS.get(server_name)
|
||||
if not config:
|
||||
logger.warning(f"MCP server '{server_name}' not found in cache")
|
||||
if raise_on_error:
|
||||
raise ValueError(f"MCP server '{server_name}' not found in cache")
|
||||
return []
|
||||
|
||||
# Get all tools (no filtering, force refresh, no cache update)
|
||||
return await get_mcp_tools(server_name, disabled_tools=[], cache=False, force_refresh=True)
|
||||
return await get_mcp_tools(
|
||||
server_name,
|
||||
disabled_tools=[],
|
||||
cache=False,
|
||||
force_refresh=True,
|
||||
raise_on_error=raise_on_error,
|
||||
)
|
||||
|
||||
278
test/test_minimax_provider.py
Normal file
278
test/test_minimax_provider.py
Normal file
@ -0,0 +1,278 @@
|
||||
"""
|
||||
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
|
||||
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."""
|
||||
|
||||
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
web/src/assets/providers/minimax-color.svg
Normal file
1
web/src/assets/providers/minimax-color.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-fill" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-fill)" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -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,
|
||||
@ -20,5 +21,6 @@ export const modelIcons = {
|
||||
together: togetherIcon,
|
||||
openrouter: openrouterIcon,
|
||||
modelscope: modelscopeIcon,
|
||||
minimax: minimaxIcon,
|
||||
default: defaultIcon // 添加默认图标
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user