返回 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 execution_id: None,
34 id: id.to_string(),
35 name: name.to_string(),
36 input,
37 caller: None,
38 thought_signature: None,
39 },
40 ],
41 }
42 }
43
44 fn tool_result_message(tool_use_id: &str, content: &str) -> Message {
45 Message {
46 role: Role::User,
47 content: vec![ContentBlock::ToolResult {
48 execution_id: None,
49 tool_use_id: tool_use_id.to_string(),
50 content: content.to_string(),
51 is_error: None,
52 content_blocks: None,
53 }],
54 }
55 }
56
57 fn make_request(messages: Vec<Message>) -> MessageRequest {
58 MessageRequest {
59 model: "deepseek-v4-pro".to_string(),
60 messages,
61 max_tokens: 4096,
62 system: None,
63 tools: None,
64 tool_choice: None,
65 metadata: None,
66 thinking: None,
67 reasoning_effort: Some("high".to_string()),
68 stream: Some(true),
69 temperature: None,
70 top_p: None,
71 }
72 }
73
74 #[tokio::test]
75 async fn reasoning_content_is_replayed_after_thinking_tool_call() {
76 let mock = MockLlmClient::new(vec![]);
77
78 mock.push_turn(vec![
79 canned::message_start("r1"),
80 canned::thinking_delta(0, "I should inspect /tmp before answering."),
81 canned::tool_use_block_start(1, "call_a", "list_dir"),
82 canned::tool_input_delta(1, r#"{"path":"/tmp"}"#),
83 canned::block_stop(1),
84 canned::message_delta("tool_use", None),
85 canned::message_stop(),
86 ]);
87
88 mock.push_factory(|request| {
89 let assistant = request
90 .messages
91 .iter()
92 .rev()
93 .find(|message| message.role == "assistant")
94 .expect("follow-up request must include the prior assistant tool-call turn");
95
96 assert!(
97 assistant
98 .content
99 .iter()
100 .any(|block| matches!(block, ContentBlock::Thinking { .. })),
101 "DeepSeek V4 follow-up requests must replay reasoning_content on the assistant tool-call turn"
102 );
103
104 canned::simple_text_turn("I see the /tmp entries.")
105 });
106
107 let mut first = mock
108 .create_message_stream(make_request(vec![user_message("list /tmp")]))
109 .await
110 .expect("first stream opens");
111 while first.next().await.is_some() {}
112
113 let mut second = mock
114 .create_message_stream(make_request(vec![
115 user_message("list /tmp"),
116 assistant_thinking_tool_call(
117 "I should inspect /tmp before answering.",
118 "call_a",
119 "list_dir",
120 serde_json::json!({ "path": "/tmp" }),
121 ),
122 tool_result_message("call_a", "/tmp/file1\n/tmp/file2"),
123 ]))
124 .await
125 .expect("second stream opens");
126 while second.next().await.is_some() {}
127 }
128
128 lines RUST