1use lellm_core::{CacheControl, ChatRequest, LlmError, Message, Prompt, ToolDefinition};
8use lellm_provider::ResolvedModel;
9
10use super::context::ContextBudget;
11use super::fallback::FallbackStrategy;
12use super::request_opts::RequestOptions;
13use super::retry::RetryPolicy;
14use std::sync::Arc;
15
16#[derive(Debug, Clone)]
24pub struct ToolUseConfig {
25 pub system: Option<Prompt>,
31 pub max_iterations: usize,
33 pub max_output_tokens: u32,
41 pub max_total_output_tokens: Option<u32>,
50 pub max_total_reasoning_tokens: Option<u32>,
57 pub context_budget: ContextBudget,
64 pub request_options: RequestOptions,
72 pub stream_thinking: bool,
80 pub tool_cache_policy: ToolCachePolicy,
87 pub retry_policy: RetryPolicy,
89}
90
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
96pub enum ToolCachePolicy {
97 #[default]
100 Auto,
101 Preserve,
103 Disabled,
105}
106
107impl Default for ToolUseConfig {
108 fn default() -> Self {
109 Self {
110 system: None,
111 max_iterations: 10,
112 max_output_tokens: 4_000,
113 max_total_output_tokens: None,
114 max_total_reasoning_tokens: None,
115 context_budget: ContextBudget::default(),
116 request_options: RequestOptions::default(),
117 stream_thinking: false,
118 tool_cache_policy: ToolCachePolicy::default(),
119 retry_policy: RetryPolicy::default(),
120 }
121 }
122}
123
124#[derive(Clone)]
130pub struct ToolUseDeps {
131 pub fallback: Arc<dyn FallbackStrategy>,
133}
134
135impl Default for ToolUseDeps {
136 fn default() -> Self {
137 Self {
138 fallback: Arc::new(super::fallback::DefaultFallback::default()),
139 }
140 }
141}
142
143pub(super) fn has_system_message(messages: &[Message]) -> bool {
147 messages.iter().any(|m| matches!(m, Message::System { .. }))
148}
149
150pub(super) fn build_request_messages_inner(
152 config: &ToolUseConfig,
153 messages: &[Message],
154) -> Result<Vec<Message>, LlmError> {
155 if let Some(ref system) = config.system {
156 if has_system_message(messages) {
157 return Err(LlmError::DuplicateSystemPrompt);
158 }
159 let mut result = vec![system.clone().build()];
160 result.extend(messages.iter().cloned());
161 Ok(result)
162 } else {
163 Ok(messages.to_vec())
164 }
165}
166
167pub(super) fn build_request_inner(
175 model: &ResolvedModel,
176 messages: &[Message],
177 max_output_tokens: u32,
178 request_options: &RequestOptions,
179 definitions: &[ToolDefinition],
180 tool_cache_policy: ToolCachePolicy,
181) -> ChatRequest {
182 let tools = match tool_cache_policy {
183 ToolCachePolicy::Auto => {
184 if definitions.is_empty() {
185 None
186 } else {
187 let last_idx = definitions.len() - 1;
191 Some(
192 definitions
193 .iter()
194 .enumerate()
195 .map(|(i, d)| {
196 if d.cache_control.is_some() {
197 d.clone()
198 } else if i == last_idx {
199 let mut cloned = d.clone();
200 cloned.cache_control = Some(CacheControl::Breakpoint);
201 cloned
202 } else {
203 d.clone()
204 }
205 })
206 .collect(),
207 )
208 }
209 }
210 ToolCachePolicy::Preserve => {
211 if definitions.is_empty() {
212 None
213 } else {
214 Some(definitions.to_vec())
215 }
216 }
217 ToolCachePolicy::Disabled => {
218 if definitions.is_empty() {
219 None
220 } else {
221 Some(
222 definitions
223 .iter()
224 .map(|d| {
225 let mut cloned = d.clone();
226 cloned.cache_control = None;
227 cloned
228 })
229 .collect(),
230 )
231 }
232 }
233 };
234
235 let mut req = ChatRequest {
236 model: model.model.clone(),
237 messages: messages.to_vec(),
238 tools,
239 max_tokens: Some(max_output_tokens),
240 temperature: None,
241 top_p: None,
242 seed: None,
243 tool_choice: None,
244 stop_sequences: None,
245 prefill: None,
246 reasoning: None,
247 max_reasoning_tokens: None,
248 extra: None,
249 };
250
251 request_options.apply(&mut req);
253
254 req
255}
256
257pub(super) fn build_request_inner_with_round(
265 model: &ResolvedModel,
266 messages: &[Message],
267 max_output_tokens: u32,
268 request_options: &RequestOptions,
269 iteration: usize,
270 definitions: &[ToolDefinition],
271 tool_cache_policy: ToolCachePolicy,
272) -> ChatRequest {
273 let mut req = build_request_inner(
274 model,
275 messages,
276 max_output_tokens,
277 request_options,
278 definitions,
279 tool_cache_policy,
280 );
281
282 if iteration > 0 && request_options.tool_choice.is_some() {
285 req.tool_choice = None;
286 }
287
288 req
289}
290
291pub(super) fn empty_response() -> lellm_core::ChatResponse {
293 lellm_core::ChatResponse::new(
294 lellm_core::text_block(String::new()),
295 lellm_core::TokenUsage::default(),
296 serde_json::Value::Null,
297 )
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use lellm_core::Prompt;
304
305 #[test]
306 fn test_build_request_messages_with_prompt() {
307 let config = ToolUseConfig {
308 system: Some(
309 Prompt::new()
310 .stable("核心身份")
311 .stable("工具指南")
312 .dynamic("会话上下文"),
313 ),
314 ..Default::default()
315 };
316
317 let messages: Vec<Message> = vec![Message::user_text("你好")];
318 let result = build_request_messages_inner(&config, &messages).unwrap();
319
320 assert_eq!(result.len(), 2);
322
323 if let Message::System { content } = &result[0] {
326 assert_eq!(content.len(), 3);
327
328 if let lellm_core::ContentBlock::Text(t) = &content[0] {
330 assert_eq!(t.text, "核心身份");
331 assert!(
332 t.cache_control.is_none(),
333 "Intermediate cached layer should NOT have breakpoint"
334 );
335 } else {
336 panic!("expected Text block");
337 }
338
339 if let lellm_core::ContentBlock::Text(t) = &content[1] {
341 assert_eq!(t.text, "工具指南");
342 assert!(
343 t.cache_control.is_some(),
344 "Last cached layer should have breakpoint"
345 );
346 } else {
347 panic!("expected Text block");
348 }
349
350 if let lellm_core::ContentBlock::Text(t) = &content[2] {
352 assert_eq!(t.text, "会话上下文");
353 assert!(t.cache_control.is_none());
354 } else {
355 panic!("expected Text block");
356 }
357 } else {
358 panic!("expected System message");
359 }
360 }
361
362 #[test]
363 fn test_build_request_messages_with_plain_prompt() {
364 let config = ToolUseConfig {
365 system: Some("简单提示".into()),
366 ..Default::default()
367 };
368
369 let messages: Vec<Message> = vec![Message::user_text("你好")];
370 let result = build_request_messages_inner(&config, &messages).unwrap();
371
372 assert_eq!(result.len(), 2);
373 if let Message::System { content } = &result[0] {
374 assert_eq!(content.len(), 1);
375 assert_eq!(content[0].as_text(), Some("简单提示"));
376 } else {
377 panic!("expected System message");
378 }
379 }
380
381 #[test]
382 fn test_build_request_messages_no_system() {
383 let config = ToolUseConfig::default();
384 let messages: Vec<Message> = vec![Message::user_text("你好")];
385 let result = build_request_messages_inner(&config, &messages).unwrap();
386
387 assert_eq!(result.len(), 1);
388 assert!(matches!(result[0], Message::User { .. }));
389 }
390
391 #[test]
392 fn test_duplicate_system_prompt_error() {
393 let config = ToolUseConfig {
394 system: Some("系统提示".into()),
395 ..Default::default()
396 };
397
398 let messages: Vec<Message> =
399 vec![Message::system_text("已有系统"), Message::user_text("你好")];
400 let result = build_request_messages_inner(&config, &messages);
401
402 assert!(result.is_err());
403 }
404
405 #[test]
406 fn test_tool_cache_auto_only_last_breakpoint() {
407 let definitions: Vec<ToolDefinition> = vec![
408 ToolDefinition {
409 name: "search".into(),
410 description: "Search".into(),
411 parameters: serde_json::json!({}),
412 cache_control: None,
413 },
414 ToolDefinition {
415 name: "calculate".into(),
416 description: "Calculate".into(),
417 parameters: serde_json::json!({}),
418 cache_control: None,
419 },
420 ToolDefinition {
421 name: "save".into(),
422 description: "Save".into(),
423 parameters: serde_json::json!({}),
424 cache_control: None,
425 },
426 ];
427
428 let req = build_request_inner(
429 &ResolvedModel {
430 provider: Arc::new(lellm_provider::providers::mock::MockProvider::new(vec![])),
431 model: "test".into(),
432 context_window: Some(100_000),
433 },
434 &[],
435 4096,
436 &RequestOptions::default(),
437 &definitions,
438 ToolCachePolicy::Auto,
439 );
440
441 let tools = req.tools.unwrap();
442 assert_eq!(tools.len(), 3);
443
444 assert!(
446 tools[0].cache_control.is_none(),
447 "First tool should NOT have breakpoint"
448 );
449 assert!(
450 tools[1].cache_control.is_none(),
451 "Second tool should NOT have breakpoint"
452 );
453 assert!(
455 tools[2].cache_control == Some(CacheControl::Breakpoint),
456 "Last tool should have breakpoint"
457 );
458 }
459
460 #[test]
461 fn test_tool_cache_auto_preserves_explicit() {
462 let definitions: Vec<ToolDefinition> = vec![
463 ToolDefinition {
464 name: "search".into(),
465 description: "Search".into(),
466 parameters: serde_json::json!({}),
467 cache_control: None,
468 },
469 ToolDefinition {
470 name: "calculate".into(),
471 description: "Calculate".into(),
472 parameters: serde_json::json!({}),
473 cache_control: Some(CacheControl::Breakpoint),
475 },
476 ];
477
478 let req = build_request_inner(
479 &ResolvedModel {
480 provider: Arc::new(lellm_provider::providers::mock::MockProvider::new(vec![])),
481 model: "test".into(),
482 context_window: Some(100_000),
483 },
484 &[],
485 4096,
486 &RequestOptions::default(),
487 &definitions,
488 ToolCachePolicy::Auto,
489 );
490
491 let tools = req.tools.unwrap();
492 assert!(tools[0].cache_control.is_none());
494 assert!(tools[1].cache_control == Some(CacheControl::Breakpoint));
496 }
497}