diff --git a/openless-all/app/crates/openless-core/src/omni.rs b/openless-all/app/crates/openless-core/src/omni.rs index 171daff84..2a5e944d0 100644 --- a/openless-all/app/crates/openless-core/src/omni.rs +++ b/openless-all/app/crates/openless-core/src/omni.rs @@ -16,7 +16,7 @@ use serde_json::{json, Value}; use crate::polish::{ append_utf8_sse_chunk, apply_openai_compatible_thinking_control, chat_completions_url, 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"; @@ -89,9 +89,9 @@ impl OpenAICompatibleOmni { "messages": messages, }); 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" - && openai_model_is_gpt5_family(&self.config.model)) + && openai_model_omits_custom_temperature(&self.config.model)) { body["temperature"] = json!(temperature); } @@ -553,6 +553,21 @@ mod tests { 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] fn omni_gemini_routing_uses_provider_id_or_base_url() { assert!(!config().is_gemini()); diff --git a/openless-all/app/crates/openless-core/src/polish.rs b/openless-all/app/crates/openless-core/src/polish.rs index 7b8e37198..75d572a7c 100644 --- a/openless-all/app/crates/openless-core/src/polish.rs +++ b/openless-all/app/crates/openless-core/src/polish.rs @@ -798,11 +798,11 @@ impl OpenAICompatibleLLMProvider { "messages": messages, }); if let Some(temperature) = self.config.temperature { - // OpenAI 官方 gpt-5 系列在 Chat Completions 只接受默认 temperature=1, - // 传 0.3 会被 400 拒绝(issue #857)。官方渠道的 gpt-5* 不下发该字段, + // OpenAI 官方 gpt-5 / gpt-6 系列在 Chat Completions 只接受默认 temperature=1, + // 传 0.3 会被 400 拒绝(issue #857 / #1101)。官方渠道的这些模型不下发该字段, // 让服务端用默认值;其余模型保持原行为。 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); } @@ -1993,24 +1993,29 @@ pub(crate) fn openai_compatible_thinking_control_for_base_url( None } -/// OpenAI 官方 gpt-5 系列(gpt-5 / gpt-5-mini / gpt-5-nano / gpt-5.5 等)在 -/// Chat Completions 中只接受默认 temperature=1,传其它值会返回 400(issue #857)。 -/// 模型名归一化规则与 `openai_chat_reasoning_effort` 保持一致。 -pub(crate) fn openai_model_is_gpt5_family(model: &str) -> bool { +fn normalize_openai_model_id(model: &str) -> String { model .trim() .strip_prefix("openai/") .unwrap_or_else(|| model.trim()) .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> { - let normalized = model - .trim() - .strip_prefix("openai/") - .unwrap_or_else(|| model.trim()) - .to_ascii_lowercase(); + let normalized = normalize_openai_model_id(model); if normalized.starts_with("gpt-5-pro") { 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] fn chat_body_keeps_default_temperature_for_openai_non_gpt5_models() { 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] fn chat_body_keeps_custom_temperature_for_gpt5_on_custom_provider() { // custom 预设由用户显式配温度(issue #857 的绕过路径:custom + temperature=1),