Merge pull request #555 from H-Chris233/fix/issue-542-openai-reasoning-effort
fix(llm): gate OpenAI reasoning effort by model
This commit is contained in:
3 files changed
+83
-11
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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 跟历史行为一致;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user