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
5 changes: 4 additions & 1 deletion backend/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
if p == "minimax":
return "MiniMax-M3"
return DEFAULT_LLM_MODEL
8 changes: 8 additions & 0 deletions backend/core/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down Expand Up @@ -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, "")
Expand Down
2 changes: 1 addition & 1 deletion backend/core/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}

# 允许的语言选项
Expand Down
18 changes: 17 additions & 1 deletion backend/tests/test_custom_mode_api_key_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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

4 changes: 4 additions & 0 deletions backend/tests/test_unit_config_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
4 changes: 4 additions & 0 deletions backend/tests/test_unit_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
31 changes: 19 additions & 12 deletions webapp/app/[locale]/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
};
Expand Down Expand Up @@ -630,6 +633,7 @@ export default function ProfilePage() {
>
<option value="aliyun">{tr("阿里百炼", "Alibaba Bailian")}</option>
<option value="deepseek">DeepSeek</option>
<option value="minimax">MiniMax</option>
</select>
</Field>
<Field label={tr("模型名称", "Model Name")}>
Expand All @@ -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" ? (
<>
<option value="qwen3.5-flash">qwen3.5-flash</option>
<option value="deepseek-v3.2">deepseek-v3.2</option>
<option value="Moonshot-Kimi-K2-Instruct">Moonshot-Kimi-K2-Instruct</option>
</>
) : (
<>
<option value="deepseek-chat">deepseek-chat</option>
<option value="deepseek-reasoner">deepseek-reasoner</option>
</>
)}
{getLlmModelOptions(llmProvider).map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
</Field>
<Field label={tr("API Key", "API Key")}>
Expand Down Expand Up @@ -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"
/>
</Field>
<Field label={tr("模型名称", "Model Name")}>
Expand All @@ -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"
/>
</Field>
<Field label={tr("API Key", "API Key")}>
Expand All @@ -735,6 +734,14 @@ export default function ProfilePage() {
autoComplete="off"
/>
</Field>
<datalist id="llm-base-url-presets">
<option value="https://api.minimax.io/v1" />
<option value="https://api.minimaxi.com/v1" />
</datalist>
<datalist id="llm-model-presets">
<option value="MiniMax-M3" />
<option value="MiniMax-M2.7" />
</datalist>

{/* ── 图像生成 ── */}
<div className="pt-4 border-t border-ink/10">
Expand Down