Unverified Commit 164b0c29 authored by tangcy98's avatar tangcy98 Committed by GitHub
Browse files

feat: Support toolcall parser for DeepSeek V3 and R1 (#4253)


Signed-off-by: default avatarzhangzhang <tangchenyu@xiaohongshu.com>
Co-authored-by: default avatarzhangzhang <tangchenyu@xiaohongshu.com>
Co-authored-by: default avatarAyush Agarwal <ayushag@nvidia.com>
parent 58405177
...@@ -35,6 +35,7 @@ Parser to Model Mapping ...@@ -35,6 +35,7 @@ Parser to Model Mapping
| harmony | openai/gpt-oss-* | | harmony | openai/gpt-oss-* |
| nemotron_deci | nvidia/nemotron-* | | nemotron_deci | nvidia/nemotron-* |
| phi4 | Phi-4-* | | phi4 | Phi-4-* |
| deepseek_v3 | deepseek-ai/DeepSeek-V3, deepseek-ai/DeepSeek-R1, deepseek-ai/DeepSeek-R1-0528 |
| deepseek_v3_1 | deepseek-ai/DeepSeek-V3.1 | | deepseek_v3_1 | deepseek-ai/DeepSeek-V3.1 |
| pythonic | meta-llama/Llama-4-* | | pythonic | meta-llama/Llama-4-* |
......
{
"request_id": "deepseek-v3.1-no-tool-test",
"expected_output": {
"normal_content": "The weather in Beijing today is sunny with temperatures between 15 and 22 degrees Celsius. It's suitable for outdoor activities.",
"reasoning_content": "The user asked about the weather in Beijing, I will provide relevant weather information.",
"tool_calls": []
},
"input_stream": [
{"data":{"id":"chatcmpl-deepseek-v3.1-no-tool","choices":[{"index":0,"delta":{"content":"<think>The user asked about the weather in Beijing, I will provide relevant weather information.</think>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":"The user asked about the weather in Beijing, I will provide relevant weather information."}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-no-tool","choices":[{"index":0,"delta":{"content":"The weather in Beijing today is sunny with temperatures between 15 and 22 degrees Celsius. It's suitable for outdoor activities.","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-no-tool","choices":[{"index":0,"delta":{"content":null,"function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null},"finish_reason":"stop"}]}}
]
}
\ No newline at end of file
{
"request_id": "deepseek-v3.1-tool-call-test",
"expected_output": {
"normal_content": "",
"reasoning_content": "The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information.",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{\"location\": \"Beijing\", \"format\": \"celsius\"}"
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{\"location\": \"Shanghai\", \"format\": \"celsius\"}"
}
}
]
},
"input_stream": [
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<think>The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information.</think>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":"The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information."}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁calls▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"get_current_weather","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁sep|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"{\"location\": \"Beijing\", \"format\": \"celsius\"}","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"get_current_weather","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁sep|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"{\"location\": \"Shanghai\", \"format\": \"celsius\"}","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":"<|tool▁calls▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3.1-tool","choices":[{"index":0,"delta":{"content":null,"function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null},"finish_reason":"tool_calls"}]}}
]
}
\ No newline at end of file
{
"request_id": "deepseek-v3-no-tool-test",
"expected_output": {
"normal_content": "The weather in Beijing today is sunny with temperatures between 15 and 22 degrees Celsius. It's suitable for outdoor activities.",
"reasoning_content": "The user asked about the weather in Beijing, I will provide relevant weather information.",
"tool_calls": []
},
"input_stream": [
{"data":{"id":"chatcmpl-deepseek-v3-no-tool","choices":[{"index":0,"delta":{"content":"<think>The user asked about the weather in Beijing, I will provide relevant weather information.</think>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":"The user asked about the weather in Beijing, I will provide relevant weather information."}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-no-tool","choices":[{"index":0,"delta":{"content":"The weather in Beijing today is sunny with temperatures between 15 and 22 degrees Celsius. It's suitable for outdoor activities.","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-no-tool","choices":[{"index":0,"delta":{"content":null,"function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null},"finish_reason":"stop"}]}}
]
}
\ No newline at end of file
{
"request_id": "deepseek-v3-tool-call-test",
"expected_output": {
"normal_content": "",
"reasoning_content": "The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information.",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{\"location\": \"Beijing\", \"format\": \"celsius\"}"
}
},
{
"id": "call_2",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{\"location\": \"Shanghai\", \"format\": \"celsius\"}"
}
}
]
},
"input_stream": [
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<think>The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information.</think>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":"The user wants to check the weather in Beijing and Shanghai, I need to call the get_current_weather tool to get this information."}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁calls▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"function","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁sep|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"get_current_weather","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"\n```json\n","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"{\"location\": \"Beijing\", \"format\": \"celsius\"}\n","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"```","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁begin|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"function","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁sep|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"get_current_weather","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"\n```json\n","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"{\"location\": \"Shanghai\", \"format\": \"celsius\"}\n","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"```","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁call▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":"<|tool▁calls▁end|>","function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null}}]}},
{"data":{"id":"chatcmpl-deepseek-v3-tool","choices":[{"index":0,"delta":{"content":null,"function_call":null,"tool_calls":null,"role":"assistant","refusal":null,"reasoning_content":null},"finish_reason":"tool_calls"}]}}
]
}
\ No newline at end of file
...@@ -881,4 +881,214 @@ mod tests { ...@@ -881,4 +881,214 @@ mod tests {
); );
} }
} }
#[tokio::test]
async fn test_deepseek_v3_e2e_with_tools_vllm() {
// E2E Parsing test for DeepSeek V3 with tools.
let file_path = format!(
"{}/vllm/deepseek-v3/chat_completion_stream_tool.json",
DATA_ROOT_PATH
);
let test_data = load_test_data(&file_path);
// Create a stream from the mock chunks
let input_stream = stream::iter(test_data.stream_chunks);
// Parse the response stream with reasoning and tool parsing enabled
let output_chunks = parse_response_stream(
input_stream,
true,
true,
Some("deepseek_v3".to_string()),
Some("deepseek_v3".to_string()),
)
.await;
// Verify we got output chunks
assert!(!output_chunks.is_empty(), "Should have output chunks");
// Aggregate content from output chunks
let aggregated = aggregate_content_from_chunks(&output_chunks);
// Assert reasoning content was parsed
assert_eq!(
aggregated.reasoning_content, test_data.expected_reasoning_content,
"Should have extracted reasoning content.",
);
assert_eq!(
aggregated.normal_content, test_data.expected_normal_content,
"Normal content should match expected value.",
);
// Verify tool calls match expectations
let expected_has_tool_calls = !test_data.expected_tool_calls.is_empty();
assert_eq!(
aggregated.has_tool_calls, expected_has_tool_calls,
"Tool calls presence should match expected value"
);
// Verify tool calls
assert_tool_calls(&aggregated.tool_calls, &test_data.expected_tool_calls);
// Verify finish_reason is valid: exactly one occurrence, in last chunk, and is ToolCalls
assert!(
validate_finish_reason(&output_chunks, FinishReason::ToolCalls),
"finish_reason validation failed for tool call case"
);
}
#[tokio::test]
async fn test_deepseek_v3_1_e2e_with_tools_vllm() {
// E2E Parsing test for DeepSeek V3.1 with tools.
let file_path = format!(
"{}/vllm/deepseek-v3.1/chat_completion_stream_tool.json",
DATA_ROOT_PATH
);
let test_data = load_test_data(&file_path);
// Create a stream from the mock chunks
let input_stream = stream::iter(test_data.stream_chunks);
// Parse the response stream with reasoning and tool parsing enabled
let output_chunks = parse_response_stream(
input_stream,
true,
true,
Some("deepseek_v3_1".to_string()),
Some("deepseek_v3_1".to_string()),
)
.await;
// Verify we got output chunks
assert!(!output_chunks.is_empty(), "Should have output chunks");
// Aggregate content from output chunks
let aggregated = aggregate_content_from_chunks(&output_chunks);
// Assert reasoning content was parsed
assert_eq!(
aggregated.reasoning_content, test_data.expected_reasoning_content,
"Should have extracted reasoning content.",
);
assert_eq!(
aggregated.normal_content, test_data.expected_normal_content,
"Normal content should match expected value.",
);
// Verify tool calls match expectations
let expected_has_tool_calls = !test_data.expected_tool_calls.is_empty();
assert_eq!(
aggregated.has_tool_calls, expected_has_tool_calls,
"Tool calls presence should match expected value"
);
// Verify tool calls
assert_tool_calls(&aggregated.tool_calls, &test_data.expected_tool_calls);
// Verify finish_reason is valid: exactly one occurrence, in last chunk, and is ToolCalls
assert!(
validate_finish_reason(&output_chunks, FinishReason::ToolCalls),
"finish_reason validation failed for tool call case"
);
}
#[tokio::test]
async fn test_deepseek_v3_e2e_with_no_tools_vllm() {
// E2E Parsing test for DeepSeek V3 without tools.
let file_path = format!(
"{}/vllm/deepseek-v3/chat_completion_stream_no_tool.json",
DATA_ROOT_PATH
);
let test_data = load_test_data(&file_path);
// Create a stream from the mock chunks
let input_stream = stream::iter(test_data.stream_chunks);
// Parse the response stream with reasoning and tool parsing enabled
let output_chunks = parse_response_stream(
input_stream,
true,
true,
Some("deepseek_v3".to_string()),
Some("deepseek_v3".to_string()),
)
.await;
// Verify we got output chunks
assert!(!output_chunks.is_empty(), "Should have output chunks");
// Aggregate content from output chunks
let aggregated = aggregate_content_from_chunks(&output_chunks);
// Assert reasoning content was parsed
assert_eq!(
aggregated.reasoning_content, test_data.expected_reasoning_content,
"Should have extracted reasoning content.",
);
assert_eq!(
aggregated.normal_content, test_data.expected_normal_content,
"Normal content should match expected value.",
);
// Verify no tool calls
assert!(!aggregated.has_tool_calls, "Should not have any tool calls");
// Verify finish_reason is valid: exactly one occurrence, in last chunk, and is Stop
assert!(
validate_finish_reason(&output_chunks, FinishReason::Stop),
"finish_reason validation failed for non-tool call case"
);
}
#[tokio::test]
async fn test_deepseek_v3_1_e2e_with_no_tools_vllm() {
// E2E Parsing test for DeepSeek V3.1 without tools.
let file_path = format!(
"{}/vllm/deepseek-v3.1/chat_completion_stream_no_tool.json",
DATA_ROOT_PATH
);
let test_data = load_test_data(&file_path);
// Create a stream from the mock chunks
let input_stream = stream::iter(test_data.stream_chunks);
// Parse the response stream with reasoning and tool parsing enabled
let output_chunks = parse_response_stream(
input_stream,
true,
true,
Some("deepseek_v3_1".to_string()),
Some("deepseek_v3_1".to_string()),
)
.await;
// Verify we got output chunks
assert!(!output_chunks.is_empty(), "Should have output chunks");
// Aggregate content from output chunks
let aggregated = aggregate_content_from_chunks(&output_chunks);
// Assert reasoning content was parsed
assert_eq!(
aggregated.reasoning_content, test_data.expected_reasoning_content,
"Should have extracted reasoning content.",
);
assert_eq!(
aggregated.normal_content, test_data.expected_normal_content,
"Normal content should match expected value.",
);
// Verify no tool calls
assert!(!aggregated.has_tool_calls, "Should not have any tool calls");
// Verify finish_reason is valid: exactly one occurrence, in last chunk, and is Stop
assert!(
validate_finish_reason(&output_chunks, FinishReason::Stop),
"finish_reason validation failed for non-tool call case"
);
}
} }
...@@ -176,4 +176,20 @@ impl ToolCallConfig { ...@@ -176,4 +176,20 @@ impl ToolCallConfig {
}, },
} }
} }
pub fn deepseek_v3() -> Self {
// DeepSeek V3 format:
// <|tool▁calls▁begin|><|tool▁call▁begin|>{type}<|tool▁sep|>{function_name}\n```json\n{arguments}\n```<|tool▁call▁end|><|tool▁calls▁end|>
// There are some differences between DeepSeek V3 and DeepSeek V3.1
Self {
format: ToolCallParserType::Json,
json: JsonParserConfig {
tool_call_start_tokens: vec!["<|tool▁calls▁begin|>".to_string()],
tool_call_end_tokens: vec!["<|tool▁calls▁end|>".to_string()],
tool_call_separator_tokens: vec!["<|tool▁sep|>".to_string()],
parser_type: JsonParserType::DeepseekV3,
..Default::default()
},
}
}
} }
...@@ -8,15 +8,15 @@ use uuid::Uuid; ...@@ -8,15 +8,15 @@ use uuid::Uuid;
use super::config::JsonParserConfig; use super::config::JsonParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType}; use super::response::{CalledFunction, ToolCallResponse, ToolCallType};
/// Extract individual tool call blocks from the input string. /// Extract individual tool call blocks from the input string for DeepSeek V3.1 format.
/// Returns a list of strings, each representing one tool call block. /// Returns a list of strings, each representing one tool call block.
/// ///
/// DeepSeek format: <|tool▁call▁begin|>{name}<|tool▁sep|>{args}<|tool▁call▁end|> /// DeepSeek V3.1 format: <|tool▁call▁begin|>{name}<|tool▁sep|>{args}<|tool▁call▁end|>
/// ///
/// DeepSeek uses nested tokens: /// DeepSeek uses nested tokens:
/// - Wrapper tokens: <|tool▁calls▁begin|> ... <|tool▁calls▁end|> (wraps all tool calls) /// - Wrapper tokens: <|tool▁calls▁begin|> ... <|tool▁calls▁end|> (wraps all tool calls)
/// - Individual tokens: <|tool▁call▁begin|> ... <|tool▁call▁end|> (individual call) /// - Individual tokens: <|tool▁call▁begin|> ... <|tool▁call▁end|> (individual call)
fn extract_tool_call_blocks( fn extract_tool_call_blocks_v3_1(
input: &str, input: &str,
start_tokens: &[String], start_tokens: &[String],
end_tokens: &[String], end_tokens: &[String],
...@@ -74,7 +74,10 @@ fn extract_tool_call_blocks( ...@@ -74,7 +74,10 @@ fn extract_tool_call_blocks(
/// Parse a single tool call block that contains function name and arguments separated by a separator token. /// Parse a single tool call block that contains function name and arguments separated by a separator token.
/// ///
/// Format: {function_name}<|tool▁sep|>{json_arguments} /// Format: {function_name}<|tool▁sep|>{json_arguments}
fn parse_single_tool_call(block: &str, separator_tokens: &[String]) -> Option<(String, Value)> { fn parse_single_tool_call_v3_1(
block: &str,
separator_tokens: &[String],
) -> Option<(String, Value)> {
// Try each separator token // Try each separator token
for sep_token in separator_tokens.iter() { for sep_token in separator_tokens.iter() {
if sep_token.is_empty() { if sep_token.is_empty() {
...@@ -186,7 +189,8 @@ pub fn parse_tool_calls_deepseek_v3_1( ...@@ -186,7 +189,8 @@ pub fn parse_tool_calls_deepseek_v3_1(
}; };
// Extract individual tool call blocks // Extract individual tool call blocks
let blocks = extract_tool_call_blocks(trimmed, &tool_call_start_tokens, &tool_call_end_tokens); let blocks =
extract_tool_call_blocks_v3_1(trimmed, &tool_call_start_tokens, &tool_call_end_tokens);
if blocks.is_empty() { if blocks.is_empty() {
// Found start token but no valid blocks // Found start token but no valid blocks
...@@ -196,7 +200,9 @@ pub fn parse_tool_calls_deepseek_v3_1( ...@@ -196,7 +200,9 @@ pub fn parse_tool_calls_deepseek_v3_1(
// Parse each block to extract function name and arguments // Parse each block to extract function name and arguments
let mut tool_calls: Vec<ToolCallResponse> = Vec::new(); let mut tool_calls: Vec<ToolCallResponse> = Vec::new();
for block in blocks { for block in blocks {
if let Some((function_name, arguments)) = parse_single_tool_call(&block, separator_tokens) { if let Some((function_name, arguments)) =
parse_single_tool_call_v3_1(&block, separator_tokens)
{
tool_calls.push(ToolCallResponse { tool_calls.push(ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()), id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function, tp: ToolCallType::Function,
......
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use regex::RegexBuilder;
use serde_json::Value;
use uuid::Uuid;
use super::config::JsonParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType};
/// Extract individual tool call blocks from the input string for DeepSeek V3 format.
/// Returns a list of strings, each representing one tool call block.
///
/// DeepSeek V3 format: <|tool▁call▁begin|>{type}<|tool▁sep|>{name}\n```json\n{args}\n```<|tool▁call▁end|>
///
fn extract_tool_call_blocks_v3(
input: &str,
start_tokens: &[String],
end_tokens: &[String],
) -> Vec<String> {
let mut blocks = Vec::new();
// Filter tokens to find individual call markers (not the wrapper "calls" versions)
let individual_start_tokens: Vec<&String> = start_tokens
.iter()
.filter(|t| t.contains("tool_call_begin") || t.contains("tool▁call▁begin"))
.collect();
let individual_end_tokens: Vec<&String> = end_tokens
.iter()
.filter(|t| t.contains("tool_call_end") || t.contains("tool▁call▁end"))
.collect();
// Try all combinations of individual start and end tokens
for start_token in individual_start_tokens.iter() {
for end_token in individual_end_tokens.iter() {
if start_token.is_empty() || end_token.is_empty() {
continue;
}
// Build regex pattern with escaped tokens
let escaped_start = regex::escape(start_token);
let escaped_end = regex::escape(end_token);
// DeepSeek V3 format: <|tool▁call▁begin|>{type}<|tool▁sep|>{function_name}\n```json\n{arguments}\n```<|tool▁call▁end|>
let pattern = format!(r"{}(.*?){}", escaped_start, escaped_end);
if let Ok(regex) = RegexBuilder::new(&pattern)
.dot_matches_new_line(true)
.build()
{
for capture in regex.captures_iter(input) {
if let Some(matched) = capture.get(1) {
// Don't trim the content - preserve whitespace for multiline JSON
let content = matched.as_str();
if !content.trim().is_empty() {
blocks.push(content.to_string());
}
}
}
// If we found matches with this token pair, don't try other combinations
if !blocks.is_empty() {
return blocks;
}
}
}
}
blocks
}
/// Parse a single tool call block for DeepSeek V3 format.
///
/// Format: {type}<|tool▁sep|>{function_name}\n```json\n{json_arguments}\n```
fn parse_single_tool_call_v3(block: &str, separator_tokens: &[String]) -> Option<(String, Value)> {
// Try each separator token
for sep_token in separator_tokens.iter() {
if sep_token.is_empty() {
continue;
}
if let Some((_type_part, function_and_args_part)) = block.split_once(sep_token) {
// Parse the function name (after the type and separator)
let (function_name_part, args_block) = function_and_args_part.split_once('\n')?;
let function_name = function_name_part.trim();
if function_name.is_empty() || function_name.contains(['{', '}', '[', ']']) {
continue;
}
// Extract JSON arguments from code block
let args_str = if let Some(json_start) = args_block.find("```json") {
let after_fence = &args_block[json_start + "```json".len()..];
let after_newline = after_fence
.strip_prefix("\r\n")
.or_else(|| after_fence.strip_prefix('\n'))
.unwrap_or(after_fence);
if let Some(json_end) = after_newline.find("```") {
after_newline[..json_end].trim()
} else {
after_newline.trim()
}
} else {
args_block.trim()
};
// Try to parse arguments as JSON
// First try parsing as-is
if let Ok(arguments) = serde_json::from_str::<Value>(args_str) {
return Some((function_name.to_string(), arguments));
}
// If that fails, try normalizing the JSON (handle multiline strings with unescaped newlines)
// This is a lenient approach for malformed JSON that may come from LLMs
let normalized = args_str
.lines()
.map(|line| line.trim_start())
.collect::<Vec<_>>()
.join(" ");
if let Ok(arguments) = serde_json::from_str::<Value>(&normalized) {
return Some((function_name.to_string(), arguments));
}
}
}
None
}
pub fn parse_tool_calls_deepseek_v3(
message: &str,
config: &JsonParserConfig,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
// Format Structure:
// <|tool▁calls▁begin|><|tool▁call▁begin|>{type}<|tool▁sep|>{function_name}\n```json\n{json_arguments}\n```<|tool▁call▁end|><|tool▁calls▁end|>
let trimmed = message.trim();
// Early exit if no content
if trimmed.is_empty() {
return Ok((vec![], Some(String::new())));
}
// For DeepSeek_v3, we consider the tool call block to be
// <|tool▁calls▁begin|>...<|tool▁calls▁end|> and only start parsing
// if seeing <|tool▁calls▁begin|>, even though the individual calls are
// parsed by <|tool▁call▁begin|>...<|tool▁call▁end|>.
let has_end_token = config
.tool_call_end_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token));
if !has_end_token {
return Ok((vec![], Some(trimmed.to_string())));
}
let mut tool_call_start_tokens = config.tool_call_start_tokens.clone();
tool_call_start_tokens.extend(vec!["<|tool▁call▁begin|>".to_string()]);
let mut tool_call_end_tokens = config.tool_call_end_tokens.clone();
tool_call_end_tokens.extend(vec!["<|tool▁call▁end|>".to_string()]);
let separator_tokens = &config.tool_call_separator_tokens;
// Early exit if no tokens configured
if tool_call_start_tokens.is_empty() || separator_tokens.is_empty() {
return Ok((vec![], Some(trimmed.to_string())));
}
// Check if tool call start token is present
if !detect_tool_call_start_deepseek_v3(trimmed, config) {
return Ok((vec![], Some(trimmed.to_string())));
}
// Extract normal text (content before the first wrapper start token)
// Look for wrapper tokens like <|tool▁calls▁begin|> (note: "calls" not "call")
let wrapper_tokens: Vec<&String> = tool_call_start_tokens
.iter()
.filter(|t| t.contains("tool_calls_begin") || t.contains("tool▁calls▁begin"))
.collect();
let normal_text = if !wrapper_tokens.is_empty() {
wrapper_tokens
.iter()
.find_map(|token| {
trimmed
.find(token.as_str())
.map(|idx| trimmed[..idx].to_string())
})
.unwrap_or_else(String::new)
} else {
// Fallback to first individual call token if no wrapper found
tool_call_start_tokens
.iter()
.filter(|token| !token.is_empty())
.find_map(|token| trimmed.find(token).map(|idx| trimmed[..idx].to_string()))
.unwrap_or_else(String::new)
};
// Extract individual tool call blocks
let blocks =
extract_tool_call_blocks_v3(trimmed, &tool_call_start_tokens, &tool_call_end_tokens);
if blocks.is_empty() {
// Found start token but no valid blocks
return Ok((vec![], Some(trimmed.to_string())));
}
// Parse each block to extract function name and arguments
let mut tool_calls: Vec<ToolCallResponse> = Vec::new();
for block in blocks {
if let Some((function_name, arguments)) =
parse_single_tool_call_v3(&block, separator_tokens)
{
tool_calls.push(ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function,
function: CalledFunction {
name: function_name,
arguments: serde_json::to_string(&arguments)?,
},
});
}
}
// If no valid tool calls were parsed, return everything as normal text
if tool_calls.is_empty() {
return Ok((vec![], Some(trimmed.to_string())));
}
Ok((tool_calls, Some(normal_text)))
}
pub fn detect_tool_call_start_deepseek_v3(chunk: &str, config: &JsonParserConfig) -> bool {
let trimmed = chunk.trim();
if trimmed.is_empty() {
return false;
}
// Check for complete start tokens first
let has_complete_token = config
.tool_call_start_tokens
.iter()
.any(|token| !token.is_empty() && trimmed.contains(token));
if has_complete_token {
return true;
}
// Check for partial start tokens (streaming scenario)
// This handles cases where start tokens are split across multiple chunks
config.tool_call_start_tokens.iter().any(|token| {
if token.is_empty() {
return false;
}
// Check if the chunk could be a prefix of this start token
// Handle Unicode character boundaries properly
for i in 1..=token.chars().count() {
if let Some(prefix) = token.chars().take(i).collect::<String>().get(..) {
let prefix_str = &prefix[..prefix.len()];
if trimmed == prefix_str || trimmed.ends_with(prefix_str) {
return true;
}
}
}
false
})
}
#[cfg(test)]
mod tests {
use super::super::config::ToolCallConfig;
use super::*;
fn extract_name_and_args(call: ToolCallResponse) -> (String, serde_json::Value) {
let args: serde_json::Value = serde_json::from_str(&call.function.arguments).unwrap();
(call.function.name, args)
}
#[test]
fn test_parse_tool_calls_deepseek_v3_basic() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "HongKong"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Paris"}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(content, Some("".to_string()));
assert_eq!(result.len(), 2);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "HongKong");
let (name, args) = extract_name_and_args(result[1].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Paris");
}
#[test]
fn test_parse_tool_calls_deepseek_v3_with_normal_text() {
let text = r#"The following tool call retrieves weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "New York"}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(
content,
Some("The following tool call retrieves weather information: ".to_string())
);
assert_eq!(result.len(), 1);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "New York");
}
#[test]
fn test_parse_tool_calls_deepseek_v3_without_tool_call_start_token() {
let text = r#"<|tool▁call▁begin|>function宽带}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(content, Some(text.to_string()));
assert_eq!(result.len(), 0);
}
#[test]
fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_multiple_args() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Shanghai", "units": "metric"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_weather_forecast
```json
{"location": "Shanghai", "days": 7, "units": "imperial"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_air_quality
```json
{"location": "Shanghai", "radius": 50}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(content, Some("".to_string()));
assert_eq!(result.len(), 3);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["units"], "metric");
let (name, args) = extract_name_and_args(result[1].clone());
assert_eq!(name, "get_weather_forecast");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["days"], 7);
assert_eq!(args["units"], "imperial");
let (name, args) = extract_name_and_args(result[2].clone());
assert_eq!(name, "get_air_quality");
assert_eq!(args["location"], "Shanghai");
assert_eq!(args["radius"], 50);
}
#[test]
fn test_parse_tool_calls_deepseek_v3_with_invalid_json() {
// Everything is normal text in case of invalid json
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(content, Some(text.trim().to_string()));
assert_eq!(result.len(), 0);
}
#[test]
fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_normal_text() {
// Everything is normal text in case of invalid json
let text = r#"The following tool calls retrieve weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}{location": "HongKong"}
```json
}
```<|tool▁call▁end|><|tool▁call▁begin|>function宽带}{location": "Shanghai", "days": 7, "units": "imperial"}
```json
}
```<|tool▁call▁end|><|tool▁call▁begin|>function宽带}{location": "Shanghai", "radius": 50}
```json
}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (result, content) = parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(content, Some(text.trim().to_string()));
assert_eq!(result.len(), 0);
}
#[test]
fn test_parse_tool_calls_deepseek_v3_with_multiline_json() {
let text = r#"I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure
and key files to provide you with a comprehensive
explanation.<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>TodoWrite
```json
{"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|>"#;
let config = ToolCallConfig::deepseek_v3().json;
let (tool_call_results, normal_content) =
parse_tool_calls_deepseek_v3(text, &config).unwrap();
assert_eq!(tool_call_results.len(), 1);
let (name, args) = extract_name_and_args(tool_call_results[0].clone());
assert_eq!(name, "TodoWrite");
assert_eq!(tool_call_results[0].tp, ToolCallType::Function);
let todos_array = args["todos"].as_array().unwrap();
assert_eq!(todos_array.len(), 5);
assert_eq!(
todos_array[0]["content"],
"Explore the root directory structure"
);
assert_eq!(todos_array[0]["status"], "in_progress");
assert_eq!(
todos_array[0]["activeForm"],
"Exploring the root directory structure"
);
assert_eq!(
todos_array[1]["content"],
"Examine package.json and configuration files"
);
assert_eq!(todos_array[1]["status"], "pending");
assert_eq!(
todos_array[4]["content"],
"Summarize the codebase purpose and functionality"
);
assert_eq!(todos_array[4]["status"], "pending");
assert_eq!(
normal_content,
Some("I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure\n and key files to provide you with a comprehensive\n explanation.".to_string())
);
}
}
#[cfg(test)]
mod detect_parser_tests {
use super::super::config::ToolCallConfig;
use super::*;
#[test]
fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}"#;
let config = ToolCallConfig::deepseek_v3().json;
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(result);
}
#[test]
fn test_detect_tool_call_start_deepseek_v3_chunk_without_tool_call_start_token() {
let text = r#"<|tool▁call▁begin|>function宽带}"#;
let config = ToolCallConfig::deepseek_v3().json;
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(!result);
}
#[test]
fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token_in_middle() {
let text = r#"The following tool calls retrieve weather information: <|tool▁calls▁begin|><|tool▁call▁begin|>function宽带}"#;
let config = ToolCallConfig::deepseek_v3().json;
let result = detect_tool_call_start_deepseek_v3(text, &config);
assert!(result);
}
#[test]
fn test_detect_tool_call_start_deepseek_v3_partial_tokens() {
// Test partial token detection for streaming scenarios with unicode characters
let config = ToolCallConfig::deepseek_v3().json;
// Test various partial prefixes
assert!(
detect_tool_call_start_deepseek_v3("<", &config),
"'<' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|", &config),
"'<|' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|tool", &config),
"'<|tool' should be detected as potential start"
);
assert!(
detect_tool_call_start_deepseek_v3("<|tool▁calls", &config),
"'<|tool▁calls' should be detected as potential start"
);
// Test that unrelated text is not detected
assert!(
!detect_tool_call_start_deepseek_v3("hello world", &config),
"'hello world' should not be detected"
);
assert!(
!detect_tool_call_start_deepseek_v3("xyz", &config),
"'xyz' should not be detected"
);
}
}
...@@ -2,11 +2,15 @@ ...@@ -2,11 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pub mod base_json_parser; pub mod base_json_parser;
pub mod deepseek_parser; pub mod deepseek_v3_1_parser;
pub mod deepseek_v3_parser;
pub use super::{config, response}; pub use super::{config, response};
pub use base_json_parser::{detect_tool_call_start_basic_json, try_tool_call_parse_basic_json}; pub use base_json_parser::{detect_tool_call_start_basic_json, try_tool_call_parse_basic_json};
pub use deepseek_parser::{detect_tool_call_start_deepseek_v3_1, parse_tool_calls_deepseek_v3_1}; pub use deepseek_v3_1_parser::{
detect_tool_call_start_deepseek_v3_1, parse_tool_calls_deepseek_v3_1,
};
pub use deepseek_v3_parser::{detect_tool_call_start_deepseek_v3, parse_tool_calls_deepseek_v3};
pub use super::config::JsonParserConfig; pub use super::config::JsonParserConfig;
pub use super::response::ToolCallResponse; pub use super::response::ToolCallResponse;
...@@ -16,6 +20,7 @@ pub enum JsonParserType { ...@@ -16,6 +20,7 @@ pub enum JsonParserType {
// Basic is generic json parser which can handle most of the cases // Basic is generic json parser which can handle most of the cases
Basic, Basic,
// Model Specific JSON Parsers // Model Specific JSON Parsers
DeepseekV3,
DeepseekV31, DeepseekV31,
} }
...@@ -31,6 +36,7 @@ pub fn try_tool_call_parse_json( ...@@ -31,6 +36,7 @@ pub fn try_tool_call_parse_json(
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> { ) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
match config.parser_type { match config.parser_type {
JsonParserType::Basic => try_tool_call_parse_basic_json(message, config), JsonParserType::Basic => try_tool_call_parse_basic_json(message, config),
JsonParserType::DeepseekV3 => parse_tool_calls_deepseek_v3(message, config),
JsonParserType::DeepseekV31 => parse_tool_calls_deepseek_v3_1(message, config), JsonParserType::DeepseekV31 => parse_tool_calls_deepseek_v3_1(message, config),
} }
} }
...@@ -38,6 +44,7 @@ pub fn try_tool_call_parse_json( ...@@ -38,6 +44,7 @@ pub fn try_tool_call_parse_json(
pub fn detect_tool_call_start_json(chunk: &str, config: &JsonParserConfig) -> bool { pub fn detect_tool_call_start_json(chunk: &str, config: &JsonParserConfig) -> bool {
match config.parser_type { match config.parser_type {
JsonParserType::Basic => detect_tool_call_start_basic_json(chunk, config), JsonParserType::Basic => detect_tool_call_start_basic_json(chunk, config),
JsonParserType::DeepseekV3 => detect_tool_call_start_deepseek_v3(chunk, config),
JsonParserType::DeepseekV31 => detect_tool_call_start_deepseek_v3_1(chunk, config), JsonParserType::DeepseekV31 => detect_tool_call_start_deepseek_v3_1(chunk, config),
} }
} }
......
...@@ -30,6 +30,7 @@ pub fn get_tool_parser_map() -> &'static HashMap<&'static str, ToolCallConfig> { ...@@ -30,6 +30,7 @@ pub fn get_tool_parser_map() -> &'static HashMap<&'static str, ToolCallConfig> {
map.insert("phi4", ToolCallConfig::phi4()); map.insert("phi4", ToolCallConfig::phi4());
map.insert("pythonic", ToolCallConfig::pythonic()); map.insert("pythonic", ToolCallConfig::pythonic());
map.insert("harmony", ToolCallConfig::harmony()); map.insert("harmony", ToolCallConfig::harmony());
map.insert("deepseek_v3", ToolCallConfig::deepseek_v3());
map.insert("deepseek_v3_1", ToolCallConfig::deepseek_v3_1()); map.insert("deepseek_v3_1", ToolCallConfig::deepseek_v3_1());
map.insert("default", ToolCallConfig::default()); map.insert("default", ToolCallConfig::default());
map map
...@@ -185,6 +186,7 @@ mod tests { ...@@ -185,6 +186,7 @@ mod tests {
"phi4", "phi4",
"default", "default",
"pythonic", "pythonic",
"deepseek_v3",
"deepseek_v3_1", "deepseek_v3_1",
]; ];
for parser in available_parsers { for parser in available_parsers {
...@@ -1509,6 +1511,28 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it ...@@ -1509,6 +1511,28 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it
assert_eq!(args["unit"], "fahrenheit"); assert_eq!(args["unit"], "fahrenheit");
} }
#[tokio::test]
async fn test_deepseek_v3_parser_basic() {
let input = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Tokyo"}
```<|tool▁call▁end|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Paris"}
```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
let (result, content) = detect_and_parse_tool_call(input, Some("deepseek_v3"))
.await
.unwrap();
assert_eq!(content, Some("".to_string()));
assert_eq!(result.len(), 2);
let (name, args) = extract_name_and_args(result[0].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Tokyo");
let (name, args) = extract_name_and_args(result[1].clone());
assert_eq!(name, "get_current_weather");
assert_eq!(args["location"], "Paris");
}
#[tokio::test] #[tokio::test]
async fn test_deepseek_v3_1_parser_basic() { async fn test_deepseek_v3_1_parser_basic() {
let input = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Tokyo"}<|tool▁call▁end|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Paris"}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#; let input = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Tokyo"}<|tool▁call▁end|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Paris"}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"#;
...@@ -2412,17 +2436,38 @@ mod detect_parser_tests { ...@@ -2412,17 +2436,38 @@ mod detect_parser_tests {
assert!(result); assert!(result);
} }
// DeepSeek V3
#[test]
fn test_e2e_detect_incomplete_tool_call_start_deepseek_v3() {
let text = r#"<|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Tokyo"}
```<|tool▁call▁end|>"#;
let result = detect_tool_call_start(text, Some("deepseek_v3")).unwrap();
assert!(!result);
}
#[test]
fn test_e2e_detect_tool_call_start_deepseek_v3() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather
```json
{"location": "Tokyo"}
```<|tool▁call▁end|>"#;
let result = detect_tool_call_start(text, Some("deepseek_v3")).unwrap();
assert!(result);
}
// DeepSeek V3.1
#[test] #[test]
fn test_e2e_detect_incomplete_tool_call_start_deepseek_v3_1() { fn test_e2e_detect_incomplete_tool_call_start_deepseek_v3_1() {
let text = let text = r#"<|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Tokyo"}<|tool▁call▁end|>"#;
r#"<|tool▁call▁begin|>get_current_weather{"location": "Tokyo"}<|tool▁call▁end|>"#;
let result = detect_tool_call_start(text, Some("deepseek_v3_1")).unwrap(); let result = detect_tool_call_start(text, Some("deepseek_v3_1")).unwrap();
assert!(!result); assert!(!result);
} }
#[test] #[test]
fn test_e2e_detect_tool_call_start_deepseek_v3_1() { fn test_e2e_detect_tool_call_start_deepseek_v3_1() {
let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather{"location": "Tokyo"}<|tool▁call▁end|>"#; let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Tokyo"}<|tool▁call▁end|>"#;
let result = detect_tool_call_start(text, Some("deepseek_v3_1")).unwrap(); let result = detect_tool_call_start(text, Some("deepseek_v3_1")).unwrap();
assert!(result); assert!(result);
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment