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![Message::System {
content: system.to_content_blocks(),
}];
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 {
Some(
definitions
.iter()
.map(|d| {
if d.cache_control.is_none() {
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::builder()
.layer_cached("核心身份")
.layer_cached("工具指南")
.layer_dynamic("会话上下文")
.build(),
),
..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_some());
} else {
panic!("expected Text block");
}
if let lellm_core::ContentBlock::Text(t) = &content[1] {
assert_eq!(t.text, "工具指南");
assert!(t.cache_control.is_some());
} 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());
}
}