use dynamo_llm::protocols::openai::{
chat_completions::NvCreateChatCompletionRequest, common_ext::CommonExt,
};
use dynamo_parsers::{ToolCallResponse, ToolCallType, detect_and_parse_tool_call};
use serde_json::json;
fn create_mock_chat_completion_request() -> NvCreateChatCompletionRequest {
let messages = vec![
dynamo_protocols::types::ChatCompletionRequestMessage::System(
dynamo_protocols::types::ChatCompletionRequestSystemMessage {
content: dynamo_protocols::types::ChatCompletionRequestSystemMessageContent::Text(
"You MUST use two tools in parallel to resolve the user request: call get_current_weather for each city AND call is_holiday_today to check if today is a holiday. Do not answer without using both tools.".to_string()
),
name: None,
}
),
dynamo_protocols::types::ChatCompletionRequestMessage::User(
dynamo_protocols::types::ChatCompletionRequestUserMessage {
content: dynamo_protocols::types::ChatCompletionRequestUserMessageContent::Text(
"What is the weather in Dallas, Texas? Is today a holiday?".to_string()
),
name: None,
}
),
];
let tools = vec![
dynamo_protocols::types::ChatCompletionTool {
r#type: dynamo_protocols::types::ChatCompletionToolType::Function,
function: dynamo_protocols::types::FunctionObject {
name: "get_current_weather".to_string(),
description: Some("Get weather for a city/state in specified units".to_string()),
parameters: Some(json!({
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g., Dallas" },
"state": { "type": "string", "description": "Two-letter state code, e.g., TX" },
"unit": { "type": "string", "enum": ["fahrenheit", "celsius"] }
},
"required": ["city", "state", "unit"],
"additionalProperties": false
})),
strict: None,
},
},
dynamo_protocols::types::ChatCompletionTool {
r#type: dynamo_protocols::types::ChatCompletionToolType::Function,
function: dynamo_protocols::types::FunctionObject {
name: "is_holiday_today".to_string(),
description: Some("Return whether today is a public holiday".to_string()),
parameters: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
strict: None,
},
},
];
let inner = dynamo_protocols::types::CreateChatCompletionRequestArgs::default()
.model("Qwen/Qwen3-0.6B")
.temperature(0.0)
.max_tokens(3000u32)
.stream(false)
.messages(messages)
.tools(tools)
.tool_choice(dynamo_protocols::types::ChatCompletionToolChoiceOption::Required)
.build()
.expect("Failed to build chat completion request");
NvCreateChatCompletionRequest {
inner,
common: CommonExt::default(),
nvext: None,
chat_template_args: None,
media_io_kwargs: None,
unsupported_fields: Default::default(),
}
}
fn get_mock_response_content() -> String {
r#"<think>Okay, the user is asking two things: the weather in Dallas, Texas, and whether today is a holiday. I need to use both tools here. First, I'll check the weather using get_current_weather with city Dallas and state Texas. Then, I'll use is_holiday_today to see if today is a public holiday. I have to make sure to call both functions in parallel. Let me structure the tool calls properly.</think>
<tool_call>
{"name": "get_current_weather", "arguments": {"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}
</tool_call>
<tool_call>
{"name": "is_holiday_today", "arguments": {}}
</tool_call>"#.to_string()
}
fn validate_weather_tool_call(tool_call: &ToolCallResponse) {
assert_eq!(tool_call.function.name, "get_current_weather");
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
.expect("Arguments should be valid JSON");
let args_obj = args.as_object().expect("Arguments should be an object");
assert_eq!(args_obj.get("city").unwrap().as_str().unwrap(), "Dallas");
assert_eq!(args_obj.get("state").unwrap().as_str().unwrap(), "TX");
assert_eq!(
args_obj.get("unit").unwrap().as_str().unwrap(),
"fahrenheit"
);
assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
assert_eq!(tool_call.tp, ToolCallType::Function);
}
fn validate_holiday_tool_call(tool_call: &ToolCallResponse) {
assert_eq!(tool_call.function.name, "is_holiday_today");
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
.expect("Arguments should be valid JSON");
let args_obj = args.as_object().expect("Arguments should be an object");
assert!(
args_obj.is_empty(),
"Holiday tool should have empty arguments"
);
assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
assert_eq!(tool_call.tp, ToolCallType::Function);
}
fn validate_unique_tool_call_ids(tool_calls: &[ToolCallResponse]) {
let mut ids = std::collections::HashSet::new();
for tool_call in tool_calls {
assert!(
ids.insert(tool_call.id.clone()),
"Tool call IDs should be unique: {}",
tool_call.id
);
}
}
#[tokio::test]
async fn test_parallel_tool_call_integration() {
let request = create_mock_chat_completion_request();
assert_eq!(request.inner.model, "Qwen/Qwen3-0.6B");
assert_eq!(request.inner.temperature, Some(0.0));
#[allow(deprecated)]
{
assert_eq!(request.inner.max_tokens, Some(3000));
}
assert_eq!(request.inner.stream, Some(false));
assert_eq!(request.inner.messages.len(), 2);
assert_eq!(request.inner.tools.as_ref().unwrap().len(), 2);
match request.inner.tool_choice.as_ref().unwrap() {
dynamo_protocols::types::ChatCompletionToolChoiceOption::Required => {
}
_ => panic!("Tool choice should be Required"),
}
let response_content = get_mock_response_content();
assert!(response_content.contains("get_current_weather"));
assert!(response_content.contains("is_holiday_today"));
assert!(response_content.contains("Dallas"));
assert!(response_content.contains("Texas"));
assert!(response_content.contains("fahrenheit"));
}
#[tokio::test]
async fn test_parallel_tool_call_parsing() {
let response_content = get_mock_response_content();
let (tool_calls, remaining_content) =
detect_and_parse_tool_call(&response_content, Some("hermes"), None)
.await
.expect("Should successfully parse tool calls");
assert_eq!(
tool_calls.len(),
2,
"Should parse exactly 2 parallel tool calls"
);
assert!(remaining_content.is_some());
let remaining = remaining_content.unwrap();
assert!(remaining.contains("<think>"));
assert!(remaining.contains("</think>"));
let mut sorted_calls = tool_calls;
sorted_calls.sort_by(|a, b| a.function.name.cmp(&b.function.name));
validate_weather_tool_call(&sorted_calls[0]);
validate_holiday_tool_call(&sorted_calls[1]);
validate_unique_tool_call_ids(&sorted_calls);
}
#[tokio::test]
async fn test_parallel_tool_call_with_explicit_parser() {
let response_content = get_mock_response_content();
let parsers_to_test = vec![
"hermes", ];
for parser in parsers_to_test {
let (tool_calls, remaining_content) =
detect_and_parse_tool_call(&response_content, Some(parser), None)
.await
.unwrap_or_else(|e| panic!("Should successfully parse with {parser} parser: {e}"));
assert_eq!(
tool_calls.len(),
2,
"Parser {parser} should find 2 tool calls"
);
assert!(remaining_content.is_some());
let mut sorted_calls = tool_calls;
sorted_calls.sort_by(|a, b| a.function.name.cmp(&b.function.name));
validate_weather_tool_call(&sorted_calls[0]);
validate_holiday_tool_call(&sorted_calls[1]);
validate_unique_tool_call_ids(&sorted_calls);
}
}
#[tokio::test]
async fn test_tool_call_json_structure() {
let response_content = get_mock_response_content();
let (tool_calls, _) = detect_and_parse_tool_call(&response_content, Some("hermes"), None)
.await
.expect("Should parse tool calls");
for tool_call in &tool_calls {
let json_str =
serde_json::to_string(tool_call).expect("Tool call should serialize to JSON");
assert!(json_str.contains("\"id\""));
assert!(json_str.contains("\"type\""));
assert!(json_str.contains("\"function\""));
assert!(json_str.contains(&tool_call.function.name));
}
}
#[tokio::test]
async fn test_openai_compatibility_structure() {
let response_content = get_mock_response_content();
let (tool_calls, _) = detect_and_parse_tool_call(&response_content, Some("hermes"), None)
.await
.expect("Should parse tool calls");
for tool_call in &tool_calls {
assert!(!tool_call.id.is_empty(), "Missing required 'id' field");
assert_eq!(
tool_call.tp,
ToolCallType::Function,
"Type should be 'function'"
);
assert!(
!tool_call.function.name.is_empty(),
"Function name should not be empty"
);
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
.expect("Arguments should be valid JSON");
assert!(args.is_object(), "Arguments should be an object");
assert!(
tool_call.id.starts_with("call-") || tool_call.id.starts_with("call_"),
"ID should start with 'call-' or 'call_': {}",
tool_call.id
);
assert!(
tool_call.id.len() > 5,
"ID should be longer than just 'call': {}",
tool_call.id
);
}
}
#[tokio::test]
async fn test_parallel_tool_call_error_handling() {
let malformed_content = r#"<tool_call>
{"name": "get_current_weather", "arguments": {"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}
</tool_call>
<tool_call>
{"invalid_json": }
</tool_call>"#;
let result = detect_and_parse_tool_call(malformed_content, Some("hermes"), None).await;
match result {
Ok((tool_calls, _)) => {
println!(
"Parsed {} tool calls from malformed content",
tool_calls.len()
);
if !tool_calls.is_empty() {
for call in &tool_calls {
assert!(
!call.function.name.is_empty(),
"Parsed tool call should have valid name"
);
}
}
}
Err(e) => {
println!("Expected error for malformed input: {}", e);
}
}
}
#[tokio::test]
async fn test_empty_tool_calls() {
let content_without_tools = "This is just a regular response without any tool calls.";
let (tool_calls, remaining_content) =
detect_and_parse_tool_call(content_without_tools, Some("hermes"), None)
.await
.expect("Should handle content without tool calls");
assert!(
tool_calls.is_empty(),
"Should return empty tool calls array"
);
assert!(
remaining_content.is_some(),
"Should return the original content"
);
assert_eq!(remaining_content.unwrap(), content_without_tools);
}
#[tokio::test]
async fn test_deepseek_v3_1_tool_call_parsing() {
let response_content = r#"I'll help you understand this codebase. Let me start by exploring the structure and key
files to provide you with a comprehensive
explanation.<|tool▁calls▁begin|><|tool▁call▁begin|>TodoWrite<|tool▁sep|>{"todos":
[{"content": "Explore the root directory structure", "status": "in_progress", "activeForm":
"Exploring the root directory structure"}, {"content": "Examine package.json and
configuration files", "status": "pending", "activeForm": "Examining package.json and
configuration files"}, {"content": "Analyze source code structure and key modules",
"status": "pending", "activeForm": "Analyzing source code structure and key modules"},
{"content": "Identify main entry points and architectural patterns", "status": "pending",
"activeForm": "Identifying main entry points and architectural patterns"}, {"content":
"Summarize the codebase purpose and functionality", "status": "pending", "activeForm":
"Summarizing the codebase purpose and
functionality"}]}<|tool▁call▁end|><|tool▁calls▁end|>"#;
println!("Response content: {}", response_content);
println!(
"Contains tool_calls_begin: {}",
response_content.contains("<|tool▁calls▁begin|>")
);
println!(
"Contains tool_call_begin: {}",
response_content.contains("<|tool▁call▁begin|>")
);
let (tool_calls, remaining_content) =
detect_and_parse_tool_call(response_content, Some("deepseek_v3_1"), None)
.await
.expect("Should successfully parse deepseek_v3_1 tool calls");
println!("Number of tool calls parsed: {}", tool_calls.len());
if let Some(ref content) = remaining_content {
println!("Remaining content: {}", content);
}
assert_eq!(tool_calls.len(), 1, "Should parse exactly 1 tool call");
assert!(remaining_content.is_some());
let remaining = remaining_content.unwrap();
assert!(remaining.contains("I'll help you understand this codebase"));
assert!(remaining.contains("comprehensive"));
let tool_call = &tool_calls[0];
assert_eq!(tool_call.function.name, "TodoWrite");
assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
assert_eq!(tool_call.tp, ToolCallType::Function);
let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
.expect("Arguments should be valid JSON");
let args_obj = args.as_object().expect("Arguments should be an object");
assert!(args_obj.contains_key("todos"), "Should have 'todos' key");
let todos = args_obj
.get("todos")
.unwrap()
.as_array()
.expect("todos should be an array");
assert_eq!(todos.len(), 5, "Should have exactly 5 todo items");
let first_todo = &todos[0];
assert_eq!(
first_todo.get("content").unwrap().as_str().unwrap(),
"Explore the root directory structure"
);
assert_eq!(
first_todo.get("status").unwrap().as_str().unwrap(),
"in_progress"
);
assert_eq!(
first_todo.get("activeForm").unwrap().as_str().unwrap(),
"Exploring the root directory structure"
);
let last_todo = &todos[4];
assert_eq!(
last_todo.get("content").unwrap().as_str().unwrap(),
"Summarize the codebase purpose and functionality"
);
assert_eq!(
last_todo.get("status").unwrap().as_str().unwrap(),
"pending"
);
}