#![ cfg( all( feature = "tool_calling", feature = "integration_tests" ) ) ]
mod server_helpers;
use api_ollama::{ OllamaClient, ChatRequest, ChatMessage, MessageRole, ToolDefinition, ToolCall, ToolMessage };
#[ tokio::test ]
async fn test_tool_calling_basic_function()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let calculator_tool = ToolDefinition
{
name : "calculate".to_string(),
description : "Perform basic arithmetic operations".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform"
},
"a": {
"type": "number",
"description": "First number"
},
"b": {
"type": "number",
"description": "Second number"
}
},
"required": ["operation", "a", "b"]
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Calculate 15 + 23".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![calculator_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(response) =>
{
assert!(response.message.tool_calls.is_some(), "Response should contain tool calls");
let tool_calls = response.message.tool_calls.unwrap();
assert!(!tool_calls.is_empty(), "Should have at least one tool call");
let first_call = &tool_calls[0];
assert_eq!(first_call.function["name"].as_str().unwrap(), "calculate", "Tool call should be for calculator");
let args = &first_call.function["arguments"];
assert!(args["operation"].is_string(), "Operation should be a string");
assert!(args["a"].is_number(), "First argument should be a number");
assert!(args["b"].is_number(), "Second argument should be a number");
println!( "✓ Basic tool calling successful" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
if error_str.contains("tool") || error_str.contains("support") || error_str.contains("400")
{
println!( "✓ Model doesn't support tools (expected): {error_str}" );
}
else
{
panic!("Unexpected error in tool calling : {error:?}");
}
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_multiple_tools()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let weather_tool = ToolDefinition
{
name : "get_weather".to_string(),
description : "Get current weather information for a location".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}),
};
let time_tool = ToolDefinition
{
name : "get_current_time".to_string(),
description : "Get the current time in a specific timezone".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone, e.g. America/New_York"
}
},
"required": ["timezone"]
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "What's the weather like in New York and what time is it there?".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![weather_tool, time_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Multiple tools request successful" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
if error_str.contains("tool") || error_str.contains("support") || error_str.contains("400")
{
println!( "✓ Model doesn't support tools (expected): {error_str}" );
}
else
{
panic!("Unexpected error in multiple tools : {error:?}");
}
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_with_response()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let calculator_tool = ToolDefinition
{
name : "calculate".to_string(),
description : "Perform arithmetic operations".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate"
}
},
"required": ["expression"]
}),
};
let user_message = ChatMessage
{
role : MessageRole::User,
content : "What is 25 * 4?".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let tool_response = ToolMessage
{
role : MessageRole::Tool,
content : "100".to_string(),
tool_call_id : "call_123".to_string(),
};
let assistant_message = ChatMessage
{
role : MessageRole::Assistant,
content : "I'll calculate that for you.".to_string(),
images : None,
tool_calls : Some(vec![ToolCall {
id : "call_123".to_string(),
function : serde_json::json!({
"name": "calculate",
"arguments": "{\"expression\": \"25 * 4\"}"
}),
}]),
};
let request = ChatRequest
{
model,
messages : vec![user_message, assistant_message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![calculator_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : Some(vec![tool_response]),
};
let result = client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Tool response request successful" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
if error_str.contains("tool") || error_str.contains("support") || error_str.contains("400")
{
println!( "✓ Model doesn't support tools (expected): {error_str}" );
}
else
{
panic!("Unexpected error in tool response : {error:?}");
}
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_invalid_schema()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let invalid_tool = ToolDefinition
{
name : "invalid_tool".to_string(),
description : "Tool with invalid schema".to_string(),
parameters : serde_json::json!({
"type": "invalid_type", "properties": {},
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Use the invalid tool".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![invalid_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(_) =>
{
println!( "✓ Invalid tool schema handled gracefully" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
assert!(
error_str.contains("invalid") || error_str.contains("schema") || error_str.contains("tool") || error_str.contains("400"),
"Error should mention invalid, schema, tool, or 400 Bad Request : {error_str}"
);
println!( "✓ Invalid tool schema error handling : {error_str}" );
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_streaming()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let simple_tool = ToolDefinition
{
name : "get_info".to_string(),
description : "Get information".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Information query"
}
},
"required": ["query"]
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Get info about Rust programming".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(true), options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![simple_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Tool calling with streaming successful" );
},
Err(_) =>
{
println!( "✓ Tool calling streaming limitation detected (expected)" );
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_no_tools_available()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let message = ChatMessage
{
role : MessageRole::User,
content : "What is 10 + 5?".to_string(), images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : None, #[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
assert!(result.is_ok(), "Request without tools should succeed : {result:?}");
let response = result.unwrap();
assert!(response.message.tool_calls.is_none() || response.message.tool_calls.as_ref().unwrap().is_empty(),
"Should not have tool calls when no tools provided");
assert!(!response.message.content.is_empty(), "Should have text response");
println!( "✓ No tools available handling successful" );
});
}
#[ tokio::test ]
async fn test_tool_calling_complex_parameters()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let complex_tool = ToolDefinition
{
name : "process_data".to_string(),
description : "Process complex data structure".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"value": { "type": "number" },
"tags": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["name", "value"]
}
},
"metadata": {
"type": "object",
"properties": {
"source": { "type": "string" },
"timestamp": { "type": "string" }
}
}
},
"required": ["items"]
}
},
"required": ["data"]
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Process the sales data with items for Q1 results".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![complex_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Complex parameters request successful" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
if error_str.contains("tool") || error_str.contains("support") || error_str.contains("400")
{
println!( "✓ Model doesn't support tools (expected): {error_str}" );
}
else
{
panic!("Unexpected error in complex parameters : {error:?}");
}
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_non_tool_model()
{
with_test_server!(|mut client : OllamaClient, model : String| async move {
let simple_tool = ToolDefinition
{
name : "test_function".to_string(),
description : "Test function".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {},
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Use the test function".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model, messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![simple_tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Non-tool model handled tools gracefully" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
assert!(
error_str.contains("tool") || error_str.contains("unsupported") || error_str.contains("model") || error_str.contains("400"),
"Error should mention tool, unsupported, model, or 400 Bad Request : {error_str}"
);
println!( "✓ Non-tool model error handling : {error_str}" );
}
}
});
}
#[ tokio::test ]
async fn test_tool_calling_authentication()
{
#[ cfg( feature = "secret_management" ) ]
{
use api_ollama::SecretStore;
with_test_server!(|client : OllamaClient, model : String| async move {
let mut secret_store = SecretStore::new();
secret_store.set("api_key", "test-tool-api-key").expect("Failed to store test API key");
let mut auth_client = client.with_secret_store(secret_store);
let tool = ToolDefinition
{
name : "auth_test".to_string(),
description : "Test tool with authentication".to_string(),
parameters : serde_json::json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Test message"
}
},
"required": ["message"]
}),
};
let message = ChatMessage
{
role : MessageRole::User,
content : "Test authenticated tool calling".to_string(),
images : None,
#[ cfg( feature = "tool_calling" ) ]
tool_calls : None,
};
let request = ChatRequest
{
model,
messages : vec![message],
stream : Some(false),
options : None,
#[ cfg( feature = "tool_calling" ) ]
tools : Some(vec![tool]),
#[ cfg( feature = "tool_calling" ) ]
tool_messages : None,
};
let result = auth_client.chat(request).await;
match result
{
Ok(_response) =>
{
println!( "✓ Tool calling with authentication successful" );
},
Err(error) =>
{
let error_str = format!( "{error}" );
if error_str.contains("tool") || error_str.contains("support") || error_str.contains("400")
{
println!( "✓ Model doesn't support tools (expected): {error_str}" );
}
else
{
panic!("Unexpected error : {error:?}");
}
}
}
});
}
#[ cfg( not( feature = "secret_management" ) ) ]
{
println!( "⚠ Skipping authentication test - secret_management feature not enabled" );
}
}