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