diff --git a/backend/core/config.py b/backend/core/config.py index a24f3f05..3d8a3a67 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -420,6 +420,7 @@ def get_default_llm_model_for_provider(provider: Optional[str]) -> str: - 百炼(aliyun):默认 deepseek-v3.2(兼容模式) - DeepSeek:默认 deepseek-chat - Moonshot:默认 moonshot-v1-8k + - MiniMax: MiniMax-M3 by default - 其他/未知:回退到 DEFAULT_LLM_MODEL """ p = (provider or "").strip().lower() or DEFAULT_LLM_PROVIDER @@ -429,4 +430,6 @@ def get_default_llm_model_for_provider(provider: Optional[str]) -> str: return "deepseek-chat" if p == "moonshot": return "moonshot-v1-8k" - return DEFAULT_LLM_MODEL \ No newline at end of file + if p == "minimax": + return "MiniMax-M3" + return DEFAULT_LLM_MODEL diff --git a/backend/core/content.py b/backend/core/content.py index 4ed8e541..f8d1a607 100644 --- a/backend/core/content.py +++ b/backend/core/content.py @@ -86,6 +86,13 @@ def _extract_llm_base_url(ctx) -> str | None: "kimi-k2-turbo-preview": {"name": "Kimi K2 Turbo", "max_tokens": 1024}, }, }, + "minimax": { + "base_url": "https://api.minimax.io/v1", + "models": { + "MiniMax-M3": {"name": "MiniMax-M3", "max_tokens": 1024}, + "MiniMax-M2.7": {"name": "MiniMax-M2.7", "max_tokens": 1024}, + }, + }, # Custom OpenAI-compatible provider: base_url must be provided at call-time. "openai_compat": { "base_url": "", @@ -292,6 +299,7 @@ def _get_client( "deepseek": "DEEPSEEK_API_KEY", "aliyun": "DASHSCOPE_API_KEY", "moonshot": "MOONSHOT_API_KEY", + "minimax": "MINIMAX_API_KEY", } env_key = api_key_map.get(provider, "DEEPSEEK_API_KEY") api_key = os.getenv(env_key, "") diff --git a/backend/core/schemas.py b/backend/core/schemas.py index f5ce5feb..23880ab2 100644 --- a/backend/core/schemas.py +++ b/backend/core/schemas.py @@ -14,7 +14,7 @@ _MAC_RE = re.compile(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$") # 允许的 LLM 提供商 -_VALID_PROVIDERS = {"deepseek", "aliyun", "moonshot"} +_VALID_PROVIDERS = {"deepseek", "aliyun", "moonshot", "minimax"} _VALID_IMAGE_PROVIDERS = {"aliyun"} # 允许的语言选项 diff --git a/backend/tests/test_custom_mode_api_key_simple.py b/backend/tests/test_custom_mode_api_key_simple.py index b88ad873..bb807170 100644 --- a/backend/tests/test_custom_mode_api_key_simple.py +++ b/backend/tests/test_custom_mode_api_key_simple.py @@ -41,6 +41,23 @@ def test_get_client_uses_env_var_when_api_key_is_none(self): assert client is not None assert max_tokens > 0 + def test_get_client_uses_minimax_defaults(self): + """Test that MiniMax uses its preset base URL and environment key.""" + env_api_key = "minimax-demo" + + with ( + patch.dict(os.environ, {"MINIMAX_API_KEY": env_api_key}, clear=False), + patch("core.content.AsyncOpenAI") as mock_openai, + ): + mock_openai.return_value = MagicMock() + client, max_tokens = _get_client("minimax", "MiniMax-M3", api_key=None) + + assert client is mock_openai.return_value + assert max_tokens > 0 + assert mock_openai.call_args is not None + assert mock_openai.call_args.kwargs.get("api_key") == env_api_key + assert mock_openai.call_args.kwargs.get("base_url") == "https://api.minimax.io/v1" + def test_get_client_raises_error_when_user_key_empty(self): """测试 _get_client 在用户配置的 api_key 为空时抛出错误""" with ( @@ -178,4 +195,3 @@ def test_none_key_flow(self): with patch.dict(os.environ, {"DEEPSEEK_API_KEY": env_api_key}, clear=False): client, max_tokens = _get_client("deepseek", "deepseek-chat", api_key=device_api_key) assert client is not None - diff --git a/backend/tests/test_unit_config_store.py b/backend/tests/test_unit_config_store.py index 816a6124..a4f54926 100644 --- a/backend/tests/test_unit_config_store.py +++ b/backend/tests/test_unit_config_store.py @@ -15,6 +15,7 @@ get_or_create_claim_token, remove_mode_from_all_configs, ) +from core.config import get_default_llm_model_for_provider @pytest.fixture(autouse=True) @@ -202,3 +203,6 @@ async def test_remove_mode_from_all_configs_cleans_modes_and_overrides(self): assert "STOIC" in config["modes"] assert "CUSTOM_DELETED" not in config["mode_overrides"] assert config["mode_overrides"]["STOIC"]["city"] == "杭州" + + def test_get_default_llm_model_for_provider_minimax(self): + assert get_default_llm_model_for_provider("minimax") == "MiniMax-M3" diff --git a/backend/tests/test_unit_schemas.py b/backend/tests/test_unit_schemas.py index 6221e4e7..b94ba7e6 100644 --- a/backend/tests/test_unit_schemas.py +++ b/backend/tests/test_unit_schemas.py @@ -72,6 +72,10 @@ def test_invalid_provider(self): with pytest.raises(ValidationError, match="无效 LLM 提供商"): ConfigRequest(mac="AA:BB:CC:DD:EE:FF", llmProvider="openai") + def test_minimax_provider_accepted(self): + body = ConfigRequest(mac="AA:BB:CC:DD:EE:FF", llmProvider="minimax") + assert body.llmProvider == "minimax" + def test_invalid_strategy(self): with pytest.raises(ValidationError, match="无效刷新策略"): ConfigRequest(mac="AA:BB:CC:DD:EE:FF", refreshStrategy="sequential") diff --git a/webapp/app/[locale]/profile/page.tsx b/webapp/app/[locale]/profile/page.tsx index b9141af2..833253ba 100644 --- a/webapp/app/[locale]/profile/page.tsx +++ b/webapp/app/[locale]/profile/page.tsx @@ -260,6 +260,9 @@ export default function ProfilePage() { if (provider === "aliyun") { return ["qwen3.5-flash", "deepseek-v3.2", "Moonshot-Kimi-K2-Instruct"]; } + if (provider === "minimax") { + return ["MiniMax-M3", "MiniMax-M2.7"]; + } // 默认 DeepSeek return ["deepseek-chat", "deepseek-reasoner"]; }; @@ -630,6 +633,7 @@ export default function ProfilePage() { > + @@ -638,18 +642,11 @@ export default function ProfilePage() { onChange={(e) => setLlmModel(e.target.value)} className="w-full rounded-sm border border-ink/20 px-3 py-2 text-sm bg-white" > - {llmProvider === "aliyun" ? ( - <> - - - - - ) : ( - <> - - - - )} + {getLlmModelOptions(llmProvider).map((model) => ( + + ))} @@ -714,6 +711,7 @@ export default function ProfilePage() { onChange={(e) => setLlmBaseUrl(e.target.value)} placeholder={tr("例如:https://api.deepseek.com/v1", "e.g. https://api.deepseek.com/v1")} className="w-full rounded-sm border border-ink/20 px-3 py-2 text-sm bg-white font-mono" + list="llm-base-url-presets" /> @@ -723,6 +721,7 @@ export default function ProfilePage() { onChange={(e) => setLlmModel(e.target.value)} placeholder={tr("例如:deepseek-chat", "e.g. deepseek-chat")} className="w-full rounded-sm border border-ink/20 px-3 py-2 text-sm bg-white font-mono" + list="llm-model-presets" /> @@ -735,6 +734,14 @@ export default function ProfilePage() { autoComplete="off" /> + + + + {/* ── 图像生成 ── */}