返回 CodeWhale
reasoning_content_replayed_after_tool_call.rs
根目录 / crates / tui / tests / integration / reasoning_content_replayed_after_tool_call.rs
1 use futures_util::StreamExt;
2
3 use crate::llm_client::LlmClient;
4 use crate::llm_client::mock::{MockLlmClient, canned};
5 use codewhale_models::Role;
6 use codewhale_models::{ContentBlock, Message, MessageRequest};
7
8 fn user_message(text: &str) -> Message {
9 Message {
10 role: Role::User,
11 content: vec![ContentBlock::Text {
12 text: text.to_string(),
13 cache_control: None,
14 }],
15 }
16 }
17
18 fn assistant_thinking_tool_call(
19 thinking: &str,
20 id: &str,
21 name: &str,
22 input: serde_json::Value,
23 ) -> Message {
24 Message {
25 role: Role::Assistant,
26 content: vec![
27 ContentBlock::Thinking {
28 thinking: thinking.to_string(),
29 signature: None,
30 state: None,
31 },
32 ContentBlock::ToolUse {
33 id: id.to_string(),
34 name: name.to_string(),
35 input,
36 caller: None,
37 thought_signature: None,
38 },
39 ],
40 }
41 }
42
43 fn tool_result_message(tool_use_id: &str, content: &str) -> Message {
44 Message {
45 role: Role::User,
46 content: vec![ContentBlock::ToolResult {
47 tool_use_id: tool_use_id.to_string(),
48 content: content.to_string(),
49 is_error: None,
50 content_blocks: None,
51 }],
52 }
53 }
54
55 fn make_request(messages: Vec<Message>) -> MessageRequest {
56 MessageRequest {
57 model: "deepseek-v4-pro".to_string(),
58 messages,
59 max_tokens: 4096,
60 system: None,
61 tools: None,
62 tool_choice: None,
63 metadata: None,
64 thinking: None,
65 reasoning_effort: Some("high".to_string()),
66 stream: Some(true),
67 temperature: None,
68 top_p: None,
69 }
70 }
71
72 #[tokio::test]
73 async fn reasoning_content_is_replayed_after_thinking_tool_call() {
74 let mock = MockLlmClient::new(vec![]);
75
76 mock.push_turn(vec![
77 canned::message_start("r1"),
78 canned::thinking_delta(0, "I should inspect /tmp before answering."),
79 canned::tool_use_block_start(1, "call_a", "list_dir"),
80 canned::tool_input_delta(1, r#"{"path":"/tmp"}"#),
81 canned::block_stop(1),
82 canned::message_delta("tool_use", None),
83 canned::message_stop(),
84 ]);
85
86 mock.push_factory(|request| {
87 let assistant = request
88 .messages
89 .iter()
90 .rev()
91 .find(|message| message.role == "assistant")
92 .expect("follow-up request must include the prior assistant tool-call turn");
93
94 assert!(
95 assistant
96 .content
97 .iter()
98 .any(|block| matches!(block, ContentBlock::Thinking { .. })),
99 "DeepSeek V4 follow-up requests must replay reasoning_content on the assistant tool-call turn"
100 );
101
102 canned::simple_text_turn("I see the /tmp entries.")
103 });
104
105 let mut first = mock
106 .create_message_stream(make_request(vec![user_message("list /tmp")]))
107 .await
108 .expect("first stream opens");
109 while first.next().await.is_some() {}
110
111 let mut second = mock
112 .create_message_stream(make_request(vec![
113 user_message("list /tmp"),
114 assistant_thinking_tool_call(
115 "I should inspect /tmp before answering.",
116 "call_a",
117 "list_dir",
118 serde_json::json!({ "path": "/tmp" }),
119 ),
120 tool_result_message("call_a", "/tmp/file1\n/tmp/file2"),
121 ]))
122 .await
123 .expect("second stream opens");
124 while second.next().await.is_some() {}
125 }
126
126 lines RUST