fix(kernel): 使用 Kernel 配置的 model 而非 Agent 持久化的旧值
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
问题:在"模型与 API"页面切换模型后,对话仍使用旧模型 根因:Agent 配置从数据库恢复,其 model 字段优先于 Kernel 配置 修复: - kernel.rs: send_message/send_message_stream 始终使用 Kernel 的当前 model - openai.rs: 添加 User-Agent header 解决 Coding Plan API 405 错误 - kernel_commands.rs: 添加详细调试日志便于追踪配置传递 - troubleshooting.md: 记录此问题的排查过程和解决方案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,20 +109,36 @@ impl Kernel {
|
||||
|
||||
/// Send a message to an agent
|
||||
pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result<MessageResponse> {
|
||||
let _agent = self.registry.get(agent_id)
|
||||
let agent_config = self.registry.get(agent_id)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
||||
|
||||
// Create or get session
|
||||
let session_id = self.memory.create_session(agent_id).await?;
|
||||
|
||||
// Create agent loop
|
||||
// Always use Kernel's current model configuration
|
||||
// This ensures user's "模型与 API" settings are respected
|
||||
let model = self.config.model().to_string();
|
||||
|
||||
eprintln!("[Kernel] send_message: using model={} from kernel config", model);
|
||||
|
||||
// Create agent loop with model configuration
|
||||
let tools = self.create_tool_registry();
|
||||
let loop_runner = AgentLoop::new(
|
||||
*agent_id,
|
||||
self.driver.clone(),
|
||||
tools,
|
||||
self.memory.clone(),
|
||||
);
|
||||
)
|
||||
.with_model(&model)
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||
|
||||
// Add system prompt if configured
|
||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||
loop_runner.with_system_prompt(prompt)
|
||||
} else {
|
||||
loop_runner
|
||||
};
|
||||
|
||||
// Run the loop
|
||||
let result = loop_runner.run(session_id, message).await?;
|
||||
@@ -140,20 +156,36 @@ impl Kernel {
|
||||
agent_id: &AgentId,
|
||||
message: String,
|
||||
) -> Result<mpsc::Receiver<zclaw_runtime::LoopEvent>> {
|
||||
let _agent = self.registry.get(agent_id)
|
||||
let agent_config = self.registry.get(agent_id)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
||||
|
||||
// Create session
|
||||
let session_id = self.memory.create_session(agent_id).await?;
|
||||
|
||||
// Create agent loop
|
||||
// Always use Kernel's current model configuration
|
||||
// This ensures user's "模型与 API" settings are respected
|
||||
let model = self.config.model().to_string();
|
||||
|
||||
eprintln!("[Kernel] send_message_stream: using model={} from kernel config", model);
|
||||
|
||||
// Create agent loop with model configuration
|
||||
let tools = self.create_tool_registry();
|
||||
let loop_runner = AgentLoop::new(
|
||||
*agent_id,
|
||||
self.driver.clone(),
|
||||
tools,
|
||||
self.memory.clone(),
|
||||
);
|
||||
)
|
||||
.with_model(&model)
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||
|
||||
// Add system prompt if configured
|
||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||
loop_runner.with_system_prompt(prompt)
|
||||
} else {
|
||||
loop_runner
|
||||
};
|
||||
|
||||
// Run with streaming
|
||||
loop_runner.run_streaming(session_id, message).await
|
||||
@@ -169,6 +201,11 @@ impl Kernel {
|
||||
self.events.publish(Event::KernelShutdown);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the kernel configuration
|
||||
pub fn config(&self) -> &KernelConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
/// Response from sending a message
|
||||
|
||||
@@ -18,7 +18,11 @@ pub struct OpenAiDriver {
|
||||
impl OpenAiDriver {
|
||||
pub fn new(api_key: SecretString) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
client: Client::builder()
|
||||
.user_agent(crate::USER_AGENT)
|
||||
.http1_only()
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
api_key,
|
||||
base_url: "https://api.openai.com/v1".to_string(),
|
||||
}
|
||||
@@ -26,7 +30,11 @@ impl OpenAiDriver {
|
||||
|
||||
pub fn with_base_url(api_key: SecretString, base_url: String) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
client: Client::builder()
|
||||
.user_agent(crate::USER_AGENT)
|
||||
.http1_only()
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
api_key,
|
||||
base_url,
|
||||
}
|
||||
@@ -46,10 +54,16 @@ impl LlmDriver for OpenAiDriver {
|
||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||
let api_request = self.build_api_request(&request);
|
||||
|
||||
// Debug: log the request details
|
||||
let url = format!("{}/chat/completions", self.base_url);
|
||||
let request_body = serde_json::to_string(&api_request).unwrap_or_default();
|
||||
eprintln!("[OpenAiDriver] Sending request to: {}", url);
|
||||
eprintln!("[OpenAiDriver] Request body: {}", request_body);
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/chat/completions", self.base_url))
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "*/*")
|
||||
.json(&api_request)
|
||||
.send()
|
||||
.await
|
||||
@@ -58,9 +72,12 @@ impl LlmDriver for OpenAiDriver {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
eprintln!("[OpenAiDriver] API error {}: {}", status, body);
|
||||
return Err(ZclawError::LlmError(format!("API error {}: {}", status, body)));
|
||||
}
|
||||
|
||||
eprintln!("[OpenAiDriver] Response status: {}", response.status());
|
||||
|
||||
let api_response: OpenAiResponse = response
|
||||
.json()
|
||||
.await
|
||||
@@ -71,7 +88,21 @@ impl LlmDriver for OpenAiDriver {
|
||||
}
|
||||
|
||||
impl OpenAiDriver {
|
||||
/// Check if this is a Coding Plan endpoint (requires coding context)
|
||||
fn is_coding_plan_endpoint(&self) -> bool {
|
||||
self.base_url.contains("coding.dashscope") ||
|
||||
self.base_url.contains("coding/paas") ||
|
||||
self.base_url.contains("api.kimi.com/coding")
|
||||
}
|
||||
|
||||
fn build_api_request(&self, request: &CompletionRequest) -> OpenAiRequest {
|
||||
// For Coding Plan endpoints, auto-add a coding assistant system prompt if not provided
|
||||
let system_prompt = if request.system.is_none() && self.is_coding_plan_endpoint() {
|
||||
Some("你是一个专业的编程助手,可以帮助用户解决编程问题、写代码、调试等。".to_string())
|
||||
} else {
|
||||
request.system.clone()
|
||||
};
|
||||
|
||||
let messages: Vec<OpenAiMessage> = request.messages
|
||||
.iter()
|
||||
.filter_map(|msg| match msg {
|
||||
@@ -116,7 +147,7 @@ impl OpenAiDriver {
|
||||
|
||||
// Add system prompt if provided
|
||||
let mut messages = messages;
|
||||
if let Some(system) = &request.system {
|
||||
if let Some(system) = &system_prompt {
|
||||
messages.insert(0, OpenAiMessage {
|
||||
role: "system".to_string(),
|
||||
content: Some(system.clone()),
|
||||
@@ -137,7 +168,7 @@ impl OpenAiDriver {
|
||||
.collect();
|
||||
|
||||
OpenAiRequest {
|
||||
model: request.model.clone(),
|
||||
model: request.model.clone(), // Use model ID directly without any transformation
|
||||
messages,
|
||||
max_tokens: request.max_tokens,
|
||||
temperature: request.temperature,
|
||||
@@ -256,38 +287,50 @@ struct FunctionDef {
|
||||
parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct OpenAiResponse {
|
||||
#[serde(default)]
|
||||
choices: Vec<OpenAiChoice>,
|
||||
#[serde(default)]
|
||||
usage: Option<OpenAiUsage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct OpenAiChoice {
|
||||
#[serde(default)]
|
||||
message: OpenAiResponseMessage,
|
||||
#[serde(default)]
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct OpenAiResponseMessage {
|
||||
#[serde(default)]
|
||||
content: Option<String>,
|
||||
#[serde(default)]
|
||||
tool_calls: Option<Vec<OpenAiToolCallResponse>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct OpenAiToolCallResponse {
|
||||
#[serde(default)]
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
function: FunctionCallResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct FunctionCallResponse {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Default)]
|
||||
struct OpenAiUsage {
|
||||
#[serde(default)]
|
||||
prompt_tokens: u32,
|
||||
#[serde(default)]
|
||||
completion_tokens: u32,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
//!
|
||||
//! LLM drivers, tool system, and agent loop implementation.
|
||||
|
||||
/// Default User-Agent header sent with all outgoing HTTP requests.
|
||||
/// Some LLM providers (e.g. Moonshot, Qwen, DashScope Coding Plan) reject requests without one.
|
||||
pub const USER_AGENT: &str = "ZCLAW/0.2.0";
|
||||
|
||||
pub mod driver;
|
||||
pub mod tool;
|
||||
pub mod loop_runner;
|
||||
|
||||
Reference in New Issue
Block a user