From 3810cbb5e4a67695cda60f6e5803f0df631436fa Mon Sep 17 00:00:00 2001 From: zhaoyanchao Date: Wed, 29 Apr 2026 17:18:40 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20IntelliJ=20IDEA?= =?UTF-8?q?=20=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 .idea 目录下的 VCS、检查配置、模块设置等 IntelliJ IDEA 项目配置文件,以便在 IDE 中正确管理项目。 --- README.md | 2 +- .../api/src/providers/openai_compat/mod.rs | 100 ++++++++++++++++-- rust/crates/api/src/types.rs | 5 + .../api/tests/openai_compat_integration.rs | 12 +-- rust/crates/claw-cli/src/main.rs | 29 ++++- rust/crates/runtime/src/compact.rs | 6 +- rust/crates/runtime/src/conversation.rs | 23 +++- rust/crates/runtime/src/session.rs | 24 +++++ rust/crates/rusty-claude-cli/src/main.rs | 31 +++++- rust/crates/tools/src/lib.rs | 7 ++ 10 files changed, 213 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0653ee9e..cce8dcd6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 2. 使用deepseek的apiKey, 模型 qwen-plus ``` export DEEPSEEK_API_KEY=sk-your-key - export DEEPSEEK_MODEL="qwen-plus" + export DEEPSEEK_MODEL="deepseek-v4-flash" ./claw ``` \ No newline at end of file diff --git a/rust/crates/api/src/providers/openai_compat/mod.rs b/rust/crates/api/src/providers/openai_compat/mod.rs index 949245e3..ab1b8104 100644 --- a/rust/crates/api/src/providers/openai_compat/mod.rs +++ b/rust/crates/api/src/providers/openai_compat/mod.rs @@ -389,9 +389,14 @@ struct StreamState { message_started: bool, text_started: bool, text_finished: bool, + reasoning_started: bool, + reasoning_finished: bool, finished: bool, stop_reason: Option, usage: Option, + next_block_index: u32, + reasoning_block_index: Option, + text_block_index: Option, tool_calls: BTreeMap, } @@ -402,9 +407,14 @@ impl StreamState { message_started: false, text_started: false, text_finished: false, + reasoning_started: false, + reasoning_finished: false, finished: false, stop_reason: None, usage: None, + next_block_index: 0, + reasoning_block_index: None, + text_block_index: None, tool_calls: BTreeMap::new(), } } @@ -443,20 +453,58 @@ impl StreamState { } for choice in chunk.choices { + if let Some(reasoning) = choice + .delta + .reasoning_content + .filter(|value| !value.is_empty()) + { + if !self.reasoning_started { + self.reasoning_started = true; + self.reasoning_block_index = Some(self.next_block_index); + events.push(StreamEvent::ContentBlockStart(ContentBlockStartEvent { + index: self.next_block_index, + content_block: OutputContentBlock::Thinking { + thinking: String::new(), + signature: None, + }, + })); + self.next_block_index += 1; + } + if let Some(idx) = self.reasoning_block_index { + events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent { + index: idx, + delta: ContentBlockDelta::ThinkingDelta { + thinking: reasoning, + }, + })); + } + } if let Some(content) = choice.delta.content.filter(|value| !value.is_empty()) { + if self.reasoning_started && !self.reasoning_finished { + self.reasoning_finished = true; + if let Some(idx) = self.reasoning_block_index { + events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent { + index: idx, + })); + } + } if !self.text_started { self.text_started = true; + self.text_block_index = Some(self.next_block_index); events.push(StreamEvent::ContentBlockStart(ContentBlockStartEvent { - index: 0, + index: self.next_block_index, content_block: OutputContentBlock::Text { text: String::new(), }, })); + self.next_block_index += 1; + } + if let Some(idx) = self.text_block_index { + events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent { + index: idx, + delta: ContentBlockDelta::TextDelta { text: content }, + })); } - events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent { - index: 0, - delta: ContentBlockDelta::TextDelta { text: content }, - })); } for tool_call in choice.delta.tool_calls { @@ -507,11 +555,22 @@ impl StreamState { self.finished = true; let mut events = Vec::new(); + if self.reasoning_started && !self.reasoning_finished { + self.reasoning_finished = true; + if let Some(idx) = self.reasoning_block_index { + events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent { + index: idx, + })); + } + } + if self.text_started && !self.text_finished { self.text_finished = true; - events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent { - index: 0, - })); + if let Some(idx) = self.text_block_index { + events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent { + index: idx, + })); + } } for state in self.tool_calls.values_mut() { @@ -581,7 +640,8 @@ impl ToolCallState { } const fn block_index(&self) -> u32 { - self.openai_index + 1 + // We use a large offset for tool calls to avoid colliding with text/reasoning blocks + self.openai_index + 100 } #[allow(clippy::unnecessary_wraps)] @@ -641,6 +701,8 @@ struct ChatMessage { content: Option, #[serde(default)] tool_calls: Vec, + #[serde(default)] + reasoning_content: Option, } #[derive(Debug, Deserialize)] @@ -685,6 +747,8 @@ struct ChunkChoice { struct ChunkDelta { #[serde(default)] content: Option, + #[serde(default)] + reasoning_content: Option, #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")] tool_calls: Vec, } @@ -901,10 +965,12 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec { match message.role.as_str() { "assistant" => { let mut text = String::new(); + let mut reasoning = String::new(); let mut tool_calls = Vec::new(); for block in &message.content { match block { InputContentBlock::Text { text: value } => text.push_str(value), + InputContentBlock::Thinking { thinking, .. } => reasoning.push_str(thinking), InputContentBlock::ToolUse { id, name, input } => tool_calls.push(json!({ "id": id, "type": "function", @@ -916,13 +982,16 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec { InputContentBlock::ToolResult { .. } => {} } } - if text.is_empty() && tool_calls.is_empty() { + if text.is_empty() && tool_calls.is_empty() && reasoning.is_empty() { Vec::new() } else { let mut msg = serde_json::json!({ "role": "assistant", "content": (!text.is_empty()).then_some(text), }); + if !reasoning.is_empty() { + msg["reasoning_content"] = json!(reasoning); + } // Only include tool_calls when non-empty: some providers reject // assistant messages with an explicit empty tool_calls array. if !tool_calls.is_empty() { @@ -957,6 +1026,7 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec { Some(msg) } InputContentBlock::ToolUse { .. } => None, + InputContentBlock::Thinking { .. } => None, }) .collect(), } @@ -1135,6 +1205,16 @@ fn normalize_response( "chat completion response missing choices", ))?; let mut content = Vec::new(); + if let Some(thinking) = choice + .message + .reasoning_content + .filter(|value| !value.is_empty()) + { + content.push(OutputContentBlock::Thinking { + thinking, + signature: None, + }); + } if let Some(text) = choice.message.content.filter(|value| !value.is_empty()) { content.push(OutputContentBlock::Text { text }); } diff --git a/rust/crates/api/src/types.rs b/rust/crates/api/src/types.rs index e136a766..89777112 100644 --- a/rust/crates/api/src/types.rs +++ b/rust/crates/api/src/types.rs @@ -81,6 +81,11 @@ pub enum InputContentBlock { Text { text: String, }, + Thinking { + thinking: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + signature: Option, + }, ToolUse { id: String, name: String, diff --git a/rust/crates/api/tests/openai_compat_integration.rs b/rust/crates/api/tests/openai_compat_integration.rs index d5596bb0..150ab9c7 100644 --- a/rust/crates/api/tests/openai_compat_integration.rs +++ b/rust/crates/api/tests/openai_compat_integration.rs @@ -187,38 +187,38 @@ async fn stream_message_normalizes_text_and_multiple_tool_calls() { assert!(matches!( events[3], StreamEvent::ContentBlockStart(ContentBlockStartEvent { - index: 1, + index: 100, content_block: OutputContentBlock::ToolUse { .. }, }) )); assert!(matches!( events[4], StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent { - index: 1, + index: 100, delta: ContentBlockDelta::InputJsonDelta { .. }, }) )); assert!(matches!( events[5], StreamEvent::ContentBlockStart(ContentBlockStartEvent { - index: 2, + index: 101, content_block: OutputContentBlock::ToolUse { .. }, }) )); assert!(matches!( events[6], StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent { - index: 2, + index: 101, delta: ContentBlockDelta::InputJsonDelta { .. }, }) )); assert!(matches!( events[7], - StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 1 }) + StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 100 }) )); assert!(matches!( events[8], - StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 2 }) + StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 101 }) )); assert!(matches!( events[9], diff --git a/rust/crates/claw-cli/src/main.rs b/rust/crates/claw-cli/src/main.rs index 2b7d6f17..c4d729ae 100644 --- a/rust/crates/claw-cli/src/main.rs +++ b/rust/crates/claw-cli/src/main.rs @@ -3158,8 +3158,12 @@ impl ApiClient for DefaultRuntimeClient { input.push_str(&partial_json); } } - ContentBlockDelta::ThinkingDelta { .. } - | ContentBlockDelta::SignatureDelta { .. } => {} + ContentBlockDelta::ThinkingDelta { thinking } => { + events.push(AssistantEvent::ThinkingDelta(thinking)); + } + ContentBlockDelta::SignatureDelta { signature } => { + events.push(AssistantEvent::SignatureDelta(signature)); + } }, ApiStreamEvent::ContentBlockStop(_) => { if let Some(rendered) = markdown_stream.flush(&renderer) { @@ -3819,7 +3823,15 @@ fn push_output_block( }; *pending_tool = Some((id, name, initial_input)); } - OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {} + OutputContentBlock::Thinking { thinking, signature } => { + if !thinking.is_empty() { + events.push(AssistantEvent::ThinkingDelta(thinking)); + } + if let Some(sig) = signature { + events.push(AssistantEvent::SignatureDelta(sig)); + } + } + OutputContentBlock::RedactedThinking { .. } => {} } Ok(()) } @@ -5050,7 +5062,7 @@ mod tests { } #[test] - fn response_to_events_ignores_thinking_blocks() { + fn response_to_events_preserves_thinking_blocks() { let mut out = Vec::new(); let events = response_to_events( MessageResponse { @@ -5083,8 +5095,15 @@ mod tests { assert!(matches!( &events[0], + AssistantEvent::ThinkingDelta(text) if text == "step 1" + )); + assert!(matches!( + &events[1], + AssistantEvent::SignatureDelta(sig) if sig == "sig_123" + )); + assert!(matches!( + &events[2], AssistantEvent::TextDelta(text) if text == "Final answer" )); - assert!(!String::from_utf8(out).expect("utf8").contains("step 1")); } } diff --git a/rust/crates/runtime/src/compact.rs b/rust/crates/runtime/src/compact.rs index 3e805dda..813371aa 100644 --- a/rust/crates/runtime/src/compact.rs +++ b/rust/crates/runtime/src/compact.rs @@ -212,7 +212,7 @@ fn summarize_messages(messages: &[ConversationMessage]) -> String { .filter_map(|block| match block { ContentBlock::ToolUse { name, .. } => Some(name.as_str()), ContentBlock::ToolResult { tool_name, .. } => Some(tool_name.as_str()), - ContentBlock::Text { .. } => None, + ContentBlock::Text { .. } | ContentBlock::Thinking { .. } => None, }) .collect::>(); tool_names.sort_unstable(); @@ -327,6 +327,7 @@ fn summarize_block(block: &ContentBlock) -> String { "tool_result {tool_name}: {}{output}", if *is_error { "error " } else { "" } ), + ContentBlock::Thinking { thinking, .. } => format!("thinking {thinking}"), }; truncate_summary(&raw, 160) } @@ -378,6 +379,7 @@ fn collect_key_files(messages: &[ConversationMessage]) -> Vec { ContentBlock::Text { text } => text.as_str(), ContentBlock::ToolUse { input, .. } => input.as_str(), ContentBlock::ToolResult { output, .. } => output.as_str(), + ContentBlock::Thinking { thinking, .. } => thinking.as_str(), }) .flat_map(extract_file_candidates) .collect::>(); @@ -400,6 +402,7 @@ fn first_text_block(message: &ConversationMessage) -> Option<&str> { ContentBlock::Text { text } if !text.trim().is_empty() => Some(text.as_str()), ContentBlock::ToolUse { .. } | ContentBlock::ToolResult { .. } + | ContentBlock::Thinking { .. } | ContentBlock::Text { .. } => None, }) } @@ -450,6 +453,7 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize { ContentBlock::ToolResult { tool_name, output, .. } => (tool_name.len() + output.len()) / 4 + 1, + ContentBlock::Thinking { thinking, .. } => thinking.len() / 4 + 1, }) .sum() } diff --git a/rust/crates/runtime/src/conversation.rs b/rust/crates/runtime/src/conversation.rs index c32a9127..47d5cce1 100644 --- a/rust/crates/runtime/src/conversation.rs +++ b/rust/crates/runtime/src/conversation.rs @@ -29,6 +29,8 @@ pub struct ApiRequest { #[derive(Debug, Clone, PartialEq, Eq)] pub enum AssistantEvent { TextDelta(String), + ThinkingDelta(String), + SignatureDelta(String), ToolUse { id: String, name: String, @@ -760,15 +762,33 @@ fn build_assistant_message( RuntimeError, > { let mut text = String::new(); + let mut thinking = String::new(); + let mut signature = None; let mut blocks = Vec::new(); let mut prompt_cache_events = Vec::new(); let mut finished = false; let mut usage = None; + let flush_thinking_block = + |thinking: &mut String, signature: &mut Option, blocks: &mut Vec| { + if !thinking.is_empty() { + blocks.push(ContentBlock::Thinking { + thinking: std::mem::take(thinking), + signature: signature.take(), + }); + } + }; + for event in events { match event { - AssistantEvent::TextDelta(delta) => text.push_str(&delta), + AssistantEvent::TextDelta(delta) => { + flush_thinking_block(&mut thinking, &mut signature, &mut blocks); + text.push_str(&delta); + } + AssistantEvent::ThinkingDelta(delta) => thinking.push_str(&delta), + AssistantEvent::SignatureDelta(delta) => signature = Some(delta), AssistantEvent::ToolUse { id, name, input } => { + flush_thinking_block(&mut thinking, &mut signature, &mut blocks); flush_text_block(&mut text, &mut blocks); blocks.push(ContentBlock::ToolUse { id, name, input }); } @@ -780,6 +800,7 @@ fn build_assistant_message( } } + flush_thinking_block(&mut thinking, &mut signature, &mut blocks); flush_text_block(&mut text, &mut blocks); if !finished { diff --git a/rust/crates/runtime/src/session.rs b/rust/crates/runtime/src/session.rs index b97378e5..87697521 100644 --- a/rust/crates/runtime/src/session.rs +++ b/rust/crates/runtime/src/session.rs @@ -30,6 +30,10 @@ pub enum ContentBlock { Text { text: String, }, + Thinking { + thinking: String, + signature: Option, + }, ToolUse { id: String, name: String, @@ -737,6 +741,19 @@ impl ContentBlock { object.insert("type".to_string(), JsonValue::String("text".to_string())); object.insert("text".to_string(), JsonValue::String(text.clone())); } + Self::Thinking { + thinking, + signature, + } => { + object.insert( + "type".to_string(), + JsonValue::String("thinking".to_string()), + ); + object.insert("thinking".to_string(), JsonValue::String(thinking.clone())); + if let Some(sig) = signature { + object.insert("signature".to_string(), JsonValue::String(sig.clone())); + } + } Self::ToolUse { id, name, input } => { object.insert( "type".to_string(), @@ -783,6 +800,13 @@ impl ContentBlock { "text" => Ok(Self::Text { text: required_string(object, "text")?, }), + "thinking" => Ok(Self::Thinking { + thinking: required_string(object, "thinking")?, + signature: object + .get("signature") + .and_then(JsonValue::as_str) + .map(ToOwned::to_owned), + }), "tool_use" => Ok(Self::ToolUse { id: required_string(object, "id")?, name: required_string(object, "name")?, diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 0100d534..903cd2aa 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -6645,6 +6645,12 @@ fn render_export_text(session: &Session) -> String { "[tool_result id={tool_use_id} name={tool_name} error={is_error}] {output}" )); } + ContentBlock::Thinking { + thinking, + signature, + } => { + lines.push(format!("[thinking signature={signature:?}] {thinking}")); + } } } lines.push(String::new()); @@ -6846,6 +6852,17 @@ fn render_session_markdown(session: &Session, session_id: &str, session_path: &P } lines.push(String::new()); } + ContentBlock::Thinking { + thinking, + signature, + } => { + lines.push(format!("**Thinking** _{signature:?}_")); + let summary = summarize_tool_payload_for_markdown(thinking); + if !summary.is_empty() { + lines.push(format!("> {summary}")); + } + lines.push(String::new()); + } } } if let Some(usage) = message.usage { @@ -7691,13 +7708,16 @@ impl AnthropicRuntimeClient { input.push_str(&partial_json); } } - ContentBlockDelta::ThinkingDelta { .. } => { + ContentBlockDelta::ThinkingDelta { thinking } => { if !block_has_thinking_summary { render_thinking_block_summary(out, None, false)?; block_has_thinking_summary = true; } + events.push(AssistantEvent::ThinkingDelta(thinking)); + } + ContentBlockDelta::SignatureDelta { signature } => { + events.push(AssistantEvent::SignatureDelta(signature)); } - ContentBlockDelta::SignatureDelta { .. } => {} }, ApiStreamEvent::ContentBlockStop(_) => { block_has_thinking_summary = false; @@ -8840,6 +8860,13 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec { .iter() .map(|block| match block { ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() }, + ContentBlock::Thinking { + thinking, + signature, + } => InputContentBlock::Thinking { + thinking: thinking.clone(), + signature: signature.clone(), + }, ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse { id: id.clone(), name: name.clone(), diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index f3d1849a..de33dcde 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -4777,6 +4777,13 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec { }], is_error: *is_error, }, + ContentBlock::Thinking { + thinking, + signature, + } => InputContentBlock::Thinking { + thinking: thinking.clone(), + signature: signature.clone(), + }, }) .collect::>(); (!content.is_empty()).then(|| InputMessage {