fix(llm): gate OpenAI reasoning effort by model

This commit is contained in:
H-Chris233 committed 2026-05-29 22:28:41 +08:00
1 parent f517912068
commit 84e2a7ba1d
3 files changed
+83 -11

No files matched your search

+81 -9
View File
@@ -35,7 +35,7 @@ pub struct OpenAICompatibleConfig {
pub request_timeout_secs: u64,
/// true = 让支持的 OpenAI-compatible provider 启用推理 / 思考;
/// false = 按渠道级官方参数关闭或压低思考。不做模型白名单判断,
/// 具体模型兼容性交给 provider 处理。
/// 但 OpenAI 官方渠道会跳过已知不支持 reasoning_effort 的普通 chat 模型。
pub thinking_enabled: bool,
}
@@ -1509,13 +1509,20 @@ fn unix_now_secs() -> u64 {
fn apply_openai_compatible_thinking_control(body: &mut Value, config: &OpenAICompatibleConfig) {
match openai_compatible_thinking_control(&config.provider_id) {
Some(ThinkingControl::ReasoningEffort) => {
// OpenAI Chat Completions 的 reasoning_effort 是渠道级请求字段。
// 关闭时统一压到 low,避免引入模型白名单;不支持该字段的模型由 provider 自行处理。
body["reasoning_effort"] = json!(if config.thinking_enabled {
"medium"
// OpenAI 官方 Chat Completions 只在推理模型族接受 reasoning_effort;
// 普通 chat 模型会直接 400。其它兼容渠道按渠道声明继续下发。
let effort = if config.provider_id.trim() == "openai" {
openai_chat_reasoning_effort(&config.model, config.thinking_enabled)
} else {
"low"
});
Some(if config.thinking_enabled {
"medium"
} else {
"low"
})
};
if let Some(effort) = effort {
body["reasoning_effort"] = json!(effort);
}
}
Some(ThinkingControl::EnableThinking) => {
body["enable_thinking"] = json!(config.thinking_enabled);
@@ -1554,6 +1561,28 @@ fn openai_compatible_thinking_control(provider_id: &str) -> Option<ThinkingContr
}
}
fn openai_chat_reasoning_effort(model: &str, thinking_enabled: bool) -> Option<&'static str> {
let normalized = model
.trim()
.strip_prefix("openai/")
.unwrap_or_else(|| model.trim())
.to_ascii_lowercase();
if normalized.starts_with("gpt-5-pro") {
return Some("high");
}
if normalized.starts_with("o1")
|| normalized.starts_with("o3")
|| normalized.starts_with("o4")
|| normalized.starts_with("gpt-5")
{
Some(if thinking_enabled { "medium" } else { "low" })
} else {
None
}
}
/// 把 working_languages + front_app 拼成 system prompt 头部前提:
/// # 上下文
/// 用户的工作语言:…
@@ -2656,14 +2685,14 @@ mod tests {
}
#[test]
fn openai_chat_body_adds_reasoning_effort_for_openai_channel() {
fn openai_chat_body_adds_reasoning_effort_for_openai_reasoning_model() {
let provider = OpenAICompatibleLLMProvider::new(
OpenAICompatibleConfig::new(
"openai",
"OpenAI",
"https://api.openai.com/v1",
"k",
"any-model",
"gpt-5-mini",
)
.with_thinking_enabled(true),
);
@@ -2673,6 +2702,49 @@ mod tests {
assert_eq!(body["reasoning_effort"], "medium");
}
#[test]
fn openai_chat_body_omits_reasoning_effort_for_non_reasoning_chat_models() {
for model in ["gpt-4o-mini", "gpt-4o", "gpt-4.1-nano"] {
let provider = OpenAICompatibleLLMProvider::new(
OpenAICompatibleConfig::new(
"openai",
"OpenAI",
"https://api.openai.com/v1",
"k",
model,
)
.with_thinking_enabled(true),
);
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
assert!(
body.get("reasoning_effort").is_none(),
"{model} must not receive reasoning_effort"
);
}
}
#[test]
fn openai_chat_body_uses_high_reasoning_effort_for_gpt_5_pro() {
for thinking_enabled in [false, true] {
let provider = OpenAICompatibleLLMProvider::new(
OpenAICompatibleConfig::new(
"openai",
"OpenAI",
"https://api.openai.com/v1",
"k",
"gpt-5-pro",
)
.with_thinking_enabled(thinking_enabled),
);
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
assert_eq!(body["reasoning_effort"], "high");
}
}
#[test]
fn openai_chat_body_lowers_reasoning_when_disabled_for_channel() {
let provider = OpenAICompatibleLLMProvider::new(OpenAICompatibleConfig::new(
+1 -1
View File
@@ -532,7 +532,7 @@ pub struct UserPreferences {
pub active_llm_provider: String, // "ark" | "openai" | ...
/// LLM 思考模式开关。默认 false 以保持既有「尽量关闭思考」行为;
/// Gemini 走原生 thinkingConfig,OpenAI-compatible 路径仅按 provider/channel
/// 下发官方渠道级字段,不用 prompt 注入,也不做模型白名单适配。详见 issue #402。
/// 下发官方渠道级字段;OpenAI 官方渠道会跳过普通 chat 模型不支持的字段。详见 issue #402。
#[serde(default)]
pub llm_thinking_enabled: bool,
/// Windows/Linux 粘贴成功后是否恢复用户原剪贴板。默认 true 跟历史行为一致;
+1 -1
View File
@@ -222,7 +222,7 @@ export interface UserPreferences {
microphoneDeviceName: string;
activeAsrProvider: string;
activeLlmProvider: string;
/** LLM 思考模式开关。默认关闭,保持既有尽量关闭思考的行为。详见 issue #402。 */
/** LLM 思考模式开关。默认关闭;OpenAI 普通 chat 模型会跳过不支持的字段。详见 issue #402。 */
llmThinkingEnabled: boolean;
/** 仅 Windows/Linux:粘贴成功后是否恢复用户原剪贴板。默认 true。详见 issue #111。 */
restoreClipboardAfterPaste: boolean;