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
| harmony | openai/gpt-oss-* |
| nemotron_deci | nvidia/nemotron-* |
| 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 |
| 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 {
);
}
}
#[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 {
},
}
}
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;
use super::config::JsonParserConfig;
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.
///
/// 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:
/// - Wrapper tokens: <|tool▁calls▁begin|> ... <|tool▁calls▁end|> (wraps all tool calls)
/// - Individual tokens: <|tool▁call▁begin|> ... <|tool▁call▁end|> (individual call)
fn extract_tool_call_blocks(
fn extract_tool_call_blocks_v3_1(
input: &str,
start_tokens: &[String],
end_tokens: &[String],
......@@ -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.
///
/// 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
for sep_token in separator_tokens.iter() {
if sep_token.is_empty() {
......@@ -186,7 +189,8 @@ pub fn parse_tool_calls_deepseek_v3_1(
};
// 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() {
// Found start token but no valid blocks
......@@ -196,7 +200,9 @@ pub fn parse_tool_calls_deepseek_v3_1(
// 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(&block, separator_tokens) {
if let Some((function_name, arguments)) =
parse_single_tool_call_v3_1(&block, separator_tokens)
{
tool_calls.push(ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function,
......
This diff is collapsed.
......@@ -2,11 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
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 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::response::ToolCallResponse;
......@@ -16,6 +20,7 @@ pub enum JsonParserType {
// Basic is generic json parser which can handle most of the cases
Basic,
// Model Specific JSON Parsers
DeepseekV3,
DeepseekV31,
}
......@@ -31,6 +36,7 @@ pub fn try_tool_call_parse_json(
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
match config.parser_type {
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),
}
}
......@@ -38,6 +44,7 @@ pub fn try_tool_call_parse_json(
pub fn detect_tool_call_start_json(chunk: &str, config: &JsonParserConfig) -> bool {
match config.parser_type {
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),
}
}
......
......@@ -30,6 +30,7 @@ pub fn get_tool_parser_map() -> &'static HashMap<&'static str, ToolCallConfig> {
map.insert("phi4", ToolCallConfig::phi4());
map.insert("pythonic", ToolCallConfig::pythonic());
map.insert("harmony", ToolCallConfig::harmony());
map.insert("deepseek_v3", ToolCallConfig::deepseek_v3());
map.insert("deepseek_v3_1", ToolCallConfig::deepseek_v3_1());
map.insert("default", ToolCallConfig::default());
map
......@@ -185,6 +186,7 @@ mod tests {
"phi4",
"default",
"pythonic",
"deepseek_v3",
"deepseek_v3_1",
];
for parser in available_parsers {
......@@ -1509,6 +1511,28 @@ Remember, San Francisco weather can be quite unpredictable, particularly with it
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]
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|>"#;
......@@ -2412,17 +2436,38 @@ mod detect_parser_tests {
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]
fn test_e2e_detect_incomplete_tool_call_start_deepseek_v3_1() {
let text =
r#"<|tool▁call▁begin|>get_current_weather{"location": "Tokyo"}<|tool▁call▁end|>"#;
let text = r#"<|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();
assert!(!result);
}
#[test]
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();
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