fix(llm): omit temperature for OpenAI GPT-6 family (#1101) (#1115)

* fix(llm): omit temperature for OpenAI GPT-6 family (#1101)

Official openai channel already skipped temperature for gpt-5* (#857);
GPT-6 Astra/Sol/Luna reject non-default temperature the same way. Extend
the omit rule and cover display-name / prefixed model ids in unit tests.

* test(llm): tighten GPT-6 API model coverage

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
This commit is contained in:
浩彬(^-^)andChris233 authored and GitHub committed 2026-09-28 01:28:26 +08:00
1 parent 820d7acf7a
commit 5b39599486
2 files changed
+80 -16

No files matched your search

@@ -16,7 +16,7 @@ use serde_json::{json, Value};
use crate::polish::{ use crate::polish::{
append_utf8_sse_chunk, apply_openai_compatible_thinking_control, chat_completions_url, append_utf8_sse_chunk, apply_openai_compatible_thinking_control, chat_completions_url,
extract_assistant_content, finish_utf8_sse_chunks, http_client_builder, extract_assistant_content, finish_utf8_sse_chunks, http_client_builder,
openai_model_is_gpt5_family, safe_str_slice, send_with_transient_retry, LLMError, openai_model_omits_custom_temperature, safe_str_slice, send_with_transient_retry, LLMError,
}; };
pub const OMNI_GEMINI_PROVIDER_ID: &str = "gemini"; pub const OMNI_GEMINI_PROVIDER_ID: &str = "gemini";
@@ -89,9 +89,9 @@ impl OpenAICompatibleOmni {
"messages": messages, "messages": messages,
}); });
if let Some(temperature) = self.config.temperature { if let Some(temperature) = self.config.temperature {
// OpenAI 官方 gpt-5 系列只接受默认 temperature=1(issue #857),同润色路径。 // OpenAI 官方 gpt-5 / gpt-6 只接受默认 temperature=1(#857 / #1101),同润色路径。
if !(self.config.provider_id.trim() == "openai" if !(self.config.provider_id.trim() == "openai"
&& openai_model_is_gpt5_family(&self.config.model)) && openai_model_omits_custom_temperature(&self.config.model))
{ {
body["temperature"] = json!(temperature); body["temperature"] = json!(temperature);
} }
@@ -553,6 +553,21 @@ mod tests {
assert!((body["temperature"].as_f64().unwrap() - 0.3).abs() < 1e-6); assert!((body["temperature"].as_f64().unwrap() - 0.3).abs() < 1e-6);
} }
#[test]
fn omni_body_omits_temperature_for_openai_gpt6_api_ids() {
for model in ["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"] {
let mut omni = config();
omni.model = model.into();
let provider = OpenAICompatibleOmni::new(omni);
let body = provider.omni_body(false, vec![json!({"role": "user", "content": "x"})]);
assert!(
body.get("temperature").is_none(),
"{model} must not receive temperature (issue #1101)"
);
}
}
#[test] #[test]
fn omni_gemini_routing_uses_provider_id_or_base_url() { fn omni_gemini_routing_uses_provider_id_or_base_url() {
assert!(!config().is_gemini()); assert!(!config().is_gemini());
@@ -798,11 +798,11 @@ impl OpenAICompatibleLLMProvider {
"messages": messages, "messages": messages,
}); });
if let Some(temperature) = self.config.temperature { if let Some(temperature) = self.config.temperature {
// OpenAI 官方 gpt-5 系列在 Chat Completions 只接受默认 temperature=1, // OpenAI 官方 gpt-5 / gpt-6 系列在 Chat Completions 只接受默认 temperature=1,
// 传 0.3 会被 400 拒绝(issue #857)。官方渠道的 gpt-5* 不下发该字段, // 传 0.3 会被 400 拒绝(issue #857 / #1101)。官方渠道的这些模型不下发该字段,
// 让服务端用默认值;其余模型保持原行为。 // 让服务端用默认值;其余模型保持原行为。
if !(self.config.provider_id.trim() == "openai" if !(self.config.provider_id.trim() == "openai"
&& openai_model_is_gpt5_family(&self.config.model)) && openai_model_omits_custom_temperature(&self.config.model))
{ {
body["temperature"] = temperature_json(temperature); body["temperature"] = temperature_json(temperature);
} }
@@ -1993,24 +1993,29 @@ pub(crate) fn openai_compatible_thinking_control_for_base_url(
None None
} }
/// OpenAI 官方 gpt-5 系列(gpt-5 / gpt-5-mini / gpt-5-nano / gpt-5.5 等)在 fn normalize_openai_model_id(model: &str) -> String {
/// Chat Completions 中只接受默认 temperature=1,传其它值会返回 400(issue #857)。
/// 模型名归一化规则与 `openai_chat_reasoning_effort` 保持一致。
pub(crate) fn openai_model_is_gpt5_family(model: &str) -> bool {
model model
.trim() .trim()
.strip_prefix("openai/") .strip_prefix("openai/")
.unwrap_or_else(|| model.trim()) .unwrap_or_else(|| model.trim())
.to_ascii_lowercase() .to_ascii_lowercase()
.starts_with("gpt-5") }
/// OpenAI 官方 gpt-5 系列(gpt-5 / gpt-5-mini / gpt-5-nano / gpt-5.5 等)。
/// 模型名归一化规则与 `openai_chat_reasoning_effort` 保持一致。
pub(crate) fn openai_model_is_gpt5_family(model: &str) -> bool {
normalize_openai_model_id(model).starts_with("gpt-5")
}
/// OpenAI 官方渠道下应省略自定义 `temperature` 的模型族。
/// gpt-5*(#857)与 gpt-6*(#1101,含 Astra/Sol/Luna)只接受服务端默认值。
/// API 模型 ID 如 `gpt-6-astra` 归一化后以 `gpt-6` 开头,一并覆盖。
pub(crate) fn openai_model_omits_custom_temperature(model: &str) -> bool {
openai_model_is_gpt5_family(model) || normalize_openai_model_id(model).starts_with("gpt-6")
} }
fn openai_chat_reasoning_effort(model: &str, thinking_enabled: bool) -> Option<&'static str> { fn openai_chat_reasoning_effort(model: &str, thinking_enabled: bool) -> Option<&'static str> {
let normalized = model let normalized = normalize_openai_model_id(model);
.trim()
.strip_prefix("openai/")
.unwrap_or_else(|| model.trim())
.to_ascii_lowercase();
if normalized.starts_with("gpt-5-pro") { if normalized.starts_with("gpt-5-pro") {
return Some("high"); return Some("high");
@@ -3394,6 +3399,32 @@ mod tests {
} }
} }
#[test]
fn chat_body_omits_temperature_for_openai_gpt6_api_ids() {
for model in [
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"openai/gpt-6-astra",
] {
let provider = OpenAICompatibleLLMProvider::new(OpenAICompatibleConfig::new(
"openai",
"OpenAI",
"https://api.openai.com/v1",
"k",
model,
));
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
assert_eq!(body["model"], model);
assert!(
body.get("temperature").is_none(),
"{model} must not receive temperature (issue #1101)"
);
}
}
#[test] #[test]
fn chat_body_keeps_default_temperature_for_openai_non_gpt5_models() { fn chat_body_keeps_default_temperature_for_openai_non_gpt5_models() {
for model in ["gpt-4o", "gpt-4o-mini", "gpt-4.1"] { for model in ["gpt-4o", "gpt-4o-mini", "gpt-4.1"] {
@@ -3411,6 +3442,24 @@ mod tests {
} }
} }
#[test]
fn chat_body_keeps_custom_temperature_for_gpt6_on_custom_provider() {
let provider = OpenAICompatibleLLMProvider::new(
OpenAICompatibleConfig::new(
"custom",
"Custom",
"https://api.openai.com/v1",
"k",
"gpt-6-astra",
)
.with_temperature(Some(1.0)),
);
let body = provider.chat_body(false, vec![json!({ "role": "user", "content": "hi" })]);
assert_eq!(body["temperature"], json!(1.0));
}
#[test] #[test]
fn chat_body_keeps_custom_temperature_for_gpt5_on_custom_provider() { fn chat_body_keeps_custom_temperature_for_gpt5_on_custom_provider() {
// custom 预设由用户显式配温度(issue #857 的绕过路径:custom + temperature=1), // custom 预设由用户显式配温度(issue #857 的绕过路径:custom + temperature=1),