use lellm_core::{CacheControl, ChatRequest, LlmError, Message, Prompt, ToolDefinition};
use lellm_provider::ResolvedModel;
use super::context::ContextBudget;
use super::fallback::FallbackStrategy;
use super::request_opts::RequestOptions;
use super::retry::RetryPolicy;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ToolUseConfig {
pub system: Option<Prompt>,
pub max_iterations: usize,
pub max_output_tokens: u32,
pub max_total_output_tokens: Option<u32>,
pub max_total_reasoning_tokens: Option<u32>,
pub context_budget: ContextBudget,
pub request_options: RequestOptions,
pub stream_thinking: bool,
pub tool_cache_policy: ToolCachePolicy,
pub retry_policy: RetryPolicy,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ToolCachePolicy {
#[default]
Auto,
Preserve,
Disabled,
}
impl Default for ToolUseConfig {
fn default() -> Self {
Self {
system: None,
max_iterations: 10,
max_output_tokens: 4_000,
max_total_output_tokens: None,
max_total_reasoning_tokens: None,
context_budget: ContextBudget::default(),
request_options: RequestOptions::default(),
stream_thinking: false,
tool_cache_policy: ToolCachePolicy::default(),
retry_policy: RetryPolicy::default(),
}
}
}
#[derive(Clone)]
pub struct ToolUseDeps {
pub fallback: Arc<dyn FallbackStrategy>,
}
impl Default for ToolUseDeps {
fn default() -> Self {
Self {
fallback: Arc::new(super::fallback::DefaultFallback::default()),
}
}
}
pub(super) fn has_system_message(messages: &[Message]) -> bool {
messages.iter().any(|m| matches!(m, Message::System { .. }))
}
pub(super) fn build_request_messages_inner(
config: &ToolUseConfig,
messages: &[Message],
) -> Result<Vec<Message>, LlmError> {
if let Some(ref system) = config.system {
if has_system_message(messages) {
return Err(LlmError::DuplicateSystemPrompt);
}
let mut result = vec![system.clone().build()];
result.extend(messages.iter().cloned());
Ok(result)
} else {
Ok(messages.to_vec())
}
}
pub(super) fn build_request_inner(
model: &ResolvedModel,
messages: &[Message],
max_output_tokens: u32,
request_options: &RequestOptions,
definitions: &[ToolDefinition],
tool_cache_policy: ToolCachePolicy,
) -> ChatRequest {
let tools = match tool_cache_policy {
ToolCachePolicy::Auto => {
if definitions.is_empty() {
None
} else {
let last_idx = definitions.len() - 1;
Some(
definitions
.iter()
.enumerate()
.map(|(i, d)| {
if d.cache_control.is_some() {
d.clone()
} else if i == last_idx {
let mut cloned = d.clone();
cloned.cache_control = Some(CacheControl::Breakpoint);
cloned
} else {
d.clone()
}
})
.collect(),
)
}
}
ToolCachePolicy::Preserve => {
if definitions.is_empty() {
None
} else {
Some(definitions.to_vec())
}
}
ToolCachePolicy::Disabled => {
if definitions.is_empty() {
None
} else {
Some(
definitions
.iter()
.map(|d| {
let mut cloned = d.clone();
cloned.cache_control = None;
cloned
})
.collect(),
)
}
}
};
let mut req = ChatRequest {
model: model.model.clone(),
messages: messages.to_vec(),
tools,
max_tokens: Some(max_output_tokens),
temperature: None,
top_p: None,
seed: None,
tool_choice: None,
stop_sequences: None,
prefill: None,
reasoning: None,
max_reasoning_tokens: None,
extra: None,
};
request_options.apply(&mut req);
req
}
pub(super) fn build_request_inner_with_round(
model: &ResolvedModel,
messages: &[Message],
max_output_tokens: u32,
request_options: &RequestOptions,
iteration: usize,
definitions: &[ToolDefinition],
tool_cache_policy: ToolCachePolicy,
) -> ChatRequest {
let mut req = build_request_inner(
model,
messages,
max_output_tokens,
request_options,
definitions,
tool_cache_policy,
);
if iteration > 0 && request_options.tool_choice.is_some() {
req.tool_choice = None;
}
req
}
pub(super) fn empty_response() -> lellm_core::ChatResponse {
lellm_core::ChatResponse::new(
lellm_core::text_block(String::new()),
lellm_core::TokenUsage::default(),
serde_json::Value::Null,
)
}
#[cfg(test)]
mod tests {
use super::*;
use lellm_core::Prompt;
#[test]
fn test_build_request_messages_with_prompt() {
let config = ToolUseConfig {
system: Some(
Prompt::new()
.stable("核心身份")
.stable("工具指南")
.dynamic("会话上下文"),
),
..Default::default()
};
let messages: Vec<Message> = vec![Message::user_text("你好")];
let result = build_request_messages_inner(&config, &messages).unwrap();
assert_eq!(result.len(), 2);
if let Message::System { content } = &result[0] {
assert_eq!(content.len(), 3);
if let lellm_core::ContentBlock::Text(t) = &content[0] {
assert_eq!(t.text, "核心身份");
assert!(
t.cache_control.is_none(),
"Intermediate cached layer should NOT have breakpoint"
);
} else {
panic!("expected Text block");
}
if let lellm_core::ContentBlock::Text(t) = &content[1] {
assert_eq!(t.text, "工具指南");
assert!(
t.cache_control.is_some(),
"Last cached layer should have breakpoint"
);
} else {
panic!("expected Text block");
}
if let lellm_core::ContentBlock::Text(t) = &content[2] {
assert_eq!(t.text, "会话上下文");
assert!(t.cache_control.is_none());
} else {
panic!("expected Text block");
}
} else {
panic!("expected System message");
}
}
#[test]
fn test_build_request_messages_with_plain_prompt() {
let config = ToolUseConfig {
system: Some("简单提示".into()),
..Default::default()
};
let messages: Vec<Message> = vec![Message::user_text("你好")];
let result = build_request_messages_inner(&config, &messages).unwrap();
assert_eq!(result.len(), 2);
if let Message::System { content } = &result[0] {
assert_eq!(content.len(), 1);
assert_eq!(content[0].as_text(), Some("简单提示"));
} else {
panic!("expected System message");
}
}
#[test]
fn test_build_request_messages_no_system() {
let config = ToolUseConfig::default();
let messages: Vec<Message> = vec![Message::user_text("你好")];
let result = build_request_messages_inner(&config, &messages).unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0], Message::User { .. }));
}
#[test]
fn test_duplicate_system_prompt_error() {
let config = ToolUseConfig {
system: Some("系统提示".into()),
..Default::default()
};
let messages: Vec<Message> =
vec![Message::system_text("已有系统"), Message::user_text("你好")];
let result = build_request_messages_inner(&config, &messages);
assert!(result.is_err());
}
#[test]
fn test_tool_cache_auto_only_last_breakpoint() {
let definitions: Vec<ToolDefinition> = vec![
ToolDefinition {
name: "search".into(),
description: "Search".into(),
parameters: serde_json::json!({}),
cache_control: None,
},
ToolDefinition {
name: "calculate".into(),
description: "Calculate".into(),
parameters: serde_json::json!({}),
cache_control: None,
},
ToolDefinition {
name: "save".into(),
description: "Save".into(),
parameters: serde_json::json!({}),
cache_control: None,
},
];
let req = build_request_inner(
&ResolvedModel {
provider: Arc::new(lellm_provider::providers::mock::MockProvider::new(vec![])),
model: "test".into(),
context_window: Some(100_000),
},
&[],
4096,
&RequestOptions::default(),
&definitions,
ToolCachePolicy::Auto,
);
let tools = req.tools.unwrap();
assert_eq!(tools.len(), 3);
assert!(
tools[0].cache_control.is_none(),
"First tool should NOT have breakpoint"
);
assert!(
tools[1].cache_control.is_none(),
"Second tool should NOT have breakpoint"
);
assert!(
tools[2].cache_control == Some(CacheControl::Breakpoint),
"Last tool should have breakpoint"
);
}
#[test]
fn test_tool_cache_auto_preserves_explicit() {
let definitions: Vec<ToolDefinition> = vec![
ToolDefinition {
name: "search".into(),
description: "Search".into(),
parameters: serde_json::json!({}),
cache_control: None,
},
ToolDefinition {
name: "calculate".into(),
description: "Calculate".into(),
parameters: serde_json::json!({}),
cache_control: Some(CacheControl::Breakpoint),
},
];
let req = build_request_inner(
&ResolvedModel {
provider: Arc::new(lellm_provider::providers::mock::MockProvider::new(vec![])),
model: "test".into(),
context_window: Some(100_000),
},
&[],
4096,
&RequestOptions::default(),
&definitions,
ToolCachePolicy::Auto,
);
let tools = req.tools.unwrap();
assert!(tools[0].cache_control.is_none());
assert!(tools[1].cache_control == Some(CacheControl::Breakpoint));
}
}