fix(llm): 修复 MiniMax thinking 关闭无效
**Bug 复现** 用户在 OpenLess 设置里把「思考」关闭后,MiniMax(M3)润色仍输出 thinking 内容块,UI 关闭形同虚设。 **根因** polish.rs::openai_compatible_thinking_control 只在 5 个 provider_id (deepseek / openrouterFree / alibabaCoding / openai / codingPlanX) 上分派 thinking 控制参数;MiniMax 不在列表里,走默认 _ => None 分支,后端根本 不下发 thinking 字段。M3 默认开启 thinking,所以 UI 关闭后服务端仍 输出 <think>…</think> 块。 **修复** - 新增 ThinkingControl::MiniMaxThinking 变体:关闭下发 `thinking.type = "disabled"`、开启下发 `thinking.type = "adaptive"`, 与 minimaxi 官方 Chat Completions schema 完全对齐。 - openai_compatible_thinking_control 增加 "minimax" provider_id 识别。 - 同时增加 base_url 兜底识别(host 含 "minimax" 关键字即命中),让用户 用「自定义」preset 接入 MiniMax 时也能正确下发 thinking 控制参数。 - 前端 ProvidersSection.tsx::LLM_PRESETS 新增 minimax preset (baseUrl=https://api.minimaxi.com/v1, model=MiniMax-M3)。 - 5 个 i18n 文件 (en/zh-CN/zh-TW/ja/ko) 加 minimax 文案。 - 加 4 个 Rust 单元测试覆盖:preset 命中、adaptive 字面量、custom base_url 兜底、host 提取对尾斜杠与路径的容错。 **兼容性** - 旧 provider (deepseek/openai/codingPlanX/alibabaCoding/openrouterFree) 行为完全不变,新增 case 仅在 provider_id 命中 minimax 或 base_url 含 minimaxi 主机时启用。 - 行为对齐 minimaxi 官方文档: https://platform.minimaxi.com/docs/api-reference/text-chat-openai#thinking-控制 **测试** cargo test --lib polish:: 41 passed, 0 failed 新增 4 个 case: - openai_chat_body_disables_minimax_thinking_by_preset - openai_chat_body_enables_minimax_thinking_with_adaptive_literal - openai_chat_body_falls_back_to_base_url_for_custom_minimax_endpoint - openai_chat_body_base_url_fallback_respects_trailing_slash_and_path
This commit is contained in:
1 parent
5ca3a80cd6
commit
063cd36a32
7 files changed
+153
-1
No files matched your search
@@ -1508,7 +1508,11 @@ fn unix_now_secs() -> u64 {
|
||||
}
|
||||
|
||||
fn apply_openai_compatible_thinking_control(body: &mut Value, config: &OpenAICompatibleConfig) {
|
||||
match openai_compatible_thinking_control(&config.provider_id) {
|
||||
// 优先按 provider_id 预设分派;custom / 未声明 provider 时回退到 base_url 兜底,
|
||||
// 让用户用"自定义"preset 接入 MiniMax 也能正确下发 thinking 控制参数。
|
||||
let control = openai_compatible_thinking_control(&config.provider_id)
|
||||
.or_else(|| openai_compatible_thinking_control_for_base_url(&config.base_url));
|
||||
match control {
|
||||
Some(ThinkingControl::ReasoningEffort) => {
|
||||
// OpenAI 官方 Chat Completions 只在推理模型族接受 reasoning_effort;
|
||||
// 普通 chat 模型会直接 400。其它兼容渠道按渠道声明继续下发。
|
||||
@@ -1540,6 +1544,17 @@ fn apply_openai_compatible_thinking_control(body: &mut Value, config: &OpenAICom
|
||||
"type": if config.thinking_enabled { "enabled" } else { "disabled" },
|
||||
});
|
||||
}
|
||||
// MiniMax OpenAI 兼容 Chat Completions 接受官方 `thinking` 字段,关闭用
|
||||
// `disabled`、开启用 `adaptive`(不传即默认开启,这里显式发 `adaptive` 与
|
||||
// 渠道文档保持一致)。schema 与 DeepSeekThinking 相同,仅取值字面量不同——
|
||||
// 走独立变体避免 OpenLess 默认值(DeepSeek 写"enabled")污染 MiniMax 字段。
|
||||
// 注:M2.x 系列不支持关闭,后端即便下发 `disabled` 服务端仍会保持开启;
|
||||
// 这与 OpenLess 渠道级"按官方参数声明下发"的策略一致,不维护单模型白名单。
|
||||
Some(ThinkingControl::MiniMaxThinking) => {
|
||||
body["thinking"] = json!({
|
||||
"type": if config.thinking_enabled { "adaptive" } else { "disabled" },
|
||||
});
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
@@ -1550,18 +1565,55 @@ enum ThinkingControl {
|
||||
EnableThinking,
|
||||
OpenRouterReasoning,
|
||||
DeepSeekThinking,
|
||||
MiniMaxThinking,
|
||||
}
|
||||
|
||||
fn openai_compatible_thinking_control(provider_id: &str) -> Option<ThinkingControl> {
|
||||
match provider_id.trim() {
|
||||
"deepseek" => Some(ThinkingControl::DeepSeekThinking),
|
||||
// provider_id 预设(见 ProvidersSection.tsx::LLM_PRESETS)。
|
||||
"minimax" => Some(ThinkingControl::MiniMaxThinking),
|
||||
"openrouterFree" => Some(ThinkingControl::OpenRouterReasoning),
|
||||
"alibabaCoding" => Some(ThinkingControl::EnableThinking),
|
||||
"openai" | "codingPlanX" => Some(ThinkingControl::ReasoningEffort),
|
||||
// custom / 其他未声明 provider 走 base_url 兜底识别——用户用自定义
|
||||
// endpoint 接入 MiniMax 时,根据 base_url 命中即下发官方 thinking 参数。
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 当 provider_id 不在已知列表(典型场景:用户用"自定义"preset 接入)时,
|
||||
/// 通过 base_url 推断该走哪种 thinking 控制策略。返回 `None` 表示无法
|
||||
/// 识别,沿用原"不主动干预"行为。
|
||||
///
|
||||
/// 命中策略:base_url 主机名包含厂商关键字。
|
||||
fn openai_compatible_thinking_control_for_base_url(base_url: &str) -> Option<ThinkingControl> {
|
||||
// 抽 host(不区分大小写),允许带端口。`base_url` 末尾可能带 `/v1`、`/v1/`、
|
||||
// 甚至 `/v1/chat/completions`——统一取第一个 `/` 段当 host。
|
||||
let host = base_url
|
||||
.trim()
|
||||
.trim_end_matches('/')
|
||||
.split_once("://")
|
||||
.map(|(_, rest)| rest.split('/').next().unwrap_or(rest).to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
if host.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if host.contains("minimax") {
|
||||
return Some(ThinkingControl::MiniMaxThinking);
|
||||
}
|
||||
if host.contains("deepseek") {
|
||||
return Some(ThinkingControl::DeepSeekThinking);
|
||||
}
|
||||
if host.contains("openrouter") {
|
||||
return Some(ThinkingControl::OpenRouterReasoning);
|
||||
}
|
||||
if host.contains("dashscope") || host.contains("aliyuncs") {
|
||||
return Some(ThinkingControl::EnableThinking);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn openai_chat_reasoning_effort(model: &str, thinking_enabled: bool) -> Option<&'static str> {
|
||||
let normalized = model
|
||||
.trim()
|
||||
@@ -2826,6 +2878,89 @@ mod tests {
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_body_disables_minimax_thinking_by_preset() {
|
||||
// provider_id 预设命中 "minimax" → 走 MiniMaxThinking 分支,关闭时下发
|
||||
// `thinking.type = "disabled"`,与 minimaxi 官方 Chat Completions 文档
|
||||
// (https://platform.minimaxi.com/docs/api-reference/text-chat-openai#thinking-控制) 一致。
|
||||
// 修这个 bug 前,provider_id 未命中时根本不下发 thinking 参数,UI 关闭无效。
|
||||
let provider = OpenAICompatibleLLMProvider::new(
|
||||
OpenAICompatibleConfig::new(
|
||||
"minimax",
|
||||
"MiniMax",
|
||||
"https://api.minimaxi.com/v1",
|
||||
"k",
|
||||
"MiniMax-M3",
|
||||
)
|
||||
.with_thinking_enabled(false),
|
||||
);
|
||||
|
||||
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_body_enables_minimax_thinking_with_adaptive_literal() {
|
||||
// MiniMax 开启 thinking 必须用 `"adaptive"`,不是 DeepSeek 的 `"enabled"`。
|
||||
// 若错发 `"enabled"`,M3 会落到未声明的 type 并报参数错误,反而失去思考。
|
||||
let provider = OpenAICompatibleLLMProvider::new(
|
||||
OpenAICompatibleConfig::new(
|
||||
"minimax",
|
||||
"MiniMax",
|
||||
"https://api.minimaxi.com/v1",
|
||||
"k",
|
||||
"MiniMax-M3",
|
||||
)
|
||||
.with_thinking_enabled(true),
|
||||
);
|
||||
|
||||
let body = provider.chat_body(true, vec![json!({ "role": "user", "content": "hi" })]);
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_body_falls_back_to_base_url_for_custom_minimax_endpoint() {
|
||||
// 用 "custom" preset + 自定义 MiniMax base_url 接入时,base_url 兜底
|
||||
// 识别需要命中"minimax"关键字,下发 thinking 控制参数。
|
||||
let provider = OpenAICompatibleLLMProvider::new(
|
||||
OpenAICompatibleConfig::new(
|
||||
"custom",
|
||||
"Custom",
|
||||
"https://api.minimaxi.com/v1",
|
||||
"k",
|
||||
"MiniMax-M3",
|
||||
)
|
||||
.with_thinking_enabled(false),
|
||||
);
|
||||
|
||||
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_body_base_url_fallback_respects_trailing_slash_and_path() {
|
||||
// base_url 可能带尾斜杠或带 /v1 后缀,host 提取逻辑都要能正确识别。
|
||||
for base_url in [
|
||||
"https://api.minimaxi.com/v1",
|
||||
"https://api.minimaxi.com/v1/",
|
||||
"https://api.minimaxi.com",
|
||||
"https://api.minimaxi.com/",
|
||||
] {
|
||||
let provider = OpenAICompatibleLLMProvider::new(
|
||||
OpenAICompatibleConfig::new("custom", "Custom", base_url, "k", "MiniMax-M3")
|
||||
.with_thinking_enabled(false),
|
||||
);
|
||||
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
|
||||
assert_eq!(
|
||||
body["thinking"]["type"], "disabled",
|
||||
"base_url={base_url} should trigger MiniMax thinking control"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_chat_body_omits_thinking_control_for_unknown_provider() {
|
||||
let provider = OpenAICompatibleLLMProvider::new(
|
||||
|
||||
@@ -611,6 +611,7 @@ export const en: typeof zhCN = {
|
||||
openrouterFree: 'OpenRouter (free models)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
minimax: 'MiniMax (M3)',
|
||||
custom: 'Custom',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrBailian: 'Alibaba Bailian realtime ASR',
|
||||
|
||||
@@ -613,6 +613,7 @@ export const ja: typeof zhCN = {
|
||||
openrouterFree: 'OpenRouter(無料モデル)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
minimax: 'MiniMax(M3)',
|
||||
custom: 'カスタム',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrBailian: 'Alibaba Bailian リアルタイム ASR',
|
||||
|
||||
@@ -613,6 +613,7 @@ export const ko: typeof zhCN = {
|
||||
openrouterFree: 'OpenRouter(무료 모델)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
minimax: 'MiniMax (M3)',
|
||||
custom: '사용자 정의',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrBailian: 'Alibaba Bailian 실시간 ASR',
|
||||
|
||||
@@ -609,6 +609,7 @@ export const zhCN = {
|
||||
openrouterFree: 'OpenRouter(免费模型)',
|
||||
alibabaCoding: '阿里云 Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
minimax: 'MiniMax(M3)',
|
||||
custom: '自定义',
|
||||
asrVolcengine: '火山引擎 bigasr',
|
||||
asrBailian: '阿里云百炼实时 ASR',
|
||||
|
||||
@@ -611,6 +611,7 @@ export const zhTW: typeof zhCN = {
|
||||
openrouterFree: 'OpenRouter(免費模型)',
|
||||
alibabaCoding: '阿里雲 Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
minimax: 'MiniMax(M3)',
|
||||
custom: '自定義',
|
||||
asrVolcengine: '火山引擎 bigasr',
|
||||
asrBailian: '阿里雲百煉即時 ASR',
|
||||
|
||||
@@ -116,6 +116,18 @@ const LLM_PRESETS = [
|
||||
baseUrl: 'https://api.codingplanx.ai/v1',
|
||||
modelPlaceholder: 'gpt-5-mini',
|
||||
},
|
||||
{
|
||||
// MiniMax 国内开放平台(minimaxi.com),OpenAI 兼容 /v1/chat/completions。
|
||||
// M3 默认开启 thinking,可通过 `thinking.type = disabled` 关闭。
|
||||
// provider_id 在后端 polish.rs::openai_compatible_thinking_control 命中
|
||||
// "minimax" → MiniMaxThinking 分支,关闭时下发 disabled、开启时发 adaptive。
|
||||
// 走"自定义"preset 接入时由 base_url 含 "minimax" 兜底识别,见 polish.rs。
|
||||
// 文档: https://platform.minimaxi.com/docs/api-reference/text-chat-openai#thinking-控制
|
||||
id: 'minimax',
|
||||
nameKey: 'minimax',
|
||||
baseUrl: 'https://api.minimaxi.com/v1',
|
||||
modelPlaceholder: 'MiniMax-M3',
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
nameKey: 'custom',
|
||||
|
||||
Reference in new issue
Block a user