fix(ai): remove temperature from AI requests

This commit is contained in:
t8y2 2026-07-02 17:53:06 +08:00
parent 5cd87e862d
commit 3e01840825
6 changed files with 35 additions and 151 deletions

View File

@ -70,7 +70,7 @@ export interface AiRequestInput {
context: AiContext;
}
function buildAgentRequest(input: AiRequestInput, history?: api.AiMessage[]): { messages: api.AiMessage[]; systemPrompt: string; taskContract: api.AiTaskContract; maxTokens: number; temperature: number } {
function buildAgentRequest(input: AiRequestInput, history?: api.AiMessage[]): { messages: api.AiMessage[]; systemPrompt: string; taskContract: api.AiTaskContract; maxTokens: number } {
const isZh = isChineseLocale(currentLocale());
const systemPrompt = buildSystemPrompt(input.action, input.context, input.mode);
const userPrompt = buildUserPrompt(input.action, input.context, input.instruction, isZh);
@ -84,23 +84,22 @@ function buildAgentRequest(input: AiRequestInput, history?: api.AiMessage[]): {
const params = actionParams(input.action);
const maxTokens = input.config.enableThinking ? Math.max(params.maxTokens, 8192) : params.maxTokens;
return { messages, systemPrompt, taskContract, maxTokens, temperature: params.temperature };
return { messages, systemPrompt, taskContract, maxTokens };
}
export async function runAiAction(input: AiRequestInput, history?: api.AiMessage[]): Promise<string> {
const { messages, systemPrompt, taskContract, maxTokens, temperature } = buildAgentRequest(input, history);
const { messages, systemPrompt, taskContract, maxTokens } = buildAgentRequest(input, history);
return api.aiComplete({
config: input.config,
systemPrompt,
messages,
taskContract,
maxTokens,
temperature,
});
}
export async function runAiStream(input: AiRequestInput, history: api.AiMessage[] | undefined, onDelta: (delta: string) => void, sessionId?: string, onReasoningDelta?: (delta: string) => void): Promise<void> {
const { messages, systemPrompt, taskContract, maxTokens, temperature } = buildAgentRequest(input, history);
const { messages, systemPrompt, taskContract, maxTokens } = buildAgentRequest(input, history);
const sid = sessionId || uuid();
await api.aiStream(
@ -111,7 +110,6 @@ export async function runAiStream(input: AiRequestInput, history: api.AiMessage[
messages,
taskContract,
maxTokens,
temperature,
},
(chunk) => {
if (!chunk.done) {
@ -123,7 +121,7 @@ export async function runAiStream(input: AiRequestInput, history: api.AiMessage[
}
export async function runAgentStream(input: AiRequestInput, history: api.AiMessage[] | undefined, onEvent: (event: AgentEvent) => void, sessionId?: string): Promise<string> {
const { messages, systemPrompt, taskContract, maxTokens, temperature } = buildAgentRequest(input, history);
const { messages, systemPrompt, taskContract, maxTokens } = buildAgentRequest(input, history);
const sid = sessionId || uuid();
return api.aiAgentStream(
@ -134,7 +132,6 @@ export async function runAgentStream(input: AiRequestInput, history: api.AiMessa
messages,
taskContract,
maxTokens,
temperature,
},
input.context.connectionId,
input.context.database,
@ -155,14 +152,14 @@ export function buildUserPrompt(action: AiAction, context: AiContext, instructio
return [`Action: ${action}`, skillInstruction, "", "User request:", userRequest].join("\n");
}
function actionParams(action: AiAction): { maxTokens: number; temperature: number } {
function actionParams(action: AiAction): { maxTokens: number } {
switch (action) {
case "explain":
return { maxTokens: 3200, temperature: 0.2 };
return { maxTokens: 3200 };
case "sampleData":
return { maxTokens: 2400, temperature: 0.1 };
return { maxTokens: 2400 };
default:
return { maxTokens: 2400, temperature: 0.15 };
return { maxTokens: 2400 };
}
}

View File

@ -275,7 +275,6 @@ export interface AiCompletionRequest {
messages: AiMessage[];
taskContract?: AiTaskContract;
maxTokens?: number;
temperature?: number;
}
export interface AiModelInfo {

View File

@ -82,7 +82,6 @@ pub async fn run_agent_loop(
on_event: impl Fn(AgentEvent) + Send + Sync + Clone + 'static,
cancelled: &Notify,
max_tokens: Option<u32>,
temperature: Option<f32>,
task_contract: Option<&AiTaskContract>,
is_agent_mode: bool,
) -> Result<String, String> {
@ -124,7 +123,6 @@ pub async fn run_agent_loop(
on_event,
cancelled,
max_tokens,
temperature,
task_contract,
)
.await;
@ -181,7 +179,6 @@ pub async fn run_agent_loop(
&conversation_messages,
&tools,
max_tokens,
temperature,
task_contract.clone(),
);
@ -434,7 +431,6 @@ fn build_tool_request(
messages: &[AiMessage],
_tools: &[ToolDefinition], // Tools are injected in ai::stream_with_tools, not via AiCompletionRequest.
max_tokens: Option<u32>,
temperature: Option<f32>,
task_contract: Option<AiTaskContract>,
) -> AiCompletionRequest {
// Note: tools are passed via the body, not via AiCompletionRequest.
@ -445,7 +441,6 @@ fn build_tool_request(
messages: messages.to_vec(),
task_contract,
max_tokens: max_tokens.or(Some(4096)),
temperature: temperature.or(Some(0.2)),
}
}
@ -652,7 +647,6 @@ async fn run_agent_loop_text_only(
on_event: impl Fn(AgentEvent) + Send + Sync + 'static,
_cancelled: &Notify,
max_tokens: Option<u32>,
temperature: Option<f32>,
task_contract: Option<&AiTaskContract>,
) -> Result<String, String> {
// Build a schema-enriched system prompt so the LLM can answer schema questions
@ -665,7 +659,6 @@ async fn run_agent_loop_text_only(
messages: messages.to_vec(),
task_contract: task_contract.cloned(),
max_tokens: max_tokens.or(Some(4096)),
temperature: temperature.or(Some(0.2)),
};
for attempt in 0..=MAX_CONTRACT_REPAIR_ATTEMPTS {
@ -898,7 +891,6 @@ async fn maybe_compact(
}],
task_contract: None,
max_tokens: Some(1024),
temperature: Some(0.1),
};
let summary = match cancelled.notified().now_or_never() {

View File

@ -177,7 +177,6 @@ pub struct AiCompletionRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_contract: Option<AiTaskContract>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -495,8 +494,8 @@ fn set_chat_completion_token_limit(body: &mut serde_json::Value, config: &AiConf
}
}
/// Kimi K2.5+ models (including K2.7-Code) require fixed sampling parameters:
/// temperature=1.0, top_p=0.95, n=1. Any other value returns an error.
/// Kimi K2.5+ models (including K2.7-Code) handle thinking flags differently
/// and reject the OpenAI-compatible `extra_body.chat_template_kwargs` toggle.
///
/// Matches `kimi-k2.5`, `kimi-k2.6`, `kimi-k2.7-code`, K3+, and future versions,
/// while excluding older K2 variants (`kimi-k2`, `kimi-k2-thinking`, etc.).
@ -516,25 +515,6 @@ fn is_kimi_model(model: &str) -> bool {
}
}
pub fn supports_temperature(config: &AiConfig) -> bool {
!(is_kimi_model(&config.model) || uses_openai_max_completion_tokens(config))
}
fn add_temperature_if_supported_for_config(body: &mut serde_json::Value, config: &AiConfig, temperature: Option<f32>) {
if supports_temperature(config) {
body["temperature"] = temperature_value(temperature);
}
}
pub fn add_temperature_if_supported(body: &mut serde_json::Value, request: &AiCompletionRequest) {
add_temperature_if_supported_for_config(body, &request.config, request.temperature);
}
fn temperature_value(temperature: Option<f32>) -> serde_json::Value {
let value = ((temperature.unwrap_or(0.2) as f64) * 100.0).round() / 100.0;
json!(value)
}
fn responses_text(data: &serde_json::Value) -> String {
if let Some(text) = data["output_text"].as_str().filter(|text| !text.is_empty()) {
return text.to_string();
@ -878,7 +858,6 @@ pub async fn call_claude(client: &reqwest::Client, request: AiCompletionRequest)
let body = json!({
"model": request.config.model,
"max_tokens": request.max_tokens.unwrap_or(2048),
"temperature": temperature_value(request.temperature),
"system": claude_system_prompt(&request.system_prompt),
"messages": request.messages,
});
@ -915,7 +894,6 @@ pub async fn call_openai_compatible(client: &reqwest::Client, request: AiComplet
"messages": messages,
});
set_chat_completion_token_limit(&mut body_obj, &request.config, request.max_tokens.unwrap_or(2048));
add_temperature_if_supported(&mut body_obj, &request);
if !request.config.enable_thinking && !is_kimi_model(&request.config.model) {
body_obj["extra_body"] = json!({
"chat_template_kwargs": { "enable_thinking": false }
@ -942,12 +920,11 @@ pub async fn call_openai_compatible(client: &reqwest::Client, request: AiComplet
pub async fn call_responses_api(client: &reqwest::Client, request: AiCompletionRequest) -> Result<String, String> {
let headers = maybe_bearer_headers(&request.config)?;
let mut body = json!({
let body = json!({
"model": request.config.model,
"input": build_responses_input(&request.system_prompt, &request.messages),
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
});
add_temperature_if_supported(&mut body, &request);
let res = client
.post(resolve_endpoint(&request.config))
@ -983,7 +960,6 @@ pub async fn call_gemini(client: &reqwest::Client, request: AiCompletionRequest)
"contents": contents,
"generationConfig": {
"maxOutputTokens": request.max_tokens.unwrap_or(2048),
"temperature": temperature_value(request.temperature),
},
});
@ -1100,7 +1076,6 @@ pub async fn test_connection_core(config: &AiConfig) -> Result<AiTestConnectionR
let body = json!({
"model": &model,
"max_tokens": 16,
"temperature": temperature_value(Some(0.0)),
"system": CLAUDE_DEFAULT_SYSTEM,
"messages": [{ "role": "user", "content": TEST_PROMPT }],
"stream": true,
@ -1126,7 +1101,7 @@ pub async fn test_connection_core(config: &AiConfig) -> Result<AiTestConnectionR
.query(&[("key", config.api_key.as_str()), ("alt", "sse")])
.json(&json!({
"contents": [{ "parts": [{ "text": TEST_PROMPT }], "role": "user" }],
"generationConfig": { "maxOutputTokens": 16, "temperature": temperature_value(Some(0.0)) },
"generationConfig": { "maxOutputTokens": 16 },
}))
.send()
.await
@ -1141,7 +1116,6 @@ pub async fn test_connection_core(config: &AiConfig) -> Result<AiTestConnectionR
let body = json!({
"model": &model,
"max_tokens": 16,
"temperature": temperature_value(Some(0.0)),
"system": CLAUDE_DEFAULT_SYSTEM,
"messages": [{ "role": "user", "content": TEST_PROMPT }],
"stream": true,
@ -1178,7 +1152,6 @@ pub async fn test_connection_core(config: &AiConfig) -> Result<AiTestConnectionR
set_chat_completion_token_limit(&mut body, config, 16);
body
};
add_temperature_if_supported_for_config(&mut body_obj, config, Some(0.0));
if config.api_style != AiApiStyle::Responses && !config.enable_thinking && !is_kimi_model(&config.model) {
body_obj["extra_body"] = json!({
"chat_template_kwargs": { "enable_thinking": false }
@ -1352,7 +1325,6 @@ async fn stream_claude(
let body = json!({
"model": request.config.model,
"max_tokens": request.max_tokens.unwrap_or(2048),
"temperature": temperature_value(request.temperature),
"system": claude_system_prompt(&request.system_prompt),
"messages": request.messages,
"stream": true,
@ -1435,7 +1407,6 @@ async fn stream_openai(
"stream": true,
});
set_chat_completion_token_limit(&mut body_obj, &request.config, request.max_tokens.unwrap_or(2048));
add_temperature_if_supported(&mut body_obj, request);
if !request.config.enable_thinking && !is_kimi_model(&request.config.model) {
body_obj["extra_body"] = json!({
"chat_template_kwargs": { "enable_thinking": false }
@ -1518,13 +1489,12 @@ async fn stream_responses_api(
) -> Result<(), String> {
let headers = maybe_bearer_headers(&request.config)?;
let mut body = json!({
let body = json!({
"model": request.config.model,
"input": build_responses_input(&request.system_prompt, &request.messages),
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
"stream": true,
});
add_temperature_if_supported(&mut body, request);
let res = client
.post(resolve_endpoint(&request.config))
@ -1608,7 +1578,6 @@ async fn stream_gemini(
"contents": contents,
"generationConfig": {
"maxOutputTokens": request.max_tokens.unwrap_or(2048),
"temperature": temperature_value(request.temperature),
},
});
@ -1813,7 +1782,7 @@ async fn stream_claude_with_tools(
let tool_json: Vec<serde_json::Value> = tools.iter().map(|t| t.to_anthropic_tool()).collect();
let mut body = json!({
let body = json!({
"model": request.config.model,
"max_tokens": request.max_tokens.unwrap_or(4096),
"system": claude_system_prompt(&request.system_prompt),
@ -1821,7 +1790,6 @@ async fn stream_claude_with_tools(
"tools": tool_json,
"stream": true,
});
add_temperature_if_supported(&mut body, request);
let res = client
.post(resolve_endpoint(&request.config))
@ -1995,7 +1963,6 @@ async fn stream_openai_with_tools(
"stream_options": { "include_usage": true },
});
set_chat_completion_token_limit(&mut body, &request.config, request.max_tokens.unwrap_or(4096));
add_temperature_if_supported(&mut body, request);
let res = client
.post(resolve_endpoint(&request.config))
@ -2098,7 +2065,7 @@ async fn stream_responses_with_tools(
let headers = maybe_bearer_headers(&request.config)?;
let tool_json: Vec<serde_json::Value> = tools.iter().map(responses_function_tool).collect();
let mut body = json!({
let body = json!({
"model": request.config.model,
"input": build_responses_input_with_tools(&request.system_prompt, &request.messages),
"max_output_tokens": responses_max_output_tokens(request.max_tokens),
@ -2106,7 +2073,6 @@ async fn stream_responses_with_tools(
"tool_choice": "auto",
"stream": true,
});
add_temperature_if_supported(&mut body, request);
let res = client
.post(resolve_endpoint(&request.config))
@ -2477,14 +2443,14 @@ mod tests {
use std::collections::{HashMap, HashSet};
use super::{
add_temperature_if_supported_for_config, build_ai_http_client, build_responses_input_with_tools,
claude_headers, claude_system_prompt, drain_next_stream_line, emit_responses_function_call_item, gemini_text,
is_kimi_model, openai_response_text, openai_stream_reasoning, openai_stream_text, parse_model_list_response,
resolve_endpoint, resolve_model_list_endpoint, responses_function_tool, responses_max_output_tokens,
responses_stream_text, responses_text, responses_token_usage, set_chat_completion_token_limit,
stream_data_payload, supports_temperature, temperature_value, uses_anthropic_messages_api, validate_config,
AiApiStyle, AiAuthMethod, AiConfig, AiMessage, AiModelInfo, AiProvider, AiReasoningLevel, StreamToolEvent,
StreamingToolCallAccumulator, ToolCallRef, AUTHORIZATION, CLAUDE_DEFAULT_SYSTEM, TEST_PROMPT,
build_ai_http_client, build_responses_input_with_tools, claude_headers, claude_system_prompt,
drain_next_stream_line, emit_responses_function_call_item, gemini_text, is_kimi_model, openai_response_text,
openai_stream_reasoning, openai_stream_text, parse_model_list_response, resolve_endpoint,
resolve_model_list_endpoint, responses_function_tool, responses_max_output_tokens, responses_stream_text,
responses_text, responses_token_usage, set_chat_completion_token_limit, stream_data_payload,
uses_anthropic_messages_api, validate_config, AiApiStyle, AiAuthMethod, AiConfig, AiMessage, AiModelInfo,
AiProvider, AiReasoningLevel, StreamToolEvent, StreamingToolCallAccumulator, ToolCallRef, AUTHORIZATION,
CLAUDE_DEFAULT_SYSTEM, TEST_PROMPT,
};
/// Reproduce the "Unknown tool:" bug: some OpenAI-compatible providers
@ -3078,82 +3044,17 @@ mod tests {
}
#[test]
fn temperature_value_rounds_f32_to_provider_safe_precision() {
assert_eq!(temperature_value(Some(0.15)), serde_json::json!(0.15));
assert_eq!(temperature_value(Some(0.149)), serde_json::json!(0.15));
assert_eq!(temperature_value(None), serde_json::json!(0.2));
assert_eq!(serde_json::to_string(&temperature_value(Some(0.15))).unwrap(), "0.15");
}
fn detects_kimi_models_that_skip_extra_body_thinking_toggle() {
assert!(is_kimi_model("kimi-k2.7-code"));
assert!(is_kimi_model("kimi-k2.6"));
assert!(is_kimi_model("kimi-k2.5"));
assert!(is_kimi_model("kimi-k3"));
#[test]
fn omits_temperature_for_openai_reasoning_models() {
let mut config = AiConfig {
provider: AiProvider::Openai,
api_key: "key".to_string(),
auth_method: AiAuthMethod::Bearer,
endpoint: "https://api.openai.com/v1/chat/completions".to_string(),
model: "gpt-5.5".to_string(),
api_style: AiApiStyle::Completions,
proxy_enabled: false,
proxy_url: String::new(),
enable_thinking: true,
reasoning_level: AiReasoningLevel::Default,
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
};
assert!(!supports_temperature(&config));
config.model = "o4-mini".to_string();
assert!(!supports_temperature(&config));
config.model = "gpt-4o".to_string();
assert!(supports_temperature(&config));
config.endpoint = "http://localhost:11434/v1".to_string();
config.model = "gpt-5-proxy".to_string();
assert!(!supports_temperature(&config));
config.provider = AiProvider::OpenaiCompatible;
config.endpoint = "http://localhost:11434/v1".to_string();
config.model = "gpt-5-local".to_string();
assert!(supports_temperature(&config));
// Kimi K2.5+ models: temperature is forced to 1.0 by the API
config.model = "kimi-k2.7-code".to_string();
assert!(!supports_temperature(&config));
assert!(is_kimi_model(&config.model));
config.model = "kimi-k2.6".to_string();
assert!(!supports_temperature(&config));
assert!(is_kimi_model(&config.model));
config.model = "kimi-k2.5".to_string();
assert!(!supports_temperature(&config));
assert!(is_kimi_model(&config.model));
// K3+ should also be matched
config.model = "kimi-k3".to_string();
assert!(!supports_temperature(&config));
assert!(is_kimi_model(&config.model));
// Older K2 variants should NOT be matched (they support custom temperature)
config.model = "kimi-k2".to_string();
assert!(supports_temperature(&config));
assert!(!is_kimi_model(&config.model));
config.model = "kimi-k2-thinking".to_string();
assert!(supports_temperature(&config));
assert!(!is_kimi_model(&config.model));
config.model = "kimi-k2-0711-preview".to_string();
assert!(supports_temperature(&config));
assert!(!is_kimi_model(&config.model));
config.model = "kimi-k2.4".to_string();
assert!(supports_temperature(&config));
assert!(!is_kimi_model(&config.model));
// Older K2 variants should not skip OpenAI-compatible thinking toggles.
assert!(!is_kimi_model("kimi-k2"));
assert!(!is_kimi_model("kimi-k2-thinking"));
assert!(!is_kimi_model("kimi-k2-0711-preview"));
assert!(!is_kimi_model("kimi-k2.4"));
}
#[test]
@ -3222,7 +3123,7 @@ mod tests {
}
#[test]
fn omits_temperature_for_kimi_test_connection_body() {
fn omits_extra_body_for_kimi_test_connection_body() {
let config = AiConfig {
provider: AiProvider::OpenaiCompatible,
api_key: "key".to_string(),
@ -3245,14 +3146,12 @@ mod tests {
"stream": true,
});
add_temperature_if_supported_for_config(&mut body, &config, Some(0.0));
if !config.enable_thinking && !is_kimi_model(&config.model) {
body["extra_body"] = serde_json::json!({
"chat_template_kwargs": { "enable_thinking": false }
});
}
assert!(body.get("temperature").is_none());
assert!(body.get("extra_body").is_none());
}

View File

@ -273,7 +273,6 @@ pub async fn ai_agent_stream(
let req_messages = request.messages;
let req_task_contract = request.task_contract;
let req_max_tokens = request.max_tokens;
let req_temperature = request.temperature;
let is_agent_mode = body.mode == "agent";
let tx2 = tx.clone();
tokio::task::spawn_blocking(move || {
@ -291,7 +290,6 @@ pub async fn ai_agent_stream(
},
&cancelled,
req_max_tokens,
req_temperature,
req_task_contract.as_ref(),
is_agent_mode,
)

View File

@ -120,7 +120,6 @@ pub async fn ai_agent_stream(
},
&cancelled,
request.max_tokens,
request.temperature,
request.task_contract.as_ref(),
is_agent_mode,
)