mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-28 16:36:45 +00:00
chore: fix conflict markers and cargo fmt drift in main (commands, openai_compat, trident, config, tools)
This commit is contained in:
@@ -510,7 +510,11 @@ impl StreamState {
|
|||||||
.delta
|
.delta
|
||||||
.reasoning_content
|
.reasoning_content
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.or(choice.delta.thinking.and_then(|t| t.content).filter(|value| !value.is_empty()))
|
.or(choice
|
||||||
|
.delta
|
||||||
|
.thinking
|
||||||
|
.and_then(|t| t.content)
|
||||||
|
.filter(|value| !value.is_empty()))
|
||||||
{
|
{
|
||||||
if !self.thinking_started {
|
if !self.thinking_started {
|
||||||
self.thinking_started = true;
|
self.thinking_started = true;
|
||||||
@@ -942,7 +946,10 @@ fn wire_model_for_base_url<'a>(
|
|||||||
if lowered_prefix == "openai" {
|
if lowered_prefix == "openai" {
|
||||||
let trimmed_base_url = base_url.trim_end_matches('/');
|
let trimmed_base_url = base_url.trim_end_matches('/');
|
||||||
let default_openai = DEFAULT_OPENAI_BASE_URL.trim_end_matches('/');
|
let default_openai = DEFAULT_OPENAI_BASE_URL.trim_end_matches('/');
|
||||||
if matches!(lowered_prefix.as_str(), "xai" | "grok" | "kimi" | "gemini" | "gemma") {
|
if matches!(
|
||||||
|
lowered_prefix.as_str(),
|
||||||
|
"xai" | "grok" | "kimi" | "gemini" | "gemma"
|
||||||
|
) {
|
||||||
return Cow::Borrowed(&model[pos + 1..]);
|
return Cow::Borrowed(&model[pos + 1..]);
|
||||||
}
|
}
|
||||||
if config.provider_name == "OpenAI" && trimmed_base_url != default_openai {
|
if config.provider_name == "OpenAI" && trimmed_base_url != default_openai {
|
||||||
@@ -1505,7 +1512,9 @@ fn parse_sse_frame(
|
|||||||
return Err(ApiError::Api {
|
return Err(ApiError::Api {
|
||||||
status: reqwest::StatusCode::BAD_REQUEST,
|
status: reqwest::StatusCode::BAD_REQUEST,
|
||||||
error_type: Some("invalid_response".to_string()),
|
error_type: Some("invalid_response".to_string()),
|
||||||
message: Some("provider returned HTML instead of JSON (check endpoint URL)".to_string()),
|
message: Some(
|
||||||
|
"provider returned HTML instead of JSON (check endpoint URL)".to_string(),
|
||||||
|
),
|
||||||
request_id: None,
|
request_id: None,
|
||||||
body: trimmed.chars().take(200).collect(),
|
body: trimmed.chars().take(200).collect(),
|
||||||
retryable: false,
|
retryable: false,
|
||||||
@@ -1555,7 +1564,9 @@ fn parse_sse_frame(
|
|||||||
return Err(ApiError::Api {
|
return Err(ApiError::Api {
|
||||||
status: reqwest::StatusCode::BAD_REQUEST,
|
status: reqwest::StatusCode::BAD_REQUEST,
|
||||||
error_type: Some("invalid_response".to_string()),
|
error_type: Some("invalid_response".to_string()),
|
||||||
message: Some("provider returned HTML instead of JSON (check endpoint URL)".to_string()),
|
message: Some(
|
||||||
|
"provider returned HTML instead of JSON (check endpoint URL)".to_string(),
|
||||||
|
),
|
||||||
request_id: None,
|
request_id: None,
|
||||||
body: payload.chars().take(200).collect(),
|
body: payload.chars().take(200).collect(),
|
||||||
retryable: false,
|
retryable: false,
|
||||||
|
|||||||
@@ -1477,10 +1477,6 @@ pub fn validate_slash_command_input(
|
|||||||
"theme" => SlashCommand::Theme { name: remainder },
|
"theme" => SlashCommand::Theme { name: remainder },
|
||||||
"voice" => SlashCommand::Voice { mode: remainder },
|
"voice" => SlashCommand::Voice { mode: remainder },
|
||||||
"usage" => SlashCommand::Usage { scope: remainder },
|
"usage" => SlashCommand::Usage { scope: remainder },
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
"setup" => SlashCommand::Setup,
|
|
||||||
>>>>>>> 2f6a225 (fix: make id field optional in OpenAI response parsing)
|
|
||||||
"rename" => SlashCommand::Rename { name: remainder },
|
"rename" => SlashCommand::Rename { name: remainder },
|
||||||
"copy" => SlashCommand::Copy { target: remainder },
|
"copy" => SlashCommand::Copy { target: remainder },
|
||||||
"hooks" => SlashCommand::Hooks { args: remainder },
|
"hooks" => SlashCommand::Hooks { args: remainder },
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
|
|||||||
let raw_keep_from = if config.preserve_recent_messages == 0 {
|
let raw_keep_from = if config.preserve_recent_messages == 0 {
|
||||||
session.messages.len()
|
session.messages.len()
|
||||||
} else {
|
} else {
|
||||||
session.messages.len().saturating_sub(config.preserve_recent_messages)
|
session
|
||||||
|
.messages
|
||||||
|
.len()
|
||||||
|
.saturating_sub(config.preserve_recent_messages)
|
||||||
};
|
};
|
||||||
// Ensure we do not split a tool-use / tool-result pair at the compaction
|
// Ensure we do not split a tool-use / tool-result pair at the compaction
|
||||||
// boundary. If the first preserved message is a user message whose first
|
// boundary. If the first preserved message is a user message whose first
|
||||||
@@ -300,7 +303,11 @@ fn merge_compact_summaries(existing_summary: Option<&str>, new_summary: &str) ->
|
|||||||
// "- Previously compacted context:" or the nesting compounds with each
|
// "- Previously compacted context:" or the nesting compounds with each
|
||||||
// compaction cycle, inflating the summary by ~depth * overhead per turn.
|
// compaction cycle, inflating the summary by ~depth * overhead per turn.
|
||||||
if !previous_highlights.is_empty() {
|
if !previous_highlights.is_empty() {
|
||||||
lines.extend(previous_highlights.into_iter().map(|line| format!("- {line}")));
|
lines.extend(
|
||||||
|
previous_highlights
|
||||||
|
.into_iter()
|
||||||
|
.map(|line| format!("- {line}")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !new_highlights.is_empty() {
|
if !new_highlights.is_empty() {
|
||||||
|
|||||||
@@ -608,16 +608,28 @@ pub fn save_user_provider_settings(
|
|||||||
let mut root = read_settings_root(&settings_path);
|
let mut root = read_settings_root(&settings_path);
|
||||||
|
|
||||||
let mut provider = serde_json::Map::new();
|
let mut provider = serde_json::Map::new();
|
||||||
provider.insert("kind".to_string(), serde_json::Value::String(kind.to_string()));
|
provider.insert(
|
||||||
provider.insert("apiKey".to_string(), serde_json::Value::String(api_key.to_string()));
|
"kind".to_string(),
|
||||||
|
serde_json::Value::String(kind.to_string()),
|
||||||
|
);
|
||||||
|
provider.insert(
|
||||||
|
"apiKey".to_string(),
|
||||||
|
serde_json::Value::String(api_key.to_string()),
|
||||||
|
);
|
||||||
if let Some(base_url) = base_url {
|
if let Some(base_url) = base_url {
|
||||||
provider.insert("baseUrl".to_string(), serde_json::Value::String(base_url.to_string()));
|
provider.insert(
|
||||||
|
"baseUrl".to_string(),
|
||||||
|
serde_json::Value::String(base_url.to_string()),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
provider.remove("baseUrl");
|
provider.remove("baseUrl");
|
||||||
}
|
}
|
||||||
root.insert("provider".to_string(), serde_json::Value::Object(provider));
|
root.insert("provider".to_string(), serde_json::Value::Object(provider));
|
||||||
if let Some(model) = model {
|
if let Some(model) = model {
|
||||||
root.insert("model".to_string(), serde_json::Value::String(model.to_string()));
|
root.insert(
|
||||||
|
"model".to_string(),
|
||||||
|
serde_json::Value::String(model.to_string()),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
root.remove("model");
|
root.remove("model");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ pub mod bash_validation;
|
|||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
pub mod branch_lock;
|
pub mod branch_lock;
|
||||||
mod compact;
|
mod compact;
|
||||||
pub mod trident;
|
|
||||||
mod config;
|
mod config;
|
||||||
pub mod config_validate;
|
pub mod config_validate;
|
||||||
mod conversation;
|
mod conversation;
|
||||||
@@ -40,6 +39,7 @@ mod report_schema;
|
|||||||
pub mod sandbox;
|
pub mod sandbox;
|
||||||
mod session;
|
mod session;
|
||||||
pub mod session_control;
|
pub mod session_control;
|
||||||
|
pub mod trident;
|
||||||
pub use session_control::SessionStore;
|
pub use session_control::SessionStore;
|
||||||
mod sse;
|
mod sse;
|
||||||
pub mod stale_base;
|
pub mod stale_base;
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ impl TridentStats {
|
|||||||
self.messages_clustered, self.clusters_found
|
self.messages_clustered, self.clusters_found
|
||||||
),
|
),
|
||||||
format!(" Original: {} messages", self.original_message_count),
|
format!(" Original: {} messages", self.original_message_count),
|
||||||
format!(" Final: {} messages ({:.1}x compression)", self.final_message_count, compression),
|
format!(
|
||||||
|
" Final: {} messages ({:.1}x compression)",
|
||||||
|
self.final_message_count, compression
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if self.tokens_saved_estimate > 0 {
|
if self.tokens_saved_estimate > 0 {
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
@@ -121,7 +124,8 @@ pub fn trident_compact_session(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if trident_config.collapse_enabled {
|
if trident_config.collapse_enabled {
|
||||||
let (collapsed, chains, collapsed_count) = stage2_collapse(&messages, trident_config.collapse_threshold);
|
let (collapsed, chains, collapsed_count) =
|
||||||
|
stage2_collapse(&messages, trident_config.collapse_threshold);
|
||||||
stats.collapsed_chains = chains;
|
stats.collapsed_chains = chains;
|
||||||
stats.messages_collapsed = collapsed_count;
|
stats.messages_collapsed = collapsed_count;
|
||||||
messages = collapsed;
|
messages = collapsed;
|
||||||
@@ -178,10 +182,10 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec<ConversationMessag
|
|||||||
for (i, msg) in messages.iter().enumerate() {
|
for (i, msg) in messages.iter().enumerate() {
|
||||||
for block in &msg.blocks {
|
for block in &msg.blocks {
|
||||||
if let Some((path, op_type)) = extract_file_operation(block) {
|
if let Some((path, op_type)) = extract_file_operation(block) {
|
||||||
file_ops.entry(path).or_default().push(FileOperation {
|
file_ops
|
||||||
index: i,
|
.entry(path)
|
||||||
op_type,
|
.or_default()
|
||||||
});
|
.push(FileOperation { index: i, op_type });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +239,9 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
|
|||||||
};
|
};
|
||||||
Some((path, op_type))
|
Some((path, op_type))
|
||||||
}
|
}
|
||||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
ContentBlock::ToolResult {
|
||||||
|
tool_name, output, ..
|
||||||
|
} => {
|
||||||
let path = extract_path_from_tool_output(tool_name, output)?;
|
let path = extract_path_from_tool_output(tool_name, output)?;
|
||||||
let op_type = match tool_name.as_str() {
|
let op_type = match tool_name.as_str() {
|
||||||
"read_file" | "Read" => FileOp::Read,
|
"read_file" | "Read" => FileOp::Read,
|
||||||
@@ -250,8 +256,10 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String> {
|
fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String> {
|
||||||
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
|
if !matches!(
|
||||||
{
|
tool_name,
|
||||||
|
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
|
||||||
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
serde_json::from_str::<serde_json::Value>(input)
|
serde_json::from_str::<serde_json::Value>(input)
|
||||||
@@ -265,8 +273,10 @@ fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option<String> {
|
fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option<String> {
|
||||||
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
|
if !matches!(
|
||||||
{
|
tool_name,
|
||||||
|
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
|
||||||
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
serde_json::from_str::<serde_json::Value>(output)
|
serde_json::from_str::<serde_json::Value>(output)
|
||||||
@@ -340,14 +350,24 @@ fn stage2_collapse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_chatty_message(msg: &ConversationMessage) -> bool {
|
fn is_chatty_message(msg: &ConversationMessage) -> bool {
|
||||||
let total_chars: usize = msg.blocks.iter().map(|b| match b {
|
let total_chars: usize = msg
|
||||||
ContentBlock::Text { text } => text.len(),
|
.blocks
|
||||||
ContentBlock::ToolUse { input, .. } => input.len(),
|
.iter()
|
||||||
ContentBlock::ToolResult { output, .. } => output.len(),
|
.map(|b| match b {
|
||||||
}).sum();
|
ContentBlock::Text { text } => text.len(),
|
||||||
|
ContentBlock::ToolUse { input, .. } => input.len(),
|
||||||
|
ContentBlock::ToolResult { output, .. } => output.len(),
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
let has_tool_use = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolUse { .. }));
|
let has_tool_use = msg
|
||||||
let has_tool_result = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. }));
|
.blocks
|
||||||
|
.iter()
|
||||||
|
.any(|b| matches!(b, ContentBlock::ToolUse { .. }));
|
||||||
|
let has_tool_result = msg
|
||||||
|
.blocks
|
||||||
|
.iter()
|
||||||
|
.any(|b| matches!(b, ContentBlock::ToolResult { .. }));
|
||||||
|
|
||||||
if has_tool_use || has_tool_result {
|
if has_tool_use || has_tool_result {
|
||||||
return false;
|
return false;
|
||||||
@@ -463,16 +483,12 @@ fn stage3_cluster(
|
|||||||
cluster_buffers.entry(cid).or_default().push(*msg_idx);
|
cluster_buffers.entry(cid).or_default().push(*msg_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for (i, msg) in messages.iter().enumerate() {
|
for (i, msg) in messages.iter().enumerate() {
|
||||||
if let Some(&cid) = cluster_assignments.get(&i) {
|
if let Some(&cid) = cluster_assignments.get(&i) {
|
||||||
if let Some(buffer) = cluster_buffers.get_mut(&cid) {
|
if let Some(buffer) = cluster_buffers.get_mut(&cid) {
|
||||||
if buffer[0] == i {
|
if buffer[0] == i {
|
||||||
let cluster_messages: Vec<&ConversationMessage> = buffer
|
let cluster_messages: Vec<&ConversationMessage> =
|
||||||
.iter()
|
buffer.iter().filter_map(|&idx| messages.get(idx)).collect();
|
||||||
.filter_map(|&idx| messages.get(idx))
|
|
||||||
.collect();
|
|
||||||
let summary = generate_cluster_summary(&cluster_messages);
|
let summary = generate_cluster_summary(&cluster_messages);
|
||||||
result.push(ConversationMessage {
|
result.push(ConversationMessage {
|
||||||
role: MessageRole::System,
|
role: MessageRole::System,
|
||||||
@@ -518,7 +534,9 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option<Messag
|
|||||||
}
|
}
|
||||||
text_length += input.len();
|
text_length += input.len();
|
||||||
}
|
}
|
||||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
ContentBlock::ToolResult {
|
||||||
|
tool_name, output, ..
|
||||||
|
} => {
|
||||||
tool_names.insert(tool_name.clone());
|
tool_names.insert(tool_name.clone());
|
||||||
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
||||||
file_paths.insert(path);
|
file_paths.insert(path);
|
||||||
@@ -591,7 +609,9 @@ fn generate_cluster_summary(messages: &[&ConversationMessage]) -> String {
|
|||||||
file_paths.insert(path);
|
file_paths.insert(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
ContentBlock::ToolResult {
|
||||||
|
tool_name, output, ..
|
||||||
|
} => {
|
||||||
tool_names.insert(tool_name.clone());
|
tool_names.insert(tool_name.clone());
|
||||||
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
||||||
file_paths.insert(path);
|
file_paths.insert(path);
|
||||||
@@ -660,13 +680,23 @@ mod tests {
|
|||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"old"}"#, false),
|
ConversationMessage::tool_result(
|
||||||
|
"1",
|
||||||
|
"read_file",
|
||||||
|
r#"{"path":"src/main.rs","content":"old"}"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||||
id: "2".to_string(),
|
id: "2".to_string(),
|
||||||
name: "edit_file".to_string(),
|
name: "edit_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
|
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
|
ConversationMessage::tool_result(
|
||||||
|
"2",
|
||||||
|
"edit_file",
|
||||||
|
r#"{"path":"src/main.rs","ok":true}"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let (kept, superseded) = stage1_supersede(&messages);
|
let (kept, superseded) = stage1_supersede(&messages);
|
||||||
@@ -682,7 +712,12 @@ mod tests {
|
|||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"data"}"#, false),
|
ConversationMessage::tool_result(
|
||||||
|
"1",
|
||||||
|
"read_file",
|
||||||
|
r#"{"path":"src/main.rs","content":"data"}"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let (kept, superseded) = stage1_supersede(&messages);
|
let (kept, superseded) = stage1_supersede(&messages);
|
||||||
@@ -699,11 +734,13 @@ mod tests {
|
|||||||
text: format!("got {i}"),
|
text: format!("got {i}"),
|
||||||
}]));
|
}]));
|
||||||
}
|
}
|
||||||
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
messages.push(ConversationMessage::assistant(vec![
|
||||||
id: "t".to_string(),
|
ContentBlock::ToolUse {
|
||||||
name: "bash".to_string(),
|
id: "t".to_string(),
|
||||||
input: r#"{"command":"ls"}"#.to_string(),
|
name: "bash".to_string(),
|
||||||
}]));
|
input: r#"{"command":"ls"}"#.to_string(),
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
let (result, chains, collapsed) = stage2_collapse(&messages, 4);
|
let (result, chains, collapsed) = stage2_collapse(&messages, 4);
|
||||||
assert!(chains > 0, "should collapse at least one chain");
|
assert!(chains > 0, "should collapse at least one chain");
|
||||||
@@ -715,11 +752,13 @@ mod tests {
|
|||||||
fn stage3_clusters_similar_messages() {
|
fn stage3_clusters_similar_messages() {
|
||||||
let mut messages = vec![];
|
let mut messages = vec![];
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
messages.push(ConversationMessage::assistant(vec![
|
||||||
id: format!("read_{i}"),
|
ContentBlock::ToolUse {
|
||||||
name: "read_file".to_string(),
|
id: format!("read_{i}"),
|
||||||
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
name: "read_file".to_string(),
|
||||||
}]));
|
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
||||||
|
},
|
||||||
|
]));
|
||||||
messages.push(ConversationMessage::tool_result(
|
messages.push(ConversationMessage::tool_result(
|
||||||
&format!("read_{i}"),
|
&format!("read_{i}"),
|
||||||
"read_file",
|
"read_file",
|
||||||
@@ -728,8 +767,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (result, clusters, clustered) =
|
let (result, clusters, clustered) = stage3_cluster(&messages, 3, 0.4);
|
||||||
stage3_cluster(&messages, 3, 0.4);
|
|
||||||
assert!(clusters > 0, "should find at least one cluster");
|
assert!(clusters > 0, "should find at least one cluster");
|
||||||
assert!(clustered > 0);
|
assert!(clustered > 0);
|
||||||
assert!(result.len() < messages.len());
|
assert!(result.len() < messages.len());
|
||||||
@@ -745,13 +783,23 @@ mod tests {
|
|||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#, false),
|
ConversationMessage::tool_result(
|
||||||
|
"1",
|
||||||
|
"read_file",
|
||||||
|
r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||||
id: "2".to_string(),
|
id: "2".to_string(),
|
||||||
name: "edit_file".to_string(),
|
name: "edit_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
|
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
|
ConversationMessage::tool_result(
|
||||||
|
"2",
|
||||||
|
"edit_file",
|
||||||
|
r#"{"path":"src/main.rs","ok":true}"#,
|
||||||
|
false,
|
||||||
|
),
|
||||||
ConversationMessage::assistant(vec![ContentBlock::Text {
|
ConversationMessage::assistant(vec![ContentBlock::Text {
|
||||||
text: "Fixed the bug in main.rs".to_string(),
|
text: "Fixed the bug in main.rs".to_string(),
|
||||||
}]),
|
}]),
|
||||||
@@ -767,7 +815,10 @@ mod tests {
|
|||||||
&trident_config,
|
&trident_config,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(result.removed_message_count > 0 || result.compacted_session.messages.len() < session.messages.len());
|
assert!(
|
||||||
|
result.removed_message_count > 0
|
||||||
|
|| result.compacted_session.messages.len() < session.messages.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2201,8 +2201,16 @@ fn check_auth_health() -> DiagnosticCheck {
|
|||||||
let env_details = format!(
|
let env_details = format!(
|
||||||
"Environment api_key={} auth_token={} openai_key={}",
|
"Environment api_key={} auth_token={} openai_key={}",
|
||||||
if api_key_present { "present" } else { "absent" },
|
if api_key_present { "present" } else { "absent" },
|
||||||
if auth_token_present { "present" } else { "absent" },
|
if auth_token_present {
|
||||||
if openai_key_present { "present" } else { "absent" }
|
"present"
|
||||||
|
} else {
|
||||||
|
"absent"
|
||||||
|
},
|
||||||
|
if openai_key_present {
|
||||||
|
"present"
|
||||||
|
} else {
|
||||||
|
"absent"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
match load_oauth_credentials() {
|
match load_oauth_credentials() {
|
||||||
@@ -5047,7 +5055,7 @@ impl LiveCli {
|
|||||||
TerminalRenderer::new().color_theme(),
|
TerminalRenderer::new().color_theme(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Auto-compact retry on context window errors
|
// Auto-compact retry on context window errors
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -5066,7 +5074,7 @@ impl LiveCli {
|
|||||||
// - "Context window blocked"
|
// - "Context window blocked"
|
||||||
// - "This model's maximum context length is X tokens..."
|
// - "This model's maximum context length is X tokens..."
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
let error_str = error.to_string();
|
let error_str = error.to_string();
|
||||||
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
|
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
|
||||||
// return 400 with "no parseable body" instead of a proper context_length_exceeded
|
// return 400 with "no parseable body" instead of a proper context_length_exceeded
|
||||||
@@ -5074,7 +5082,7 @@ impl LiveCli {
|
|||||||
let is_context_window = error_str.contains("context_window")
|
let is_context_window = error_str.contains("context_window")
|
||||||
|| error_str.contains("Context window")
|
|| error_str.contains("Context window")
|
||||||
|| error_str.contains("no parseable body");
|
|| error_str.contains("no parseable body");
|
||||||
|
|
||||||
if is_context_window {
|
if is_context_window {
|
||||||
// A single compaction pass may not free enough context space.
|
// A single compaction pass may not free enough context space.
|
||||||
// Progressive retry: each round preserves fewer recent messages (4→2→1→0),
|
// Progressive retry: each round preserves fewer recent messages (4→2→1→0),
|
||||||
@@ -5082,7 +5090,7 @@ impl LiveCli {
|
|||||||
// Max 4 rounds before giving up and surfacing the error to the user.
|
// Max 4 rounds before giving up and surfacing the error to the user.
|
||||||
let max_compact_rounds = 4;
|
let max_compact_rounds = 4;
|
||||||
let preserve_schedule = [4, 2, 1, 0];
|
let preserve_schedule = [4, 2, 1, 0];
|
||||||
|
|
||||||
for round in 0..max_compact_rounds {
|
for round in 0..max_compact_rounds {
|
||||||
let preserve = preserve_schedule[round];
|
let preserve = preserve_schedule[round];
|
||||||
println!(
|
println!(
|
||||||
@@ -5091,7 +5099,7 @@ impl LiveCli {
|
|||||||
max_compact_rounds,
|
max_compact_rounds,
|
||||||
preserve
|
preserve
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run Trident pipeline then summary-based compaction
|
// Run Trident pipeline then summary-based compaction
|
||||||
let result = runtime::trident::trident_compact_session(
|
let result = runtime::trident::trident_compact_session(
|
||||||
runtime.session(),
|
runtime.session(),
|
||||||
@@ -5102,38 +5110,53 @@ impl LiveCli {
|
|||||||
&runtime::trident::TridentConfig::default(),
|
&runtime::trident::TridentConfig::default(),
|
||||||
);
|
);
|
||||||
let removed = result.removed_message_count;
|
let removed = result.removed_message_count;
|
||||||
|
|
||||||
if removed == 0 && round > 0 {
|
if removed == 0 && round > 0 {
|
||||||
// No more messages to compact — further rounds won't help
|
// No more messages to compact — further rounds won't help
|
||||||
println!(" No further compaction possible.");
|
println!(" No further compaction possible.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if removed > 0 {
|
if removed > 0 {
|
||||||
println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false));
|
println!(
|
||||||
|
"{}",
|
||||||
|
format_compact_report(
|
||||||
|
removed,
|
||||||
|
result.compacted_session.messages.len(),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without this, prepare_turn_runtime() reads from self.runtime.session()
|
// Without this, prepare_turn_runtime() reads from self.runtime.session()
|
||||||
// which still holds the ORIGINAL un-compacted session, so every retry round
|
// which still holds the ORIGINAL un-compacted session, so every retry round
|
||||||
// would send the same bloated request — compaction was wasted.
|
// would send the same bloated request — compaction was wasted.
|
||||||
*self.runtime.session_mut() = result.compacted_session.clone();
|
*self.runtime.session_mut() = result.compacted_session.clone();
|
||||||
|
|
||||||
// Build a new runtime with the compacted session and retry
|
// Build a new runtime with the compacted session and retry
|
||||||
let (mut new_runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
let (mut new_runtime, hook_abort_monitor) =
|
||||||
|
self.prepare_turn_runtime(true)?;
|
||||||
drop(hook_abort_monitor);
|
drop(hook_abort_monitor);
|
||||||
|
|
||||||
let mut rp = CliPermissionPrompter::new(self.permission_mode);
|
let mut rp = CliPermissionPrompter::new(self.permission_mode);
|
||||||
match new_runtime.run_turn(input, Some(&mut rp)) {
|
match new_runtime.run_turn(input, Some(&mut rp)) {
|
||||||
Ok(summary) => {
|
Ok(summary) => {
|
||||||
self.replace_runtime(new_runtime)?;
|
self.replace_runtime(new_runtime)?;
|
||||||
spinner.finish(
|
spinner.finish(
|
||||||
if round == 0 { "✨ Done (after auto-compact)" } else { "✨ Done (after aggressive auto-compact)" },
|
if round == 0 {
|
||||||
|
"✨ Done (after auto-compact)"
|
||||||
|
} else {
|
||||||
|
"✨ Done (after aggressive auto-compact)"
|
||||||
|
},
|
||||||
TerminalRenderer::new().color_theme(),
|
TerminalRenderer::new().color_theme(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
)?;
|
)?;
|
||||||
println!();
|
println!();
|
||||||
if let Some(event) = summary.auto_compaction {
|
if let Some(event) = summary.auto_compaction {
|
||||||
println!("{}", format_auto_compaction_notice(event.removed_message_count));
|
println!(
|
||||||
|
"{}",
|
||||||
|
format_auto_compaction_notice(event.removed_message_count)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.persist_session()?;
|
self.persist_session()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -5143,7 +5166,7 @@ impl LiveCli {
|
|||||||
let still_context_window = retry_str.contains("context_window")
|
let still_context_window = retry_str.contains("context_window")
|
||||||
|| retry_str.contains("Context window")
|
|| retry_str.contains("Context window")
|
||||||
|| retry_str.contains("no parseable body");
|
|| retry_str.contains("no parseable body");
|
||||||
|
|
||||||
if still_context_window && round + 1 < max_compact_rounds {
|
if still_context_window && round + 1 < max_compact_rounds {
|
||||||
// The compacted session was still too large for the model's context.
|
// The compacted session was still too large for the model's context.
|
||||||
// Shut down the old runtime, adopt the partially-compacted one,
|
// Shut down the old runtime, adopt the partially-compacted one,
|
||||||
@@ -5152,14 +5175,14 @@ impl LiveCli {
|
|||||||
runtime = new_runtime;
|
runtime = new_runtime;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a context window error, or out of rounds
|
// Not a context window error, or out of rounds
|
||||||
return Err(Box::new(retry_error));
|
return Err(Box::new(retry_error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not a context window error, return original error
|
// If not a context window error, return original error
|
||||||
Err(Box::new(error))
|
Err(Box::new(error))
|
||||||
}
|
}
|
||||||
@@ -8942,7 +8965,11 @@ impl AnthropicRuntimeClient {
|
|||||||
}
|
}
|
||||||
ApiStreamEvent::ContentBlockStart(start) => {
|
ApiStreamEvent::ContentBlockStart(start) => {
|
||||||
// 特判 Thinking 块:初始化 pending_thinking(用于累积后续 ThinkingDelta)
|
// 特判 Thinking 块:初始化 pending_thinking(用于累积后续 ThinkingDelta)
|
||||||
if let OutputContentBlock::Thinking { thinking, signature } = &start.content_block {
|
if let OutputContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} = &start.content_block
|
||||||
|
{
|
||||||
pending_thinking = Some((thinking.clone(), signature.clone()));
|
pending_thinking = Some((thinking.clone(), signature.clone()));
|
||||||
}
|
}
|
||||||
push_output_block(
|
push_output_block(
|
||||||
@@ -8999,7 +9026,10 @@ impl AnthropicRuntimeClient {
|
|||||||
}
|
}
|
||||||
// 把累积的 thinking 转成 AssistantEvent::Thinking(让 build_assistant_message 写入 session)
|
// 把累积的 thinking 转成 AssistantEvent::Thinking(让 build_assistant_message 写入 session)
|
||||||
if let Some((thinking, signature)) = pending_thinking.take() {
|
if let Some((thinking, signature)) = pending_thinking.take() {
|
||||||
events.push(AssistantEvent::Thinking { thinking, signature });
|
events.push(AssistantEvent::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some((id, name, input)) = pending_tool.take() {
|
if let Some((id, name, input)) = pending_tool.take() {
|
||||||
if let Some(progress_reporter) = &self.progress_reporter {
|
if let Some(progress_reporter) = &self.progress_reporter {
|
||||||
@@ -10137,7 +10167,10 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
|||||||
ContentBlock::Text { text } => {
|
ContentBlock::Text { text } => {
|
||||||
Some(InputContentBlock::Text { text: text.clone() })
|
Some(InputContentBlock::Text { text: text.clone() })
|
||||||
}
|
}
|
||||||
ContentBlock::Thinking { thinking, signature } => {
|
ContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => {
|
||||||
// 保留 Thinking 块:OpenAI 兼容协议会把它转成 reasoning_content 字段
|
// 保留 Thinking 块:OpenAI 兼容协议会把它转成 reasoning_content 字段
|
||||||
// 回传给 DeepSeek V4(避免 400 "reasoning_content must be passed back" 错误)
|
// 回传给 DeepSeek V4(避免 400 "reasoning_content must be passed back" 错误)
|
||||||
Some(InputContentBlock::Thinking {
|
Some(InputContentBlock::Thinking {
|
||||||
@@ -11024,7 +11057,10 @@ mod tests {
|
|||||||
fn resolves_known_model_aliases() {
|
fn resolves_known_model_aliases() {
|
||||||
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-6");
|
assert_eq!(resolve_model_alias("opus"), "anthropic/claude-opus-4-6");
|
||||||
assert_eq!(resolve_model_alias("sonnet"), "anthropic/claude-sonnet-4-6");
|
assert_eq!(resolve_model_alias("sonnet"), "anthropic/claude-sonnet-4-6");
|
||||||
assert_eq!(resolve_model_alias("haiku"), "anthropic/claude-haiku-4-5-20251213");
|
assert_eq!(
|
||||||
|
resolve_model_alias("haiku"),
|
||||||
|
"anthropic/claude-haiku-4-5-20251213"
|
||||||
|
);
|
||||||
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11498,7 +11534,10 @@ mod tests {
|
|||||||
model_flag_raw,
|
model_flag_raw,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(model, "anthropic/claude-sonnet-4-6", "sonnet alias should resolve");
|
assert_eq!(
|
||||||
|
model, "anthropic/claude-sonnet-4-6",
|
||||||
|
"sonnet alias should resolve"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model_flag_raw.as_deref(),
|
model_flag_raw.as_deref(),
|
||||||
Some("sonnet"),
|
Some("sonnet"),
|
||||||
@@ -15220,9 +15259,18 @@ mod alias_resolution_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_alias_resolution_builtin() {
|
fn test_alias_resolution_builtin() {
|
||||||
// Built-in aliases should resolve to their full IDs
|
// Built-in aliases should resolve to their full IDs
|
||||||
assert_eq!(resolve_model_alias_with_config("opus"), "anthropic/claude-opus-4-6");
|
assert_eq!(
|
||||||
assert_eq!(resolve_model_alias_with_config("sonnet"), "anthropic/claude-sonnet-4-6");
|
resolve_model_alias_with_config("opus"),
|
||||||
assert_eq!(resolve_model_alias_with_config("haiku"), "anthropic/claude-haiku-4-5-20251213");
|
"anthropic/claude-opus-4-6"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_model_alias_with_config("sonnet"),
|
||||||
|
"anthropic/claude-sonnet-4-6"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_model_alias_with_config("haiku"),
|
||||||
|
"anthropic/claude-haiku-4-5-20251213"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1937,7 +1937,10 @@ fn run_git_status(input: GitStatusInput) -> Result<String, String> {
|
|||||||
Some(output) => to_pretty_json(json!({
|
Some(output) => to_pretty_json(json!({
|
||||||
"output": output
|
"output": output
|
||||||
})),
|
})),
|
||||||
None => Err("git status failed. Ensure the current directory is inside a git repository.".to_string()),
|
None => Err(
|
||||||
|
"git status failed. Ensure the current directory is inside a git repository."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1965,7 +1968,9 @@ fn run_git_diff(input: GitDiffInput) -> Result<String, String> {
|
|||||||
Some(output) => to_pretty_json(json!({
|
Some(output) => to_pretty_json(json!({
|
||||||
"output": output
|
"output": output
|
||||||
})),
|
})),
|
||||||
None => Err("git diff failed. Ensure the current directory is inside a git repository.".to_string()),
|
None => Err(
|
||||||
|
"git diff failed. Ensure the current directory is inside a git repository.".to_string(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1997,7 +2002,9 @@ fn run_git_log(input: GitLogInput) -> Result<String, String> {
|
|||||||
Some(output) => to_pretty_json(json!({
|
Some(output) => to_pretty_json(json!({
|
||||||
"output": output
|
"output": output
|
||||||
})),
|
})),
|
||||||
None => Err("git log failed. Ensure the current directory is inside a git repository.".to_string()),
|
None => Err(
|
||||||
|
"git log failed. Ensure the current directory is inside a git repository.".to_string(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2019,7 +2026,10 @@ fn run_git_show(input: GitShowInput) -> Result<String, String> {
|
|||||||
Some(output) => to_pretty_json(json!({
|
Some(output) => to_pretty_json(json!({
|
||||||
"output": output
|
"output": output
|
||||||
})),
|
})),
|
||||||
None => Err(format!("git show {} failed. Ensure the commit exists.", input.commit)),
|
None => Err(format!(
|
||||||
|
"git show {} failed. Ensure the commit exists.",
|
||||||
|
input.commit
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user