ref(upstream): FULL TREE — Deep-Spark xllm (1470) + ds_vllm csrc/models (703)

Replaces cherry-picked upstream_ref with complete source trees.

xllm/ — Iluvatar official C++ inference engine (15MB, 1470 files)
  Complete: kernels → layers → models → runtime → scheduler → api
  Excluded: .git, binary images, third_party submodule checkouts

ds_vllm/ — Iluvatar official vllm fork (8MB, 703 files)
  Included: csrc/ (ALL CUDA kernels), fused_moe/, qwen3_5 model, _custom_ops
  Excluded: tests, benchmarks, docs, examples (not needed for reference)

Critical call chains now fully traceable:
  MoE: moe_topk_softmax_kernels.cuh → ixformer.h → fused_moe.cpp → layer
  GDN: qwen3_gated_delta_net_base.cpp → qwen3_5_gated_delta_net.cpp
  Attention: ixformer.h → xllm_paged_attention → attention.cpp
This commit is contained in:
EX Engine
2026-08-10 02:53:54 +00:00
parent 9e4fb3712f
commit 002f9879b2
2179 changed files with 494021 additions and 79 deletions

View File

@@ -0,0 +1,25 @@
include(cc_test)
add_subdirectory(partial_json_parser)
function(add_detector_test TEST_NAME)
cc_test(
NAME
${TEST_NAME}
SRCS
${TEST_NAME}.cpp
DEPS
:function_call
GTest::gtest
GTest::gtest_main
nlohmann_json::nlohmann_json
)
endfunction()
add_detector_test(qwen25_detector_test)
add_detector_test(qwen3_coder_detector_test)
add_detector_test(kimik2_detector_test)
add_detector_test(deepseekv3_detector_test)
add_detector_test(glm45_detector_test)
add_detector_test(glm47_detector_test)
add_detector_test(deepseekv32_detector_test)

View File

@@ -0,0 +1,943 @@
/* Copyright 2026 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "deepseekv32_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace xllm {
namespace function_call {
class DeepSeek32DetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<DeepSeekV32Detector>();
// Setup test tools
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"city",
{{"type", "string"},
{"description", "The city name, e.g. Beijing, Shanghai"}}},
{"date",
{{"type", "string"},
{"description", "Date in YYYY-MM-DD format"}}}}},
{"required", {"city"}}};
JsonFunction weather_func("get_weather",
"Get the weather information for a given city",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}},
{"precision",
{{"type", "number"}, {"description", "Number of decimal places"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
std::unique_ptr<DeepSeekV32Detector> detector_;
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
// Test constructor and basic properties
TEST_F(DeepSeek32DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection
std::string text_with_tool_call =
"Some text "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">Beijing</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(DeepSeek32DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<DSMLfunction_calls>"));
EXPECT_TRUE(detector_->has_tool_call("<DSMLinvoke"));
EXPECT_TRUE(detector_->has_tool_call(
"Previous text <DSMLfunction_calls>Following content"));
EXPECT_TRUE(
detector_->has_tool_call("<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">北京</DSMLparameter></"
"DSMLinvoke></DSMLfunction_calls>"));
EXPECT_TRUE(detector_->has_tool_call("{\"tool_calls\": []}"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(detector_->has_tool_call("DSML without brackets"));
EXPECT_FALSE(detector_->has_tool_call("<function_calls> without DSML"));
}
// Test single tool call parsing
TEST_F(DeepSeek32DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">北京</DSMLparameter><DSMLparameter "
"name=\"date\" "
"string=\"true\">2024-06-27</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "北京");
EXPECT_EQ(params["date"], "2024-06-27");
}
// Test single tool call parsing with direct JSON format
TEST_F(DeepSeek32DetectorTest, SingleToolCallParsingJsonFormat) {
std::string text =
"Please help me check the weather "
"<DSMLfunction_calls><DSMLinvoke name=\"get_weather\">{\"city\": "
"\"北京\", \"date\": "
"\"2024-06-27\"}</DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "北京");
EXPECT_EQ(params["date"], "2024-06-27");
}
// Test multiple tool calls parsing
TEST_F(DeepSeek32DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">上海</DSMLparameter><DSMLparameter "
"name=\"date\" "
"string=\"true\">2024-06-27</DSMLparameter></"
"DSMLinvoke><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter name=\"city\" "
"string=\"true\">北京</DSMLparameter><DSMLparameter "
"name=\"date\" "
"string=\"true\">2024-06-27</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate");
ASSERT_EQ(result.calls.size(), 2);
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["city"], "上海");
EXPECT_EQ(params1["date"], "2024-06-27");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "get_weather");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["city"], "北京");
EXPECT_EQ(params2["date"], "2024-06-27");
}
// Test number type handling
TEST_F(DeepSeek32DetectorTest, NumberTypeHandling) {
std::string text =
"Calculate with precision "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"calculate\"><DSMLparameter "
"name=\"expression\" string=\"true\">3.14 * "
"2</DSMLparameter><DSMLparameter "
"name=\"precision\" "
"string=\"false\">2</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Calculate with precision");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["expression"], "3.14 * 2");
// precision should be parsed as number
EXPECT_TRUE(params["precision"].is_number());
EXPECT_EQ(params["precision"], 2);
}
// Test empty tool call content
TEST_F(DeepSeek32DetectorTest, EmptyToolCallContent) {
std::string text =
"Test empty content <DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"></DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content");
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params.empty());
}
// Test incomplete tool call (only start tag)
TEST_F(DeepSeek32DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">Beijing";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(DeepSeek32DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"unknown_tool\"><DSMLparameter "
"name=\"param\" "
"string=\"true\">value</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool");
// Base class will skip unknown tools, so should be 0 calls
EXPECT_EQ(result.calls.size(), 0);
}
// Test case with only normal text
TEST_F(DeepSeek32DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(DeepSeek32DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test whitespace-only input
TEST_F(DeepSeek32DetectorTest, WhitespaceOnlyInput) {
std::string text = " \t\n\r ";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
}
// Test complex nested JSON parameters
TEST_F(DeepSeek32DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">Beijing</DSMLparameter><DSMLparameter "
"name=\"options\" string=\"false\">{\"include_forecast\": true, "
"\"days\": "
"7}</DSMLparameter></DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
}
// Test special characters handling
TEST_F(DeepSeek32DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">New York "
"City</DSMLparameter><DSMLparameter "
"name=\"note\" string=\"true\">Contains "
"symbols@#$%^&*()_+=</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Test whitespace handling in parameter values
TEST_F(DeepSeek32DetectorTest, WhitespaceHandlingInParameterValues) {
std::string text =
"Whitespace test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\"> Beijing "
"</DSMLparameter><DSMLparameter "
"name=\"date\" "
"string=\"true\">\n\t2024-06-27\r\n</DSMLparameter></"
"DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Whitespace test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
// Note: whitespace trimming behavior depends on implementation
// This test verifies the basic parsing works
EXPECT_TRUE(params.contains("city"));
EXPECT_TRUE(params.contains("date"));
}
// ========== Streaming Tests ==========
// Test basic streaming parsing
TEST_F(DeepSeek32DetectorTest, BasicStreamingParsing) {
std::vector<std::string> chunks = {
"<DSMLfunction_calls><DSMLinvoke name=\"get_weather\">",
"<DSMLparameter name=\"city\" string=\"true\">",
"Beijing</DSMLparameter></DSMLinvoke></DSMLfunction_calls>"};
std::vector<StreamingParseResult> results;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
results.push_back(result);
}
bool found_tool_name = false;
bool found_arguments = false;
for (const auto& result : results) {
if (!result.calls.empty()) {
for (const auto& call : result.calls) {
if (call.name.has_value() && call.name.value() == "get_weather") {
found_tool_name = true;
}
if (!call.parameters.empty() &&
call.parameters.find("Beijing") != std::string::npos) {
found_arguments = true;
}
}
}
}
EXPECT_TRUE(found_tool_name) << "Should find tool name in streaming results";
EXPECT_TRUE(found_arguments) << "Should find arguments in streaming results";
}
// Test incremental argument streaming
TEST_F(DeepSeek32DetectorTest, IncrementalArgumentStreaming) {
std::vector<std::string> chunks = {
"<DSMLfunction_calls><DSMLinvoke name=\"get_weather\">",
"<DSMLparameter name=\"city\" string=\"true\">",
"Beijing",
"</DSMLparameter><DSMLparameter name=\"date\" string=\"true\">",
"2024-06-27</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>"};
std::string accumulated_args;
bool tool_name_sent = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
for (const auto& call : result.calls) {
if (call.name.has_value()) {
tool_name_sent = true;
EXPECT_EQ(call.name.value(), "get_weather");
} else {
accumulated_args += call.parameters;
}
}
}
EXPECT_TRUE(tool_name_sent)
<< "Tool name should be sent when tool call is complete";
if (!accumulated_args.empty()) {
EXPECT_TRUE(accumulated_args.find("Beijing") != std::string::npos)
<< "Should contain city argument";
EXPECT_TRUE(accumulated_args.find("2024-06-27") != std::string::npos)
<< "Should contain date argument";
}
}
// Test normal text handling during streaming
TEST_F(DeepSeek32DetectorTest, StreamingNormalTextHandling) {
std::vector<std::string> chunks = {
"This is normal text before tool call. ",
"<DSMLfunction_calls><DSMLinvoke name=\"get_weather\">",
"<DSMLparameter name=\"city\" "
"string=\"true\">Tokyo</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>",
" And this is text after."};
std::string accumulated_normal_text;
bool found_tool_call = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
if (!result.calls.empty()) {
found_tool_call = true;
}
}
EXPECT_TRUE(found_tool_call) << "Should find tool call";
EXPECT_TRUE(accumulated_normal_text.find("This is normal text") !=
std::string::npos)
<< "Should preserve normal text before tool call";
// Note: We don't expect "And this is text after" to be in
// accumulated_normal_text
}
TEST_F(DeepSeek32DetectorTest, StreamingNormalTextBuffersIncompleteUtf8Tail) {
std::string chunk1 = "The user didn";
chunk1.push_back(static_cast<char>(0xE2));
chunk1.push_back(static_cast<char>(0x80));
std::string chunk2;
chunk2.push_back(static_cast<char>(0x99));
chunk2 += "t specify temperature units.";
std::string accumulated_normal_text;
for (const auto& chunk : {chunk1, chunk2}) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
accumulated_normal_text += result.normal_text;
}
EXPECT_EQ(accumulated_normal_text,
"The user didnt specify temperature units.");
}
TEST_F(DeepSeek32DetectorTest, StreamingToolArgumentsBufferIncompleteUtf8Tail) {
std::string chunk1 =
"<DSMLfunction_calls><DSMLinvoke name=\"get_weather\">"
"<DSMLparameter name=\"city\" string=\"true\">";
std::string chunk2;
chunk2.push_back(static_cast<char>(0xE5));
chunk2.push_back(static_cast<char>(0x8C));
std::string chunk3;
chunk3.push_back(static_cast<char>(0x97));
chunk3 += "京</DSMLparameter></DSMLinvoke></DSMLfunction_calls>";
std::string accumulated_arguments;
bool tool_name_sent = false;
for (const auto& chunk : {chunk1, chunk2, chunk3}) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
for (const auto& call : result.calls) {
if (call.name.has_value()) {
tool_name_sent = true;
EXPECT_EQ(call.name.value(), "get_weather");
} else {
accumulated_arguments += call.parameters;
}
}
}
ASSERT_TRUE(tool_name_sent);
nlohmann::json params = nlohmann::json::parse(accumulated_arguments);
EXPECT_EQ(params["city"], "北京");
}
// Test invalid JSON in parameter values
TEST_F(DeepSeek32DetectorTest, InvalidJsonInParameterValues) {
std::string text =
"Invalid JSON test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">Beijing</DSMLparameter><DSMLparameter "
"name=\"config\" string=\"false\">{invalid "
"json}</DSMLparameter></DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Invalid JSON test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
// Invalid JSON should be treated as string or cause parsing to fail
// gracefully
EXPECT_TRUE(params.contains("config"));
}
// Test nested braces in JSON values
TEST_F(DeepSeek32DetectorTest, NestedBracesInJsonValues) {
std::string text =
"Nested braces test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">Beijing</DSMLparameter><DSMLparameter "
"name=\"config\" string=\"false\">{\"nested\": {\"deep\": "
"\"value\"}}</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Nested braces test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
}
// Test mixed format (XML and JSON in same invoke)
TEST_F(DeepSeek32DetectorTest, MixedFormatNotSupported) {
// Note: This test verifies that the parser handles the format correctly
// In practice, an invoke should use either XML or JSON, not both
std::string text =
"Mixed format test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">Beijing</DSMLparameter>{\"date\": "
"\"2024-06-27\"}</DSMLinvoke></DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Mixed format test");
// The parser should handle this gracefully
ASSERT_GE(result.calls.size(), 0);
}
// Test performance with many tool calls
TEST_F(DeepSeek32DetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test <DSMLfunction_calls>";
// Build text containing multiple tool calls
for (int i = 0; i < 10; ++i) {
text +=
"<DSMLinvoke name=\"calculate\"><DSMLparameter "
"name=\"expression\" string=\"true\">" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"</DSMLparameter></DSMLinvoke>";
}
text += "</DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test");
ASSERT_EQ(result.calls.size(), 10);
// Verify each tool call is correctly parsed
for (int i = 0; i < 10; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Test array parameter with string="false"
TEST_F(DeepSeek32DetectorTest, ArrayParameterHandling) {
std::string text =
"Array parameter test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"cities\" string=\"false\">[\"Beijing\", \"Shanghai\", "
"\"Guangzhou\"]</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Array parameter test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_TRUE(params["cities"].is_array());
EXPECT_EQ(params["cities"].size(), 3);
EXPECT_EQ(params["cities"][0], "Beijing");
EXPECT_EQ(params["cities"][1], "Shanghai");
EXPECT_EQ(params["cities"][2], "Guangzhou");
}
// Test boolean parameter with string="false"
TEST_F(DeepSeek32DetectorTest, BooleanParameterHandling) {
std::string text =
"Boolean parameter test "
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"include_forecast\" "
"string=\"false\">true</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Boolean parameter test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_TRUE(params["include_forecast"].is_boolean());
EXPECT_EQ(params["include_forecast"], true);
}
// Test JSON format tool calls (fallback)
TEST_F(DeepSeek32DetectorTest, JsonFormatToolCalls) {
std::string text =
"Some text before "
"{\"tool_calls\": [{\"name\": \"get_weather\", \"arguments\": "
"{\"city\": \"Beijing\", \"date\": \"2024-06-27\"}}]}";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Some text before");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_EQ(params["date"], "2024-06-27");
}
// Test JSON format with function wrapper
TEST_F(DeepSeek32DetectorTest, JsonFormatWithFunctionWrapper) {
std::string text =
"Text before "
"{\"tool_calls\": [{\"function\": {\"name\": \"get_weather\", "
"\"arguments\": \"{\\\"city\\\": \\\"Tokyo\\\"}\"}}]}";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Text before");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Tokyo");
}
// Test trailing markdown language hint before DSML tool block
TEST_F(DeepSeek32DetectorTest, StripTrailingJsonHintBeforeDsmlToolBlock) {
std::string text =
"Some text before tool call\n"
"json\n"
"<DSMLfunction_calls><DSMLinvoke "
"name=\"get_weather\"><DSMLparameter "
"name=\"city\" "
"string=\"true\">Beijing</DSMLparameter></DSMLinvoke></"
"DSMLfunction_calls>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Some text before tool call");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
}
// Test trailing markdown language hint before JSON fallback tool block
TEST_F(DeepSeek32DetectorTest, StripTrailingJsonHintBeforeJsonToolBlock) {
std::string text =
"Some text before tool call\n"
"json\n"
"{\"tool_calls\": [{\"name\": \"get_weather\", \"arguments\": "
"{\"city\": \"Beijing\"}}]}";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Some text before tool call");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
}
// Test streaming with multiple tool calls
TEST_F(DeepSeek32DetectorTest, StreamingMultipleToolCalls) {
std::vector<std::string> chunks = {"<DSMLfunction_calls>",
"\n",
"<DSMLinvoke",
" name=\"get_weather\">",
"<DSMLparameter name=\"city\" "
"string=\"true\">",
"Tokyo",
"</DSMLparameter>",
"</DSMLinvoke>",
"\n",
"<DSMLinvoke",
" name=\"get_weather\">",
"<DSMLparameter name=\"city\" "
"string=\"true\">",
"Paris",
"</DSMLparameter>",
"</DSMLinvoke>",
"\n",
"</DSMLfunction_calls>"};
int tool_calls_found = 0;
for (const auto& chunk : chunks) {
auto stream_result = detector_->parse_streaming_increment(chunk, tools_);
for (const auto& call : stream_result.calls) {
if (call.name.has_value()) {
tool_calls_found++;
}
}
}
EXPECT_EQ(tool_calls_found, 2)
<< "Should find 2 tool calls in streaming mode";
}
// Test streaming response with trailing empty markdown json fence before
// tool-call chunks. The empty fence should not leak to normal_text.
TEST_F(DeepSeek32DetectorTest,
StreamingSkipsTrailingEmptyJsonFenceBeforeToolCall) {
std::vector<std::string> chunks = {
"Let me break this down.\n\nNow I'll call the tool.\n\n```json\n",
"\n```",
"<DSMLinvoke name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">Boston</DSMLparameter></"
"DSMLinvoke>"};
std::string accumulated_normal_text;
int tool_name_events = 0;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
for (const auto& call : result.calls) {
if (call.name.has_value()) {
tool_name_events++;
}
}
}
EXPECT_EQ(tool_name_events, 1);
EXPECT_TRUE(accumulated_normal_text.find("```json") == std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("Now I'll call the tool") !=
std::string::npos);
}
// Test streaming response with trailing placeholder punctuation and fence at
// stream end. These artifacts should be removed from flushed normal_text.
TEST_F(DeepSeek32DetectorTest,
StreamingFlushStripsTrailingPlaceholderAndFence) {
std::vector<std::string> chunks = {
"Let me break this down.\n\n"
"The user is asking for weather info.\n\n"
"I will call the weather tool now.\n\n"
"、、、\n"
"```\n"};
std::string accumulated_normal_text;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
}
auto flush_result = detector_->parse_streaming_increment("", tools_);
if (!flush_result.normal_text.empty()) {
accumulated_normal_text += flush_result.normal_text;
}
EXPECT_TRUE(accumulated_normal_text.find(
"I will call the weather tool now.") != std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("、、、") == std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("\n\n\n") == std::string::npos);
EXPECT_TRUE(!accumulated_normal_text.empty() &&
accumulated_normal_text.back() != ' ');
}
// Test standalone placeholder/fence chunks right before invoke start.
// They should not leak into streamed normal_text.
TEST_F(DeepSeek32DetectorTest,
StreamingDropsStandalonePlaceholderChunkBeforeToolCall) {
std::vector<std::string> chunks = {
"I'll use the weather tool for Boston.\n\n",
"、、、",
"\n```",
"<DSMLinvoke name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">Boston</DSMLparameter></"
"DSMLinvoke>"};
std::string accumulated_normal_text;
int tool_name_events = 0;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
for (const auto& call : result.calls) {
if (call.name.has_value()) {
tool_name_events++;
}
}
}
EXPECT_EQ(tool_name_events, 1);
EXPECT_TRUE(
accumulated_normal_text.find("I'll use the weather tool for Boston.") !=
std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("、、、") == std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("```") == std::string::npos);
}
// Test that deferred trailing whitespace does not grow into large blank blocks
// across chunks.
TEST_F(DeepSeek32DetectorTest,
StreamingDeferredWhitespaceDoesNotAccumulateBlankLines) {
std::vector<std::string> chunks = {"First line.\n\n\n", "Second line."};
std::string accumulated_normal_text;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
}
EXPECT_TRUE(accumulated_normal_text.find("First line.") != std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("Second line.") !=
std::string::npos);
EXPECT_TRUE(accumulated_normal_text.find("\n\n\n") == std::string::npos);
}
// Test tool call without function_calls wrapper
TEST_F(DeepSeek32DetectorTest, ToolCallWithoutWrapper) {
std::string text =
"Direct invoke "
"<DSMLinvoke name=\"get_weather\"><DSMLparameter "
"name=\"city\" string=\"true\">Shanghai</DSMLparameter></"
"DSMLinvoke>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Direct invoke");
ASSERT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].name.value(), "get_weather");
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,663 @@
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "deepseekv3_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace xllm {
namespace function_call {
class DeepSeekV3DetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<DeepSeekV3Detector>();
// Setup test tools
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"location",
{{"type", "string"},
{"description", "The city and state, e.g. San Francisco, CA"}}},
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
{"required", {"location"}}};
JsonFunction weather_func("get_current_weather",
"Get the current weather in a given location",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
std::unique_ptr<DeepSeekV3Detector> detector_;
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
// Test constructor and basic properties
TEST_F(DeepSeekV3DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection
std::string text_with_tool_call =
"Some text "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>test\n`"
"``json\n{\"name\": "
"\"test\"}\n```<tool▁call▁end><tool▁calls▁end>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(DeepSeekV3DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<tool▁calls▁begin>"));
EXPECT_TRUE(detector_->has_tool_call(
"Previous text <tool▁calls▁begin>Following content"));
EXPECT_TRUE(detector_->has_tool_call(
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>test\n`"
"``json\n{\"name\": "
"\"test\"}\n```<tool▁call▁end><tool▁calls▁end>"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(detector_->has_tool_call("tool_calls without special tokens"));
EXPECT_FALSE(detector_->has_tool_call("<tool_call> without unicode tokens"));
}
// Test single tool call parsing
TEST_F(DeepSeekV3DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": \"Beijing\", \"unit\": "
"\"celsius\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["unit"], "celsius");
}
// Test multiple tool calls parsing
TEST_F(DeepSeekV3DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate an expression "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": "
"\"Shanghai\"}\n```<tool▁call▁end>\n<tool▁call▁begin>function<"
"tool▁sep>calculate\n```json\n{\"expression\": \"2 + 3 * "
"4\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate an expression");
EXPECT_EQ(result.calls.size(), 2);
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_current_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["location"], "Shanghai");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "calculate");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
}
// Test DeepSeekV3 specific format with exact tokens
TEST_F(DeepSeekV3DetectorTest, DeepSeekV3SpecificFormat) {
std::string text =
"I need weather info "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": "
"\"Tokyo\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "I need weather info");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Tokyo");
}
// Test invalid JSON handling
TEST_F(DeepSeekV3DetectorTest, InvalidJsonHandling) {
std::string text =
"Test invalid JSON "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": \"Beijing\", "
"invalid_json}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test invalid JSON");
EXPECT_EQ(result.calls.size(), 0); // Invalid JSON should be ignored
}
// Test empty tool call content
TEST_F(DeepSeekV3DetectorTest, EmptyToolCallContent) {
std::string text =
"Test empty content "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>\n```"
"json\n \t\n \n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content");
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
}
// Test incomplete tool call (only start tag)
TEST_F(DeepSeekV3DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": \"Beijing\"}";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(DeepSeekV3DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>"
"unknown_tool\n```json\n{\"param\": "
"\"value\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool");
// Base class will skip unknown tools, so should be 0 calls
EXPECT_EQ(result.calls.size(), 0);
}
// Test case with only normal text
TEST_F(DeepSeekV3DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(DeepSeekV3DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test whitespace-only input
TEST_F(DeepSeekV3DetectorTest, WhitespaceOnlyInput) {
std::string text = " \t\n\r ";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
}
// Test complex nested JSON parameters
TEST_F(DeepSeekV3DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": \"Beijing\", \"options\": "
"{\"include_forecast\": true, \"days\": 7, \"details\": "
"[\"temperature\", \"humidity\", "
"\"wind\"]}}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
EXPECT_EQ(params["options"]["details"].size(), 3);
}
// Test tool call in the middle of text
TEST_F(DeepSeekV3DetectorTest, ToolCallInMiddleOfText) {
std::string text =
"Previous text "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>"
"calculate\n```json\n{\"expression\": "
"\"1+1\"}\n```<tool▁call▁end><tool▁calls▁end> Following text";
auto result = detector_->detect_and_parse(text, tools_);
// Note: According to implementation, only text before tool call is preserved
// as normal_text
EXPECT_EQ(result.normal_text, "Previous text");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
}
// Test special characters handling
TEST_F(DeepSeekV3DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": \"New York City\", \"note\": "
"\"Contains "
"symbols@#$%^&*()_+=\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Test whitespace trimming
TEST_F(DeepSeekV3DetectorTest, WhitespaceTrimming) {
std::string text_with_whitespace =
" \t\nPrevious text\r\n "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n{\"location\": "
"\"Beijing\"}\n```<tool▁call▁end><tool▁calls▁end> \t\r\n";
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
// Verify normal text is correctly trimmed
EXPECT_EQ(result.normal_text, "Previous text");
// Verify tool call is correctly parsed
EXPECT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].tool_index, -1); // Base class always returns -1
}
// Test regex pattern matching edge cases
TEST_F(DeepSeekV3DetectorTest, RegexPatternEdgeCases) {
// Test with newlines in function name (should fail)
std::string text1 =
"Test "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current\nweather\n```json\n{\"location\": "
"\"Beijing\"}\n```<tool▁call▁end><tool▁calls▁end>";
auto result1 = detector_->detect_and_parse(text1, tools_);
EXPECT_EQ(result1.calls.size(), 0); // Should fail to match
// Test with missing json markers
std::string text2 =
"Test "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n{\"location\": "
"\"Beijing\"}\n<tool▁call▁end><tool▁calls▁end>";
auto result2 = detector_->detect_and_parse(text2, tools_);
EXPECT_EQ(result2.calls.size(),
0); // Should fail to match without ```json``` markers
}
// Performance test: multiple tool calls
TEST_F(DeepSeekV3DetectorTest, PerformanceWithMultipleToolCalls) {
std::string text = "Performance test";
// Build text containing multiple tool calls
for (int i = 0; i < 10000; ++i) {
text +=
" <tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>"
"calculate\n```json\n{\"expression\": \"" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"\"}\n```<tool▁call▁end><tool▁calls▁end>";
}
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test");
EXPECT_EQ(result.calls.size(), 10000);
// Verify each tool call is correctly parsed
for (int i = 0; i < 10000; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Test error handling with malformed tokens
TEST_F(DeepSeekV3DetectorTest, MalformedTokensHandling) {
// Test with incomplete start token
std::string text1 =
"Test "
"<tool▁calls▁begi><tool▁call▁begin>function<tool▁sep>test\n```"
"json\n{}\n```<tool▁call▁end><tool▁calls▁end>";
auto result1 = detector_->detect_and_parse(text1, tools_);
EXPECT_EQ(result1.normal_text,
"Test "
"<tool▁calls▁begi><tool▁call▁begin>function<tool▁sep>"
"test\n```json\n{}\n```<tool▁call▁end><tool▁calls▁end>");
EXPECT_EQ(result1.calls.size(), 0);
// Test with incomplete end token
std::string text2 =
"Test "
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>test\n`"
"``json\n{}\n```<tool▁call▁end><tool▁calls▁en>";
auto result2 = detector_->detect_and_parse(text2, tools_);
EXPECT_EQ(result2.normal_text, "Test");
EXPECT_EQ(result2.calls.size(), 0); // Should not match incomplete pattern
}
// ========== Streaming Tests ==========
// Test basic streaming parsing
TEST_F(DeepSeekV3DetectorTest, BasicStreamingParsing) {
std::vector<std::string> chunks = {
"<tool▁calls▁begin>",
"<tool▁call▁begin>function<tool▁sep>get_current_weather\n```"
"json\n",
"{\"location\": \"Tokyo\"}",
"\n```<tool▁call▁end>",
"<tool▁calls▁end>"};
std::vector<StreamingParseResult> results;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
results.push_back(result);
}
bool found_tool_name = false;
bool found_arguments = false;
for (const auto& result : results) {
if (!result.calls.empty()) {
for (const auto& call : result.calls) {
if (call.name.has_value() &&
call.name.value() == "get_current_weather") {
found_tool_name = true;
}
if (!call.parameters.empty() &&
call.parameters.find("Tokyo") != std::string::npos) {
found_arguments = true;
}
}
}
}
EXPECT_TRUE(found_tool_name) << "Should find tool name in streaming results";
EXPECT_TRUE(found_arguments) << "Should find arguments in streaming results";
}
// Test incremental argument streaming
TEST_F(DeepSeekV3DetectorTest, IncrementalArgumentStreaming) {
std::vector<std::string> chunks = {
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n",
"{\"location\":",
" \"San Francisco\"",
", \"unit\": \"celsius\"",
"}\n```<tool▁call▁end><tool▁calls▁end>"};
std::string accumulated_args;
bool tool_name_sent = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
for (const auto& call : result.calls) {
if (call.name.has_value()) {
tool_name_sent = true;
EXPECT_EQ(call.name.value(), "get_current_weather");
} else {
accumulated_args += call.parameters;
}
}
}
EXPECT_TRUE(tool_name_sent)
<< "Tool name should be sent when tool call is complete";
if (!accumulated_args.empty()) {
EXPECT_TRUE(accumulated_args.find("San Francisco") != std::string::npos)
<< "Should contain location argument";
EXPECT_TRUE(accumulated_args.find("celsius") != std::string::npos)
<< "Should contain unit argument";
}
}
// Test streaming with multiple tool calls
TEST_F(DeepSeekV3DetectorTest, StreamingMultipleToolCalls) {
std::vector<std::string> chunks = {"<tool▁calls▁begin>",
"\n",
"<tool▁call▁begin>",
"function",
"<tool▁sep>",
"get_current_weather",
"\n",
"```",
"json",
"\n",
"{",
"\"location\"",
":",
" \"",
"Tokyo",
"\"}",
"\n",
"```",
"<tool▁call▁end>",
"\n",
"<tool▁call▁begin>",
"function",
"<tool▁sep>",
"get_current_weather",
"\n",
"```",
"json",
"\n",
"{",
"\"location\"",
":",
" \"",
"Paris",
"\",",
" \"",
"unit\"",
":",
" \"",
"celsius",
"\"}",
"\n",
"```",
"<tool▁call▁end>",
"\n",
"<tool▁calls▁end>"};
int tool_calls_found = 0;
for (const auto& chunk : chunks) {
auto stream_result = detector_->parse_streaming_increment(chunk, tools_);
for (const auto& call : stream_result.calls) {
if (call.name.has_value()) {
tool_calls_found++;
}
}
}
EXPECT_EQ(tool_calls_found, 2)
<< "Should find 2 tool calls in streaming mode";
}
// Test normal text handling during streaming
TEST_F(DeepSeekV3DetectorTest, StreamingNormalTextHandling) {
std::vector<std::string> chunks = {
"This is normal text before tool call. ",
"<tool▁calls▁begin><tool▁call▁begin>function<tool▁sep>get_"
"current_weather\n```json\n",
"{\"location\": \"Tokyo\"}\n```<tool▁call▁end><tool▁calls▁end>",
" And this is text after."};
std::string accumulated_normal_text;
bool found_tool_call = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
if (!result.calls.empty()) {
found_tool_call = true;
}
}
EXPECT_TRUE(found_tool_call) << "Should find tool call";
EXPECT_TRUE(accumulated_normal_text.find("This is normal text") !=
std::string::npos)
<< "Should preserve normal text before tool call";
// Note: We don't expect "And this is text after" to be in
// accumulated_normal_text
}
// Test partial token handling
TEST_F(DeepSeekV3DetectorTest, StreamingPartialTokenHandling) {
std::vector<std::string> chunks = {
"<tool▁calls▁beg", // Partial start token
"in><tool▁call▁begin>function<tool▁sep>get_current_weather\n```"
"json\n",
"{\"location\": \"Tokyo\"}\n```<tool▁call▁e", // Partial end token
"nd><tool▁calls▁end>"};
bool found_tool_call = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.calls.empty()) {
for (const auto& call : result.calls) {
if (call.name.has_value() &&
call.name.value() == "get_current_weather") {
found_tool_call = true;
}
}
}
}
EXPECT_TRUE(found_tool_call) << "Should handle partial tokens correctly";
}
// Test streaming with empty chunks
TEST_F(DeepSeekV3DetectorTest, StreamingEmptyChunks) {
std::vector<std::string> chunks = {
"",
"<tool▁calls▁begin>",
"",
"<tool▁call▁begin>function<tool▁sep>get_current_weather\n```"
"json\n",
"",
"{\"location\": \"Tokyo\"}",
"",
"\n```<tool▁call▁end><tool▁calls▁end>",
""};
bool found_tool_call = false;
for (const auto& chunk : chunks) {
auto result = detector_->parse_streaming_increment(chunk, tools_);
if (!result.calls.empty()) {
for (const auto& call : result.calls) {
if (call.name.has_value() &&
call.name.value() == "get_current_weather") {
found_tool_call = true;
}
}
}
}
EXPECT_TRUE(found_tool_call) << "Should handle empty chunks correctly";
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,499 @@
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "glm45_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace xllm {
namespace function_call {
class Glm45DetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<Glm45Detector>();
// Setup test tools
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"location",
{{"type", "string"},
{"description", "The city and state, e.g. San Francisco, CA"}}},
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
{"required", {"location"}}};
JsonFunction weather_func("get_current_weather",
"Get the current weather in a given location",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
std::unique_ptr<Glm45Detector> detector_;
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
// Test constructor and basic properties
TEST_F(Glm45DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection
std::string text_with_tool_call =
"Some text "
"<tool_call>test\n<arg_key>param</arg_key>\n<arg_value>value</"
"arg_value>\n</tool_call>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(Glm45DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
EXPECT_TRUE(
detector_->has_tool_call("Previous text <tool_call>Following content"));
EXPECT_TRUE(detector_->has_tool_call(
"<tool_call>get_weather\n<arg_key>city</arg_key>\n<arg_value>Beijing</"
"arg_value>\n</tool_call>"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
EXPECT_FALSE(detector_->has_tool_call("<tool_call without closing"));
}
// Test single tool call parsing
TEST_F(Glm45DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
"<arg_key>unit</arg_key>\n<arg_value>celsius</arg_value>\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["unit"], "celsius");
}
// Test multiple tool calls parsing
TEST_F(Glm45DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate an expression "
"<tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Shanghai</arg_value>\n</"
"tool_call>"
"<tool_call>calculate\n"
"<arg_key>expression</arg_key>\n<arg_value>2 + 3 * "
"4</arg_value>\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate an expression");
EXPECT_EQ(result.calls.size(), 2);
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_current_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["location"], "Shanghai");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "calculate");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
}
// Test GLM-4.5 specific format with Chinese characters
TEST_F(Glm45DetectorTest, Glm45SpecificFormatWithChinese) {
std::string text =
"I need weather info "
"<tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>北京</arg_value>\n"
"<arg_key>unit</arg_key>\n<arg_value>celsius</arg_value>\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "I need weather info");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "北京");
EXPECT_EQ(params["unit"], "celsius");
}
// Test empty tool call content
TEST_F(Glm45DetectorTest, EmptyToolCallContent) {
std::string text =
"Test empty content <tool_call>test\n \t\n \n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content");
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
}
// Test incomplete tool call (only start tag)
TEST_F(Glm45DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(Glm45DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool <tool_call>unknown_tool\n"
"<arg_key>param</arg_key>\n<arg_value>value</arg_value>\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool");
// Base class will skip unknown tools, so should be 0 calls
EXPECT_EQ(result.calls.size(), 0);
}
// Test case with only normal text
TEST_F(Glm45DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(Glm45DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test whitespace-only input
TEST_F(Glm45DetectorTest, WhitespaceOnlyInput) {
std::string text = " \t\n\r ";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
}
// Test complex nested parameters (JSON values)
TEST_F(Glm45DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
"<arg_key>options</arg_key>\n<arg_value>{\"include_forecast\": true, "
"\"days\": 7}</arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
}
// Test special characters handling
TEST_F(Glm45DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>New York City</arg_value>\n"
"<arg_key>note</arg_key>\n<arg_value>Contains "
"symbols@#$%^&*()_+=</arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Test tool call in the middle of text
TEST_F(Glm45DetectorTest, ToolCallInMiddleOfText) {
std::string text =
"Previous text <tool_call>calculate\n"
"<arg_key>expression</arg_key>\n<arg_value>1+1</arg_value>\n"
"</tool_call> Following text";
auto result = detector_->detect_and_parse(text, tools_);
// Note: According to GLM-4.5 implementation, only text before tool call is
// preserved as normal_text
EXPECT_EQ(result.normal_text, "Previous text");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
}
// Test malformed arg tags handling
TEST_F(Glm45DetectorTest, MalformedArgTagsHandling) {
std::string text =
"Malformed args test <tool_call>get_current_weather\n"
"<arg_key>location</arg_key><arg_value>Beijing</arg_value>\n" // Missing
// newline
"<arg_key>unit<arg_value>celsius</arg_value>\n" // Missing closing tag
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Malformed args test");
// Should still parse what it can
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
// The malformed unit parameter should not be parsed
EXPECT_FALSE(params.contains("unit"));
}
// Test whitespace handling in arg values
TEST_F(Glm45DetectorTest, WhitespaceHandlingInArgValues) {
std::string text =
"Whitespace test <tool_call>get_current_weather\n"
"<arg_key> location </arg_key>\n<arg_value> Beijing </arg_value>\n"
"<arg_key>\t\nunit\r\n</arg_key>\n<arg_value>\n\tcelsius\r\n</"
"arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Whitespace test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing"); // Whitespace should be trimmed
EXPECT_EQ(params["unit"], "celsius");
}
// Test multiple sections (edge case)
TEST_F(Glm45DetectorTest, MultipleSections) {
std::string text =
"First section <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
"</tool_call> Middle text <tool_call>calculate\n"
"<arg_key>expression</arg_key>\n<arg_value>1+1</arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
// Should extract text before first tool call
EXPECT_EQ(result.normal_text, "First section");
// Should parse all tool calls
EXPECT_EQ(result.calls.size(), 2);
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result.calls[1].name.value(), "calculate");
}
// Test performance with many tool calls
TEST_F(Glm45DetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test ";
// Build text containing multiple tool calls
for (int i = 0; i < 100; ++i) { // Reduced from 10000 for faster testing
text +=
"<tool_call>calculate\n"
"<arg_key>expression</arg_key>\n<arg_value>" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"</arg_value>\n</tool_call>";
}
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test");
EXPECT_EQ(result.calls.size(), 100);
// Verify each tool call is correctly parsed
for (int i = 0; i < 100; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Test edge case: nested braces in JSON values
TEST_F(Glm45DetectorTest, NestedBracesInJsonValues) {
std::string text =
"Nested braces test <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
"<arg_key>config</arg_key>\n<arg_value>{\"nested\": {\"deep\": "
"\"value\"}}</arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Nested braces test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
}
// Test streaming parsing functionality
TEST_F(Glm45DetectorTest, StreamingParseBasicFunctionality) {
std::string chunk1 = "<tool_call>get_current_weather\n";
std::string chunk2 = "<arg_key>location</arg_key>\n<arg_value>";
std::string chunk3 = "Beijing</arg_value>\n</tool_call>";
// First chunk - should buffer and wait for complete tool call
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.normal_text, "");
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - still incomplete
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.normal_text, "");
EXPECT_EQ(result2.calls.size(), 0);
// Third chunk - completes the tool call
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.normal_text, "");
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
}
// Test streaming parse with normal text
TEST_F(Glm45DetectorTest, StreamingParseWithNormalText) {
std::string chunk1 = "Please check the weather ";
std::string chunk2 = "<tool_call>get_current_weather\n";
std::string chunk3 =
"<arg_key>location</arg_key>\n<arg_value>Tokyo</arg_value>\n</tool_call>";
// First chunk - normal text should be returned
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.normal_text, "Please check the weather ");
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - tool call start, should be buffered
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.normal_text, "");
EXPECT_EQ(result2.calls.size(), 0);
// Third chunk - complete tool call
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.normal_text, "");
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
}
// Test invalid JSON in arg values
TEST_F(Glm45DetectorTest, InvalidJsonInArgValues) {
std::string text =
"Invalid JSON test <tool_call>get_current_weather\n"
"<arg_key>location</arg_key>\n<arg_value>Beijing</arg_value>\n"
"<arg_key>config</arg_key>\n<arg_value>{invalid json}</arg_value>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Invalid JSON test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["config"], "{invalid json}"); // Should be treated as string
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,767 @@
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "glm47_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace xllm {
namespace function_call {
class Glm47DetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<Glm47Detector>();
// Setup test tools
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"city",
{{"type", "string"},
{"description", "The city name, e.g. Beijing, Shanghai"}}},
{"date",
{{"type", "string"},
{"description", "Date in YYYY-MM-DD format"}}}}},
{"required", {"city"}}};
JsonFunction weather_func("get_weather",
"Get the weather information for a given city",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}},
{"precision",
{{"type", "number"}, {"description", "Number of decimal places"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
std::unique_ptr<Glm47Detector> detector_;
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
// Test constructor and basic properties
TEST_F(Glm47DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection (GLM-4.7 compact format)
std::string text_with_tool_call =
"Some text "
"<tool_call>test<arg_key>param</arg_key><arg_value>value</arg_value></"
"tool_call>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(Glm47DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
EXPECT_TRUE(
detector_->has_tool_call("Previous text <tool_call>Following content"));
EXPECT_TRUE(detector_->has_tool_call(
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</"
"arg_value></tool_call>"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
EXPECT_FALSE(detector_->has_tool_call("<tool_call without closing"));
}
// Test single tool call parsing (GLM-4.7 compact format)
TEST_F(Glm47DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "北京");
EXPECT_EQ(params["date"], "2024-06-27");
}
// Test multiple tool calls parsing (GLM-4.7 format)
TEST_F(Glm47DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>上海</arg_value>"
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>"
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate");
ASSERT_EQ(result.calls.size(), 2); // Use ASSERT to stop test if this fails
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, -1);
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["city"], "上海");
EXPECT_EQ(params1["date"], "2024-06-27");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, -1);
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "get_weather");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["city"], "北京");
EXPECT_EQ(params2["date"], "2024-06-27");
}
// Test GLM-4.7 specific compact format
TEST_F(Glm47DetectorTest, Glm47CompactFormat) {
std::string text =
"Weather query "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
"arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Weather query");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
}
// Test number type coercion
TEST_F(Glm47DetectorTest, NumberTypeCoercion) {
std::string text =
"Calculate with precision "
"<tool_call>calculate<arg_key>expression</arg_key><arg_value>3.14 * "
"2</arg_value>"
"<arg_key>precision</arg_key><arg_value>2</arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Calculate with precision");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["expression"], "3.14 * 2");
// precision should be parsed as number
EXPECT_TRUE(params["precision"].is_number());
EXPECT_EQ(params["precision"], 2);
}
// Test empty tool call content
TEST_F(Glm47DetectorTest, EmptyToolCallContent) {
std::string text = "Test empty content <tool_call>test</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content");
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
}
// Test incomplete tool call
TEST_F(Glm47DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(Glm47DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool "
"<tool_call>unknown_tool<arg_key>param</arg_key><arg_value>value</"
"arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool");
// Base class will skip unknown tools
EXPECT_EQ(result.calls.size(), 0);
}
// Test case with only normal text
TEST_F(Glm47DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(Glm47DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test complex nested JSON parameters
TEST_F(Glm47DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
"arg_value>"
"<arg_key>options</arg_key><arg_value>{\"include_forecast\": true, "
"\"days\": 7}</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1);
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
}
// Test special characters handling
TEST_F(Glm47DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>New York "
"City</arg_value>"
"<arg_key>note</arg_key><arg_value>Contains "
"symbols@#$%^&*()_+=</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1);
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Test whitespace handling in arg values
TEST_F(Glm47DetectorTest, WhitespaceHandlingInArgValues) {
std::string text =
"Whitespace test "
"<tool_call>get_weather<arg_key> city </arg_key><arg_value> Beijing "
"</arg_value>"
"<arg_key>\t\ndate\r\n</arg_key><arg_value>\n\t2024-06-27\r\n</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Whitespace test");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing"); // Whitespace should be trimmed
EXPECT_EQ(params["date"], "2024-06-27");
}
// Test streaming parsing functionality
TEST_F(Glm47DetectorTest, StreamingParseBasicFunctionality) {
std::string chunk1 = "<tool_call>get_weather";
std::string chunk2 = "<arg_key>city</arg_key><arg_value>";
std::string chunk3 = "Beijing</arg_value></tool_call>";
// First chunk - function name not yet complete (no <arg_key> or </tool_call>)
// Should wait for more data to avoid sending partial names
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - now we have <arg_key>, so function name is complete
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.calls.size(), 1);
EXPECT_TRUE(result2.calls[0].name.has_value());
EXPECT_EQ(result2.calls[0].name.value(), "get_weather");
// Third chunk - completes the tool call
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 0); // Should complete the JSON
}
// Test streaming parse with normal text
TEST_F(Glm47DetectorTest, StreamingParseWithNormalText) {
std::string chunk1 = "Please check the weather ";
std::string chunk2 = "<tool_call>get_weather";
std::string chunk3 =
"<arg_key>city</arg_key><arg_value>Tokyo</arg_value></tool_call>";
// First chunk - normal text should be returned
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.normal_text, "Please check the weather ");
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - tool call start, but function name not yet complete
// (no <arg_key> or </tool_call>), should wait for more data
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.calls.size(), 0);
// Third chunk - now we have <arg_key>, function name is complete
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
EXPECT_EQ(result3.calls[0].name.value(), "get_weather");
}
// Test invalid JSON in arg values
TEST_F(Glm47DetectorTest, InvalidJsonInArgValues) {
std::string text =
"Invalid JSON test "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
"arg_value>"
"<arg_key>config</arg_key><arg_value>{invalid json}</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Invalid JSON test");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_EQ(params["config"], "{invalid json}"); // Should be treated as string
}
// Test nested braces in JSON values
TEST_F(Glm47DetectorTest, NestedBracesInJsonValues) {
std::string text =
"Nested braces test "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</"
"arg_value>"
"<arg_key>config</arg_key><arg_value>{\"nested\": {\"deep\": "
"\"value\"}}</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Nested braces test");
ASSERT_EQ(result.calls.size(), 1); // Use ASSERT to stop test if this fails
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1);
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "Beijing");
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
}
// Test performance with many tool calls
TEST_F(Glm47DetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test ";
// Build text containing multiple tool calls
for (int i = 0; i < 100; ++i) {
text += "<tool_call>calculate<arg_key>expression</arg_key><arg_value>" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"</arg_value></tool_call>";
}
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test");
ASSERT_EQ(result.calls.size(), 100); // Use ASSERT to stop test if this fails
// Verify each tool call is correctly parsed
for (int i = 0; i < 100; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, -1);
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Regression test for issue #751: std::regex stack overflow on large payloads
// The old regex implementation with [\s\S]*? pattern caused O(n) recursion
// depth, leading to stack overflow on inputs larger than ~46KB (depending on
// stack size). The fix uses string::find() and substr() for O(1) stack usage.
TEST_F(Glm47DetectorTest, LargePayloadNoStackOverflow) {
// Create a tool that accepts large content
nlohmann::json write_params = {
{"type", "object"},
{"properties",
{{"filename", {{"type", "string"}, {"description", "Filename"}}},
{"content",
{{"type", "string"}, {"description", "Content to write"}}}}},
{"required", {"filename", "content"}}};
JsonFunction write_func("write_file", "Write content to file", write_params);
JsonTool write_tool("function", write_func);
std::vector<JsonTool> tools = {write_tool};
// Test with 50KB payload (larger than the ~46KB that caused stack overflow)
std::string large_content(50000, 'A');
std::string text =
"Test "
"<tool_call>write_file"
"<arg_key>filename</arg_key><arg_value>test.txt</arg_value>"
"<arg_key>content</arg_key><arg_value>" +
large_content +
"</arg_value>"
"</tool_call>";
// This would crash with stack overflow before the fix
auto result = detector_->detect_and_parse(text, tools);
EXPECT_EQ(result.normal_text, "Test");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "write_file");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["filename"], "test.txt");
EXPECT_EQ(params["content"].get<std::string>().size(), 50000);
}
// Test with Chinese content to validate UTF-8 handling with large payloads
TEST_F(Glm47DetectorTest, LargeChinesePayloadNoStackOverflow) {
nlohmann::json write_params = {
{"type", "object"},
{"properties",
{{"filename", {{"type", "string"}}}, {"content", {{"type", "string"}}}}},
{"required", {"filename", "content"}}};
JsonFunction write_func("write_file", "Write content to file", write_params);
JsonTool write_tool("function", write_func);
std::vector<JsonTool> tools = {write_tool};
// Generate ~50KB of Chinese content (each char is 3 bytes in UTF-8)
std::string chinese_char = ""; // 3 bytes in UTF-8
std::string large_content;
large_content.reserve(50000);
for (int i = 0; i < 16667; ++i) { // 16667 * 3 ≈ 50KB
large_content += chinese_char;
}
std::string text =
"<tool_call>write_file"
"<arg_key>filename</arg_key><arg_value>中文.txt</arg_value>"
"<arg_key>content</arg_key><arg_value>" +
large_content +
"</arg_value>"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools);
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.name.value(), "write_file");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["filename"], "中文.txt");
// Verify content length (each Chinese char is 3 bytes)
EXPECT_GE(params["content"].get<std::string>().size(), 49000);
}
// Test streaming with large payload
TEST_F(Glm47DetectorTest, StreamingLargePayloadNoStackOverflow) {
nlohmann::json write_params = {
{"type", "object"},
{"properties", {{"content", {{"type", "string"}}}}},
{"required", {"content"}}};
JsonFunction write_func("write_file", "Write content", write_params);
JsonTool write_tool("function", write_func);
std::vector<JsonTool> tools = {write_tool};
// Build a large tool call incrementally (simulating streaming)
std::string large_content(20000, 'X'); // 20KB for streaming test
std::string full_text =
"<tool_call>write_file"
"<arg_key>content</arg_key><arg_value>" +
large_content +
"</arg_value>"
"</tool_call>";
// Simulate streaming by sending chunks
// Create fresh detector for streaming test
auto streaming_detector = std::make_unique<Glm47Detector>();
std::vector<StreamingParseResult> results;
size_t chunk_size = 1000; // 1KB chunks
for (size_t i = 0; i < full_text.size(); i += chunk_size) {
std::string chunk = full_text.substr(i, chunk_size);
auto result = streaming_detector->parse_streaming_increment(chunk, tools);
results.push_back(result);
}
// Verify we got the function name and completed without crash
bool found_name = false;
bool found_args = false;
for (const auto& r : results) {
for (const auto& call : r.calls) {
if (call.name.has_value() && call.name.value() == "write_file") {
found_name = true;
}
if (!call.parameters.empty()) {
found_args = true;
}
}
}
EXPECT_TRUE(found_name);
EXPECT_TRUE(found_args);
}
// =============================================================================
// UTF-8 Streaming Tests
// =============================================================================
// These tests verify that multi-byte UTF-8 characters are handled correctly
// when split across streaming chunks. The fix buffers incomplete UTF-8
// sequences until the next chunk completes them.
// Test streaming with Chinese characters split across chunks
// "北京" = 0xE5 0x8C 0x97 (北) + 0xE4 0xBA 0xAC (京)
TEST_F(Glm47DetectorTest, StreamingParseWithChineseCharactersSplit) {
// Chunk 1: Function name and key, start of value
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
// Chunk 2: First 2 bytes of "北" (0xE5 0x8C) - incomplete 3-byte sequence
std::string chunk2 = "\xE5\x8C";
// Chunk 3: Last byte of "北" (0x97) + first 2 bytes of "京" (0xE4 0xBA)
std::string chunk3 = "\x97\xE4\xBA";
// Chunk 4: Last byte of "京" (0xAC) + closing tags
std::string chunk4 = "\xAC</arg_value></tool_call>";
// Process each chunk - should not crash with UTF-8 errors
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
// Function name is returned when <arg_key> is seen
EXPECT_GE(result1.calls.size(), 1);
if (result1.calls.size() > 0) {
EXPECT_TRUE(result1.calls[0].name.has_value());
EXPECT_EQ(result1.calls[0].name.value(), "get_weather");
}
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
// Should buffer incomplete UTF-8, not crash
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
// Should complete first character and buffer second incomplete
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
// Should complete the tool call
EXPECT_GE(result4.calls.size(), 1);
}
// Test with single Chinese character split (simpler case)
TEST_F(Glm47DetectorTest, StreamingParseWithSingleChineseCharacterSplit) {
// "北" = 0xE5 0x8C 0x97 (3-byte UTF-8)
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
std::string chunk2 = "\xE5\x8C"; // First 2 bytes (incomplete)
std::string chunk3 = "\x97</arg_value></tool_call>"; // Last byte + closing
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
// Should not crash with "invalid UTF-8 byte at index 1: 0xE5"
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test with 4-byte UTF-8 emoji split
// Emoji "😊" = 0xF0 0x9F 0x98 0x8A (4-byte UTF-8)
TEST_F(Glm47DetectorTest, StreamingParseWithEmojiSplit) {
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Tokyo ";
std::string chunk2 = "\xF0\x9F"; // First 2 bytes of emoji (incomplete)
std::string chunk3 =
"\x98\x8A</arg_value></tool_call>"; // Last 2 bytes + closing
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
// Should buffer incomplete 4-byte sequence
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test with complete UTF-8 characters (no splitting needed)
TEST_F(Glm47DetectorTest, StreamingParseWithCompleteUtf8) {
// Complete Chinese characters in single chunks
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
std::string chunk2 = "北京"; // Complete UTF-8 characters
std::string chunk3 = "</arg_value></tool_call>";
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
// Function name is returned when <arg_key> is seen
EXPECT_GE(result1.calls.size(), 1);
if (result1.calls.size() > 0) {
EXPECT_TRUE(result1.calls[0].name.has_value());
EXPECT_EQ(result1.calls[0].name.value(), "get_weather");
}
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test with mixed UTF-8 and ASCII
TEST_F(Glm47DetectorTest, StreamingParseWithMixedUtf8AndAscii) {
// Mix of ASCII and Chinese with UTF-8 split at boundary
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>City: ";
std::string chunk2 = "\xE5"; // First byte of "北" (incomplete)
std::string chunk3 =
"\x8C\x97京</arg_value></tool_call>"; // Rest of "北" + complete "京"
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
// Should buffer the single incomplete byte
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test with Japanese characters (also 3-byte UTF-8)
// "東京" = 0xE6 0x9D 0xB1 (東) + 0xE4 0xBA 0xAC (京)
TEST_F(Glm47DetectorTest, StreamingParseWithJapaneseCharactersSplit) {
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
std::string chunk2 = "\xE6\x9D"; // First 2 bytes of "東" (incomplete)
std::string chunk3 = "\xB1\xE4\xBA\xAC</arg_value></tool_call>"; // Rest
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test with Korean characters (also 3-byte UTF-8)
// "서울" = 0xEC 0x84 0x9C (서) + 0xEC 0x9A 0xB8 (울)
TEST_F(Glm47DetectorTest, StreamingParseWithKoreanCharactersSplit) {
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
std::string chunk2 = "\xEC\x84"; // First 2 bytes of "서" (incomplete)
std::string chunk3 = "\x9C\xEC\x9A\xB8</arg_value></tool_call>"; // Rest
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_GE(result3.calls.size(), 1);
}
// Test UTF-8 boundary: one byte at a time for 3-byte char
TEST_F(Glm47DetectorTest, StreamingParseWithUtf8OneByteAtATime) {
// Split "北" (0xE5 0x8C 0x97) one byte at a time
std::string chunk1 =
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>";
std::string chunk2 = "\xE5"; // First byte
std::string chunk3 = "\x8C"; // Second byte
std::string chunk4 = "\x97</arg_value></tool_call>"; // Third byte + closing
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
EXPECT_GE(result4.calls.size(), 1);
}
// Test UTF-8 in non-streaming (full parse) mode still works
TEST_F(Glm47DetectorTest, NonStreamingParseWithUtf8) {
std::string text =
"Query for city "
"<tool_call>get_weather<arg_key>city</arg_key><arg_value>北京</arg_value>"
"<arg_key>date</arg_key><arg_value>2024-06-27</arg_value></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Query for city");
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["city"], "北京");
EXPECT_EQ(params["date"], "2024-06-27");
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,641 @@
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "kimik2_detector.h"
#include <gtest/gtest.h>
#include <iostream>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace xllm {
namespace function_call {
class KimiK2DetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<KimiK2Detector>();
// Setup test tools
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"location",
{{"type", "string"},
{"description", "The city and state, e.g. San Francisco, CA"}}},
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
{"required", {"location"}}};
JsonFunction weather_func("get_current_weather",
"Get the current weather in a given location",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
std::unique_ptr<KimiK2Detector> detector_;
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
// Test constructor and basic properties
TEST_F(KimiK2DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection
std::string text_with_tool_call =
"Some text "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0 "
"<|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(KimiK2DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<|tool_calls_section_begin|>"));
EXPECT_TRUE(detector_->has_tool_call(
"Previous text <|tool_calls_section_begin|>Following content"));
EXPECT_TRUE(detector_->has_tool_call(
"<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0 "
"<|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(
detector_->has_tool_call("tool_calls_section_begin without brackets"));
EXPECT_FALSE(
detector_->has_tool_call("<tool_calls_section_begin without pipes"));
}
// Test trim_whitespace method (indirectly tested through public interface)
TEST_F(KimiK2DetectorTest, TrimWhitespaceHandling) {
std::string text_with_whitespace =
" \t\nPrevious text\r\n "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": "
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|> \t\r\n";
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
// Verify normal text is correctly extracted
EXPECT_EQ(result.normal_text, " \t\nPrevious text\r\n ");
// Verify tool call is correctly parsed
EXPECT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].tool_index, 0);
}
// Test single tool call parsing
TEST_F(KimiK2DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
"\"unit\": \"celsius\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["unit"], "celsius");
}
// Test multiple tool calls parsing
TEST_F(KimiK2DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate an expression "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": "
"\"Shanghai\"}<|tool_call_end|><|tool_call_begin|>functions.calculate:1 "
"<|tool_call_argument_begin|>{\"expression\": \"2 + 3 * "
"4\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate an expression ");
EXPECT_EQ(result.calls.size(), 2);
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, 0);
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_current_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["location"], "Shanghai");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, 1);
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "calculate");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
}
// Test invalid JSON handling
TEST_F(KimiK2DetectorTest, InvalidJsonHandling) {
std::string text =
"Test invalid JSON "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
"invalid_json}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test invalid JSON ");
// KimiK2 detector should still parse the call even with invalid JSON, leaving
// JSON validation to higher levels
EXPECT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
}
// Test empty tool call content
TEST_F(KimiK2DetectorTest, EmptyToolCallContent) {
std::string text =
"Test empty content "
"<|tool_calls_section_begin|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content ");
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
}
// Test incomplete tool call (only start tag)
TEST_F(KimiK2DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call ");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(KimiK2DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.unknown_tool:0 "
"<|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool ");
// KimiK2 detector should parse the call regardless of whether the tool is
// known
EXPECT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].name.value(), "unknown_tool");
}
// Test case with only normal text
TEST_F(KimiK2DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(KimiK2DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test whitespace-only input
TEST_F(KimiK2DetectorTest, WhitespaceOnlyInput) {
std::string text = " \t\n\r ";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, " \t\n\r ");
EXPECT_EQ(result.calls.size(), 0);
}
// Test complex nested JSON parameters
TEST_F(KimiK2DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
"\"options\": {\"include_forecast\": true, \"days\": 7, \"details\": "
"[\"temperature\", \"humidity\", "
"\"wind\"]}}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
EXPECT_EQ(params["options"]["details"].size(), 3);
}
// Test tool call in the middle of text
TEST_F(KimiK2DetectorTest, ToolCallInMiddleOfText) {
std::string text =
"Previous text "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:0 "
"<|tool_call_argument_begin|>{\"expression\": "
"\"1+1\"}<|tool_call_end|><|tool_calls_section_end|> Following text";
auto result = detector_->detect_and_parse(text, tools_);
// Note: According to KimiK2 implementation, only text before tool call
// section is preserved as normal_text
EXPECT_EQ(result.normal_text, "Previous text ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
EXPECT_EQ(call.name.value(), "calculate");
}
// Test special characters handling
TEST_F(KimiK2DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": \"New York City\", "
"\"note\": \"Contains "
"symbols@#$%^&*()_+=\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Test function name extraction
TEST_F(KimiK2DetectorTest, FunctionNameExtraction) {
std::string text =
"Function name test "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.my_custom_"
"function:5 <|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Function name test ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 5); // Should extract the index correctly
EXPECT_EQ(call.name.value(), "my_custom_function");
}
// Test malformed function ID handling
TEST_F(KimiK2DetectorTest, MalformedFunctionIdHandling) {
std::string text =
"Malformed ID test "
"<|tool_calls_section_begin|><|tool_call_begin|>invalid_format "
"<|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Malformed ID test ");
// Malformed format that doesn't match regex should result in no calls
EXPECT_EQ(result.calls.size(), 0);
}
// Test malformed function ID that matches regex but has invalid format
TEST_F(KimiK2DetectorTest, MalformedButMatchingFunctionId) {
std::string text =
"Malformed but matching test "
"<|tool_calls_section_begin|><|tool_call_begin|>invalid.format:0 "
"<|tool_call_argument_begin|>{\"param\": "
"\"value\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Malformed but matching test ");
// Should parse but with empty function name due to missing "functions."
// prefix
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), ""); // Empty function name for malformed ID
EXPECT_EQ(call.tool_index, 0);
}
// Test multiple sections (edge case)
TEST_F(KimiK2DetectorTest, MultipleSections) {
std::string text =
"First section "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": "
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|> Middle text "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:1 "
"<|tool_call_argument_begin|>{\"expression\": "
"\"1+1\"}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
// Should extract text before first section
EXPECT_EQ(result.normal_text, "First section ");
// Should parse all tool calls from all sections
EXPECT_EQ(result.calls.size(), 2);
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result.calls[1].name.value(), "calculate");
}
// Performance test: many tool calls
TEST_F(KimiK2DetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test <|tool_calls_section_begin|>";
// Build text containing multiple tool calls
for (int i = 0; i < 10000; ++i) {
text += "<|tool_call_begin|>functions.calculate:" + std::to_string(i) +
" <|tool_call_argument_begin|>{\"expression\": \"" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"\"}<|tool_call_end|>";
}
text += "<|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test ");
EXPECT_EQ(result.calls.size(), 10000);
// Verify each tool call is correctly parsed
for (int i = 0; i < 10000; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, i);
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Test edge case: nested braces in JSON
TEST_F(KimiK2DetectorTest, NestedBracesInJson) {
std::string text =
"Nested braces test "
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": \"Beijing\", "
"\"config\": {\"nested\": {\"deep\": "
"\"value\"}}}<|tool_call_end|><|tool_calls_section_end|>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Nested braces test ");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["config"]["nested"]["deep"], "value");
}
// Test streaming parsing functionality
TEST_F(KimiK2DetectorTest, StreamingParseBasicFunctionality) {
std::string chunk1 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>";
std::string chunk2 = "{\"location\": ";
std::string chunk3 =
"\"Beijing\"}<|tool_call_end|><|tool_calls_section_end|>";
// First chunk - no calls returned yet, waiting for arguments to start
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.normal_text, "");
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - function name sent with empty parameters when arguments
// start
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.normal_text, "");
EXPECT_EQ(result2.calls.size(), 1);
EXPECT_TRUE(result2.calls[0].name.has_value());
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result2.calls[0].parameters, "");
// Third chunk - incremental arguments sent
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.normal_text, "");
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_FALSE(result3.calls[0].name.has_value());
EXPECT_EQ(result3.calls[0].parameters, "{\"location\": \"Beijing\"}");
}
TEST_F(KimiK2DetectorTest, StreamingParseWithNormalText) {
// Test streaming parsing with normal text before tool call
std::string chunk1 = "Please check the weather ";
std::string chunk2 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>";
std::string chunk3 =
"{\"location\": \"Tokyo\"}<|tool_call_end|><|tool_calls_section_end|>";
// First chunk - normal text
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.normal_text, "Please check the weather ");
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - tool call start (no calls returned yet, waiting for
// arguments)
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.normal_text, "");
EXPECT_EQ(result2.calls.size(), 0);
// Third chunk - complete arguments (function name sent with empty parameters
// first)
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.normal_text, "");
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_TRUE(result3.calls[0].name.has_value());
EXPECT_EQ(result3.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result3.calls[0].parameters, "");
}
TEST_F(KimiK2DetectorTest, StreamingParseMultipleToolCalls) {
// Test streaming parsing with multiple tool calls
std::string chunk1 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>";
std::string chunk2 = "{\"location\": \"Beijing\"}<|tool_call_end|>";
std::string chunk3 =
"<|tool_call_begin|>functions.calculate:1 <|tool_call_argument_begin|>";
std::string chunk4 =
"{\"expression\": \"2+3\"}<|tool_call_end|><|tool_calls_section_end|>";
// First chunk - no calls returned yet, waiting for arguments
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - first tool call completes, function name sent with empty
// parameters then arguments are sent separately
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.calls.size(), 1);
EXPECT_TRUE(result2.calls[0].name.has_value());
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result2.calls[0].parameters, "");
// Third chunk - second tool call starts, first tool's arguments are sent
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_FALSE(
result3.calls[0].name.has_value()); // Only parameters for first tool
EXPECT_EQ(result3.calls[0].parameters, "{\"location\": \"Beijing\"}");
// Fourth chunk - second tool call completes
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
EXPECT_EQ(result4.calls.size(), 1);
EXPECT_TRUE(result4.calls[0].name.has_value());
EXPECT_EQ(result4.calls[0].name.value(), "calculate");
EXPECT_EQ(result4.calls[0].parameters, "");
}
TEST_F(KimiK2DetectorTest, StreamingParseIncrementalArguments) {
// Test streaming parsing with arguments arriving in small chunks
std::string chunk1 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>";
std::string chunk2 = "{";
std::string chunk3 = "\"location\":";
std::string chunk4 = " \"Shanghai\",";
std::string chunk5 = " \"unit\": \"celsius\"";
std::string chunk6 = "}<|tool_call_end|><|tool_calls_section_end|>";
// First chunk - no calls returned yet, waiting for arguments
auto result1 = detector_->parse_streaming_increment(chunk1, tools_);
EXPECT_EQ(result1.calls.size(), 0);
// Second chunk - function name sent with empty parameters when arguments
// start
auto result2 = detector_->parse_streaming_increment(chunk2, tools_);
EXPECT_EQ(result2.calls.size(), 1);
EXPECT_TRUE(result2.calls[0].name.has_value());
EXPECT_EQ(result2.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result2.calls[0].parameters, "");
// Incremental argument chunks - each sends the incremental part
auto result3 = detector_->parse_streaming_increment(chunk3, tools_);
EXPECT_EQ(result3.calls.size(), 1);
EXPECT_FALSE(result3.calls[0].name.has_value());
EXPECT_EQ(result3.calls[0].parameters, "{\"location\":");
auto result4 = detector_->parse_streaming_increment(chunk4, tools_);
EXPECT_EQ(result4.calls.size(), 1);
EXPECT_FALSE(result4.calls[0].name.has_value());
EXPECT_EQ(result4.calls[0].parameters, " \"Shanghai\",");
auto result5 = detector_->parse_streaming_increment(chunk5, tools_);
EXPECT_EQ(result5.calls.size(), 1);
EXPECT_FALSE(result5.calls[0].name.has_value());
EXPECT_EQ(result5.calls[0].parameters, " \"unit\": \"celsius\"");
// Complete arguments - final chunk
auto result6 = detector_->parse_streaming_increment(chunk6, tools_);
EXPECT_EQ(result6.calls.size(), 1);
EXPECT_FALSE(result6.calls[0].name.has_value());
EXPECT_EQ(result6.calls[0].parameters, "}");
}
TEST_F(KimiK2DetectorTest, StreamingParseStateReset) {
// Test that streaming state is properly reset between different parsing
// sessions
// First complete tool call
std::string text1 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_"
"weather:0 <|tool_call_argument_begin|>{\"location\": "
"\"Beijing\"}<|tool_call_end|>";
auto result1 = detector_->parse_streaming_increment(text1, tools_);
// Create a new detector to simulate fresh state
auto fresh_detector = std::make_unique<KimiK2Detector>();
// Second complete tool call with fresh detector
std::string text2 =
"<|tool_calls_section_begin|><|tool_call_begin|>functions.calculate:0 "
"<|tool_call_argument_begin|>{\"expression\": \"1+1\"}<|tool_call_end|>";
auto result2 = fresh_detector->parse_streaming_increment(text2, tools_);
EXPECT_EQ(result2.calls.size(), 1);
EXPECT_EQ(result2.calls[0].name.value(), "calculate");
EXPECT_EQ(result2.calls[0].tool_index,
0); // Should start from 0 with fresh state
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,13 @@
include(cc_test)
cc_test(
NAME
partial_json_parser_test
SRCS
test_examples.cpp
test_property_based.cpp
DEPS
:partial_json_parser
GTest::gtest
GTest::gtest_main
)

View File

@@ -0,0 +1,191 @@
#include <gtest/gtest.h>
#include <cmath>
#include "partial_json_parser/options.h"
#include "partial_json_parser/parser.h"
using namespace partial_json_parser;
// Test string parsing
TEST(PartialJsonParserTest, TestStr) {
// Test incomplete string with STR option
EXPECT_EQ(parse_malformed_string("\"", STR), "\"\"");
// Test incomplete string without STR option should throw
EXPECT_THROW(parse_malformed_string("\"", static_cast<TypeOptions>(~STR)),
MalformedJSONException);
// Test escaped backslash
EXPECT_EQ(parse_malformed_string("\"\\\\", STR), "\"\\\\\"");
// Test unicode escape sequences
EXPECT_EQ(parse_malformed_string("\"\\\\u", STR), "\"\\\\u\"");
EXPECT_EQ(parse_malformed_string("\"\\\\U\\\\u", STR), "\"\\\\U\\\\u\"");
}
// Test array parsing
TEST(PartialJsonParserTest, TestArr) {
// Test incomplete array with ARR option only
EXPECT_EQ(parse_malformed_string("[\"", ARR), "[]");
// Test incomplete array with both ARR and STR options
EXPECT_EQ(parse_malformed_string("[\"", static_cast<TypeOptions>(ARR | STR)),
"[\"\"]");
// Test various incomplete arrays without proper options should throw
EXPECT_THROW(parse_malformed_string("[", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("[\"", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("[\"\",", STR), MalformedJSONException);
}
// Test object parsing
TEST(PartialJsonParserTest, TestObj) {
// Test incomplete object with OBJ option only
EXPECT_EQ(parse_malformed_string("{\"\": \"", OBJ), "{}");
// Test incomplete object with both OBJ and STR options
EXPECT_EQ(
parse_malformed_string("{\"\": \"", static_cast<TypeOptions>(OBJ | STR)),
"{\"\": \"\"}");
// Test various incomplete objects without proper options should throw
EXPECT_THROW(parse_malformed_string("{", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{\"", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{\"\"]", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{\"\":", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{\"\":\"", STR), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{\"\":\"\"]", STR),
MalformedJSONException);
}
// Test singleton values
TEST(PartialJsonParserTest, TestSingletons) {
// Test null
EXPECT_EQ(parse_malformed_string("n", NULL_TYPE), "null");
EXPECT_THROW(
parse_malformed_string("n", static_cast<TypeOptions>(~NULL_TYPE)),
MalformedJSONException);
// Test boolean true
EXPECT_EQ(parse_malformed_string("t", BOOL), "true");
EXPECT_THROW(parse_malformed_string("t", static_cast<TypeOptions>(~BOOL)),
MalformedJSONException);
// Test boolean false
EXPECT_EQ(parse_malformed_string("f", BOOL), "false");
EXPECT_THROW(parse_malformed_string("f", static_cast<TypeOptions>(~BOOL)),
MalformedJSONException);
// Test Infinity
EXPECT_EQ(parse_malformed_string("I", INF), "Infinity");
EXPECT_THROW(
parse_malformed_string("I", static_cast<TypeOptions>(~INFINITY_TYPE)),
MalformedJSONException);
// Test negative Infinity
EXPECT_EQ(parse_malformed_string("-I", INF), "-Infinity");
EXPECT_THROW(
parse_malformed_string("-I", static_cast<TypeOptions>(~NEG_INFINITY)),
MalformedJSONException);
// Test NaN
EXPECT_EQ(parse_malformed_string("N", NAN_TYPE), "NaN");
EXPECT_THROW(parse_malformed_string("N", static_cast<TypeOptions>(~NAN_TYPE)),
MalformedJSONException);
}
// Test number parsing
TEST(PartialJsonParserTest, TestNum) {
// Test complete numbers (should work without NUM option)
EXPECT_EQ(parse_malformed_string("0", static_cast<TypeOptions>(~NUM)), "0");
EXPECT_EQ(parse_malformed_string("-1.25e+4", static_cast<TypeOptions>(~NUM)),
"-1.25e+4");
// Test incomplete numbers (need NUM option)
EXPECT_EQ(parse_malformed_string("-1.25e+", NUM), "-1.25");
EXPECT_EQ(parse_malformed_string("-1.25e", NUM), "-1.25");
}
// Test error cases
TEST(PartialJsonParserTest, TestError) {
// Test unexpected characters
EXPECT_THROW(parse_malformed_string("a", ALL), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("{0", ALL), MalformedJSONException);
EXPECT_THROW(parse_malformed_string("--", ALL), MalformedJSONException);
}
// Test basic functionality
TEST(PartialJsonParserTest, TestBasicFunctionality) {
// Test basic incomplete JSON structures
EXPECT_EQ(parse_malformed_string("[", ALL), "[]");
EXPECT_EQ(parse_malformed_string("[0.", ALL), "[0]");
EXPECT_EQ(parse_malformed_string("{\"key\": ", ALL), "{}");
EXPECT_EQ(parse_malformed_string("t", ALL), "true");
// Test with restricted options
EXPECT_EQ(parse_malformed_string("[1", static_cast<TypeOptions>(~NUM)), "[]");
EXPECT_EQ(parse_malformed_string("1", static_cast<TypeOptions>(~NUM)), "1");
// Test error case
EXPECT_THROW(parse_malformed_string("-", ALL), MalformedJSONException);
}
// Test complex nested structures
TEST(PartialJsonParserTest, TestComplexStructures) {
// Test nested array with incomplete string
std::string result1 =
parse_malformed_string("[\"a\", \"b", static_cast<TypeOptions>(~STR));
EXPECT_EQ(result1, "[\"a\"]");
// Test nested object with incomplete values
std::string result2 =
parse_malformed_string("{\"key1\": 123, \"key2\": \"val", ALL);
EXPECT_EQ(result2, "{\"key1\": 123, \"key2\": \"val\"}");
// Test deeply nested structure
std::string result3 =
parse_malformed_string("{\"arr\": [1, 2, {\"nested\":", ALL);
EXPECT_EQ(result3, "{\"arr\": [1, 2, {}]}");
}
// Test edge cases
TEST(PartialJsonParserTest, TestEdgeCases) {
// Test empty string
EXPECT_THROW(parse_malformed_string("", ALL), MalformedJSONException);
// Test whitespace only
EXPECT_THROW(parse_malformed_string(" ", ALL), MalformedJSONException);
// Test single characters
EXPECT_EQ(parse_malformed_string("n", NULL_TYPE), "null");
EXPECT_EQ(parse_malformed_string("t", BOOL), "true");
EXPECT_EQ(parse_malformed_string("f", BOOL), "false");
// Test numbers with trailing operators
EXPECT_EQ(parse_malformed_string("123.", NUM), "123");
EXPECT_EQ(parse_malformed_string("123e", NUM), "123");
EXPECT_EQ(parse_malformed_string("123e+", NUM), "123");
EXPECT_EQ(parse_malformed_string("123e-", NUM), "123");
}
// Test format parameter
TEST(PartialJsonParserTest, TestFormatParameter) {
std::string result1 = parse_malformed_string("{\"foo\":\"bar", ALL, false);
EXPECT_EQ(result1, "{\"foo\":\"bar\"}");
std::string result2 = parse_malformed_string("{\"foo\":\"bar", ALL, true);
EXPECT_EQ(result2, "{\n \"foo\": \"bar\"\n}");
}
// Test Go example
TEST(PartialJsonParserTest, TestGoExample) {
// Test the example: `{"foo":"bar`
std::string result = parse_malformed_string("{\"foo\":\"bar", ALL);
EXPECT_EQ(result, "{\"foo\":\"bar\"}");
// Test the array example: `["a",{"a":123`
std::string result2 = parse_malformed_string(
"[\"a\",{\"a\":123", static_cast<TypeOptions>(NUM | ARR | OBJ));
EXPECT_EQ(result2, "[\"a\",{\"a\":123}]");
}

View File

@@ -0,0 +1,353 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <nlohmann/json.hpp>
#include <random>
#include <sstream>
#include <string>
#include <vector>
#include "partial_json_parser/options.h"
#include "partial_json_parser/parser.h"
using namespace partial_json_parser;
using json = nlohmann::json;
// JSON value generator class
class JsonGenerator {
private:
std::mt19937 rng;
std::uniform_int_distribution<int> int_dist;
std::uniform_real_distribution<double> float_dist;
std::uniform_int_distribution<int> bool_dist;
std::uniform_int_distribution<int> type_dist;
std::uniform_int_distribution<int> size_dist;
std::uniform_int_distribution<int> char_dist;
public:
JsonGenerator(unsigned seed = std::random_device{}())
: rng(seed),
int_dist(-1000, 1000),
float_dist(-1000.0, 1000.0),
bool_dist(0, 1),
type_dist(
0,
5), // 6 basic types: null, bool, int, float, string, array, object
size_dist(0, 5), // max collection size
char_dist(32, 126) // printable ASCII
{}
json generateJson(int depth = 0, int maxDepth = 3) {
if (depth >= maxDepth) {
// Generate only primitive types at max depth
int type = type_dist(rng) % 4; // null, bool, int, float, string
return generatePrimitive(type);
}
int type = type_dist(rng);
switch (type) {
case 0:
return json(nullptr); // null
case 1:
return json(bool_dist(rng) == 1); // bool
case 2:
return json(int_dist(rng)); // int
case 3:
return json(float_dist(rng)); // float
case 4:
return generateString(); // string
case 5:
return generateArray(depth, maxDepth); // array
default:
return generateObject(depth, maxDepth); // object
}
}
private:
json generatePrimitive(int type) {
switch (type) {
case 0:
return json(nullptr); // null
case 1:
return json(bool_dist(rng) == 1); // bool
case 2:
return json(int_dist(rng)); // int
case 3:
return json(float_dist(rng)); // float
default:
return generateString(); // string
}
}
json generateString() {
int length = size_dist(rng);
std::string str;
for (int i = 0; i < length; ++i) {
char c = static_cast<char>(char_dist(rng));
// Avoid problematic characters for JSON
if (c == '"' || c == '\\' || c < 32) {
c = 'a' + (i % 26);
}
str += c;
}
return json(str);
}
json generateArray(int depth, int maxDepth) {
json array = json::array();
int size = size_dist(rng);
for (int i = 0; i < size; ++i) {
array.push_back(generateJson(depth + 1, maxDepth));
}
return array;
}
json generateObject(int depth, int maxDepth) {
json object = json::object();
int size = size_dist(rng);
for (int i = 0; i < size; ++i) {
std::string key = "key" + std::to_string(i);
object[key] = generateJson(depth + 1, maxDepth);
}
return object;
}
};
// Helper function to convert json to string
std::string jsonToString(const json& value) { return value.dump(); }
// Helper function to parse JSON string back to json
json parse_json_string(const std::string& json_str) {
try {
return json::parse(json_str);
} catch (const json::parse_error& e) {
throw std::runtime_error("Failed to parse JSON: " + std::string(e.what()));
}
}
// Test class for property-based testing of partial JSON parser
class PartialJsonParserPropertyTest : public ::testing::Test {
protected:
JsonGenerator generator;
static const int FINE_JSON_EXAMPLES =
100; // Reduced from 333 for faster testing
static const int PARTIAL_JSON_EXAMPLES =
100; // Reduced from 333 for faster testing
void SetUp() override { generator = JsonGenerator(); }
};
// Test that complete JSON strings are parsed correctly
TEST_F(PartialJsonParserPropertyTest, TestFineJson) {
for (int i = 0; i < FINE_JSON_EXAMPLES; ++i) {
json originalJson = generator.generateJson();
std::string json_string = jsonToString(originalJson);
try {
// Parse with our parser
std::string result = parse_malformed_string(json_string, ALL, false);
// Parse both original and result with standard JSON parser
json original_parsed = parse_json_string(json_string);
json result_parsed = parse_json_string(result);
// They should be equivalent
EXPECT_EQ(original_parsed, result_parsed)
<< "Original: " << json_string << "\nResult: " << result;
} catch (const std::exception& e) {
// If our parser fails, the original should also be invalid JSON
// This is acceptable for some edge cases
GTEST_SKIP() << "Skipping invalid JSON: " << json_string
<< " Error: " << e.what();
}
}
}
// Test that partial JSON strings can be completed
TEST_F(PartialJsonParserPropertyTest, TestPartialJson) {
for (int i = 0; i < PARTIAL_JSON_EXAMPLES; ++i) {
json originalJson = generator.generateJson();
std::string json_string = jsonToString(originalJson);
if (json_string.empty()) continue;
// Test various prefixes of the JSON string
int step = std::max(1, static_cast<int>(json_string.length()) / 10);
for (size_t pos = 1; pos < json_string.length(); pos += step) {
std::string partial_json = json_string.substr(0, pos);
// Skip if starts with '-' (known problematic case)
if (partial_json[0] == '-' && partial_json.length() == 1) {
continue;
}
try {
std::string result = parse_malformed_string(partial_json, ALL, false);
// The result should be valid JSON
json result_parsed = parse_json_string(result);
// Basic sanity check: result should not be empty
EXPECT_FALSE(result.empty())
<< "Empty result for partial JSON: " << partial_json;
} catch (const MalformedJSONException& e) {
// Some partial JSONs are expected to fail
// This is acceptable behavior
continue;
} catch (const std::exception& e) {
FAIL() << "Unexpected exception for partial JSON: " << partial_json
<< " Error: " << e.what();
}
}
}
}
// Test specific edge cases that caused issues in Go version
TEST_F(PartialJsonParserPropertyTest, TestKnownEdgeCases) {
// Test cases from Go test files that should fail
std::vector<std::string> shouldFail = {
"{0", // Invalid object key
"--", // Invalid number
"a", // Invalid character
"", // Empty string
" " // Whitespace only
};
for (const auto& test_case : shouldFail) {
EXPECT_THROW(parse_malformed_string(test_case, ALL, false),
MalformedJSONException)
<< "Expected failure for: " << test_case;
}
}
// Test specific cases that should succeed
TEST_F(PartialJsonParserPropertyTest, TestKnownSuccessCases) {
struct TestCase {
std::string input;
std::string expected;
TypeOptions options;
};
std::vector<TestCase> test_cases = {
{"[", "[]", ALL},
{"[0.", "[0]", ALL},
{"{\"key\": ", "{}", ALL},
{"t", "true", ALL},
{"\"", "\"\"", STR},
{"[\"", "[\"\"]", static_cast<TypeOptions>(ARR | STR)},
{"{\"foo\":\"bar", "{\"foo\":\"bar\"}", ALL}};
for (const auto& test_case : test_cases) {
try {
std::string result =
parse_malformed_string(test_case.input, test_case.options, false);
EXPECT_EQ(result, test_case.expected)
<< "Input: " << test_case.input << " Expected: " << test_case.expected
<< " Got: " << result;
} catch (const std::exception& e) {
FAIL() << "Unexpected exception for input: " << test_case.input
<< " Error: " << e.what();
}
}
}
// Test option restrictions
TEST_F(PartialJsonParserPropertyTest, TestOptionRestrictions) {
struct TestCase {
std::string input;
TypeOptions allowedOptions;
bool shouldSucceed;
};
std::vector<TestCase> test_cases = {
{"\"", STR, true},
{"\"", static_cast<TypeOptions>(~STR), false},
{"[", ARR, true},
{"[", STR, false},
{"{", OBJ, true},
{"{", STR, false},
{"t", BOOL, true},
{"t", static_cast<TypeOptions>(~BOOL), false},
{"n", NULL_TYPE, true},
{"n", static_cast<TypeOptions>(~NULL_TYPE), false}};
for (const auto& test_case : test_cases) {
if (test_case.shouldSucceed) {
EXPECT_NO_THROW(parse_malformed_string(
test_case.input, test_case.allowedOptions, false))
<< "Expected success for: " << test_case.input
<< " with options: " << test_case.allowedOptions;
} else {
EXPECT_THROW(parse_malformed_string(
test_case.input, test_case.allowedOptions, false),
MalformedJSONException)
<< "Expected failure for: " << test_case.input
<< " with options: " << test_case.allowedOptions;
}
}
}
// Performance test - ensure reasonable performance on large inputs
TEST_F(PartialJsonParserPropertyTest, TestPerformance) {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; ++i) {
json json = generator.generateJson(0, 2); // Smaller depth for performance
std::string json_string = jsonToString(json);
if (!json_string.empty()) {
try {
parse_malformed_string(json_string, ALL, false);
} catch (const MalformedJSONException&) {
// Expected for some cases
}
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Should complete 1000 operations in reasonable time (less than 10 seconds)
EXPECT_LT(duration.count(), 10000)
<< "Performance test took too long: " << duration.count() << "ms";
}
// Test consistency between different option combinations
TEST_F(PartialJsonParserPropertyTest, TestOptionConsistency) {
std::vector<std::string> testInputs = {
"[1,2,3", "{\"a\":1,\"b\":", "\"hello", "123.", "true", "null"};
for (const auto& input : testInputs) {
// Test that ALL option works when individual options work
bool individualSuccess = false;
std::string individual_result;
std::vector<TypeOptions> individualOptions = {
STR, NUM, ARR, OBJ, NULL_TYPE, BOOL};
for (auto option : individualOptions) {
try {
individual_result = parse_malformed_string(input, option, false);
individualSuccess = true;
break;
} catch (const MalformedJSONException&) {
continue;
}
}
if (individualSuccess) {
// ALL option should also succeed
EXPECT_NO_THROW({
std::string all_result = parse_malformed_string(input, ALL, false);
// Results might differ, but both should be valid
EXPECT_FALSE(all_result.empty());
}) << "ALL option failed when individual option succeeded for: "
<< input;
}
}
}

View File

@@ -0,0 +1,545 @@
/* Copyright 2025 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "qwen25_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include "core_types.h"
#include "function_call_parser.h"
namespace xllm {
namespace function_call {
class Qwen25TestBase : public ::testing::Test {
protected:
void SetUp() override {
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"location",
{{"type", "string"},
{"description", "The city and state, e.g. San Francisco, CA"}}},
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}}}},
{"required", {"location"}}};
JsonFunction weather_func("get_current_weather",
"Get the current weather in a given location",
weather_params);
weather_tool_ = JsonTool("function", weather_func);
nlohmann::json calculator_params = {
{"type", "object"},
{"properties",
{{"expression",
{{"type", "string"},
{"description", "Mathematical expression to evaluate"}}}}},
{"required", {"expression"}}};
JsonFunction calculator_func(
"calculate", "Calculate mathematical expressions", calculator_params);
calculator_tool_ = JsonTool("function", calculator_func);
tools_ = {weather_tool_, calculator_tool_};
}
JsonTool weather_tool_;
JsonTool calculator_tool_;
std::vector<JsonTool> tools_;
};
class Qwen25DetectorTest : public Qwen25TestBase {
protected:
void SetUp() override {
Qwen25TestBase::SetUp();
detector_ = std::make_unique<Qwen25Detector>();
}
std::unique_ptr<Qwen25Detector> detector_;
};
class Qwen25StreamingTest : public Qwen25TestBase {
protected:
void SetUp() override { Qwen25TestBase::SetUp(); }
};
// Test constructor and basic properties
TEST_F(Qwen25DetectorTest, ConstructorInitializesCorrectly) {
EXPECT_NE(detector_, nullptr);
// Test basic token detection
std::string text_with_tool_call =
"Some text <tool_call>\n{\"name\": \"test\"}\n</tool_call>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
// Test has_tool_call method
TEST_F(Qwen25DetectorTest, HasToolCallDetection) {
// Test text containing tool calls
EXPECT_TRUE(detector_->has_tool_call("<tool_call>\n"));
EXPECT_TRUE(
detector_->has_tool_call("Previous text <tool_call>\nFollowing content"));
EXPECT_TRUE(detector_->has_tool_call(
"<tool_call>\n{\"name\": \"test\"}\n</tool_call>"));
// Test text not containing tool calls
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("Regular text"));
EXPECT_FALSE(detector_->has_tool_call("tool_call without brackets"));
EXPECT_FALSE(detector_->has_tool_call("<tool_call without newline"));
}
// Test trim_whitespace method (indirectly tested through public interface)
TEST_F(Qwen25DetectorTest, TrimWhitespaceHandling) {
std::string text_with_whitespace =
" \t\nPrevious text\r\n <tool_call>\n {\"name\": "
"\"get_current_weather\", \"arguments\": {\"location\": \"Beijing\"}} "
"\n</tool_call> \t\r\n";
auto result = detector_->detect_and_parse(text_with_whitespace, tools_);
// Verify normal text is correctly trimmed
EXPECT_EQ(result.normal_text, "Previous text");
// Verify tool call is correctly parsed
EXPECT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].tool_index, -1); // Base class always returns -1
}
// Test single tool call parsing
TEST_F(Qwen25DetectorTest, SingleToolCallParsing) {
std::string text =
"Please help me check the weather <tool_call>\n{\"name\": "
"\"get_current_weather\", \"arguments\": {\"location\": \"Beijing\", "
"\"unit\": \"celsius\"}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Please help me check the weather");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
// Verify parameter JSON
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_EQ(params["unit"], "celsius");
}
// Test multiple tool calls parsing
TEST_F(Qwen25DetectorTest, MultipleToolCallsParsing) {
std::string text =
"Please help me check the weather and calculate an expression "
"<tool_call>\n{\"name\": \"get_current_weather\", \"arguments\": "
"{\"location\": \"Shanghai\"}}\n</tool_call>\n<tool_call>\n{\"name\": "
"\"calculate\", \"arguments\": {\"expression\": \"2 + 3 * "
"4\"}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"Please help me check the weather and calculate an expression");
EXPECT_EQ(result.calls.size(), 2);
// Verify first tool call
const auto& call1 = result.calls[0];
EXPECT_EQ(call1.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call1.name.has_value());
EXPECT_EQ(call1.name.value(), "get_current_weather");
nlohmann::json params1 = nlohmann::json::parse(call1.parameters);
EXPECT_EQ(params1["location"], "Shanghai");
// Verify second tool call
const auto& call2 = result.calls[1];
EXPECT_EQ(call2.tool_index, -1); // Base class always returns -1
EXPECT_TRUE(call2.name.has_value());
EXPECT_EQ(call2.name.value(), "calculate");
nlohmann::json params2 = nlohmann::json::parse(call2.parameters);
EXPECT_EQ(params2["expression"], "2 + 3 * 4");
}
// Test invalid JSON handling
TEST_F(Qwen25DetectorTest, InvalidJsonHandling) {
std::string text =
"Test invalid JSON <tool_call>\n{\"name\": \"get_current_weather\", "
"\"arguments\": {\"location\": \"Beijing\", invalid_json}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test invalid JSON");
EXPECT_EQ(result.calls.size(), 0); // Invalid JSON should be ignored
}
// Test empty tool call content
TEST_F(Qwen25DetectorTest, EmptyToolCallContent) {
std::string text = "Test empty content <tool_call>\n \t\n \n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Test empty content");
EXPECT_EQ(result.calls.size(), 0); // Empty content should be ignored
}
// Test incomplete tool call (only start tag)
TEST_F(Qwen25DetectorTest, IncompleteToolCall) {
std::string text =
"Incomplete tool call <tool_call>\n{\"name\": \"get_current_weather\"";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Incomplete tool call");
EXPECT_EQ(result.calls.size(), 0); // Incomplete calls should be ignored
}
// Test unknown tool name handling
TEST_F(Qwen25DetectorTest, UnknownToolName) {
std::string text =
"Unknown tool <tool_call>\n{\"name\": \"unknown_tool\", \"arguments\": "
"{\"param\": \"value\"}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Unknown tool");
// Base class will skip unknown tools, so should be 0 calls
EXPECT_EQ(result.calls.size(), 0);
}
// Test case with only normal text
TEST_F(Qwen25DetectorTest, OnlyNormalText) {
std::string text = "This is a regular text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text,
"This is a regular text without any tool calls.");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test empty string input
TEST_F(Qwen25DetectorTest, EmptyStringInput) {
std::string text = "";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
EXPECT_FALSE(result.has_calls());
}
// Test whitespace-only input
TEST_F(Qwen25DetectorTest, WhitespaceOnlyInput) {
std::string text = " \t\n\r ";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "");
EXPECT_EQ(result.calls.size(), 0);
}
// Test complex nested JSON parameters
TEST_F(Qwen25DetectorTest, ComplexNestedJsonParameters) {
std::string text =
"Complex parameter test <tool_call>\n{\"name\": \"get_current_weather\", "
"\"arguments\": {\"location\": \"Beijing\", \"options\": "
"{\"include_forecast\": true, \"days\": 7, \"details\": "
"[\"temperature\", \"humidity\", \"wind\"]}}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Complex parameter test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Beijing");
EXPECT_TRUE(params["options"]["include_forecast"]);
EXPECT_EQ(params["options"]["days"], 7);
EXPECT_EQ(params["options"]["details"].size(), 3);
}
// Test tool call in the middle of text
TEST_F(Qwen25DetectorTest, ToolCallInMiddleOfText) {
std::string text =
"Previous text <tool_call>\n{\"name\": \"calculate\", \"arguments\": "
"{\"expression\": \"1+1\"}}\n</tool_call> Following text";
auto result = detector_->detect_and_parse(text, tools_);
// Note: According to implementation, only text before tool call is preserved
// as normal_text
EXPECT_EQ(result.normal_text, "Previous text");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
}
// Test special characters handling
TEST_F(Qwen25DetectorTest, SpecialCharactersHandling) {
std::string text =
"Special characters test <tool_call>\n{\"name\": "
"\"get_current_weather\", \"arguments\": {\"location\": \"New York "
"City\", \"note\": \"Contains symbols@#$%^&*()_+=\"}}\n</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Special characters test");
EXPECT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "New York City");
EXPECT_EQ(params["note"], "Contains symbols@#$%^&*()_+=");
}
// Performance test: many tool calls
TEST_F(Qwen25DetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test";
// Build text containing multiple tool calls
for (int i = 0; i < 10000; ++i) {
text +=
" <tool_call>\n{\"name\": \"calculate\", \"arguments\": "
"{\"expression\": \"" +
std::to_string(i) + " + " + std::to_string(i + 1) +
"\"}}\n</tool_call>";
}
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test");
EXPECT_EQ(result.calls.size(), 10000);
// Verify each tool call is correctly parsed
for (int i = 0; i < 10000; ++i) {
const auto& call = result.calls[i];
EXPECT_EQ(call.tool_index, -1); // Base class always returns -1
EXPECT_EQ(call.name.value(), "calculate");
nlohmann::json params = nlohmann::json::parse(call.parameters);
std::string expected_expr =
std::to_string(i) + " + " + std::to_string(i + 1);
EXPECT_EQ(params["expression"], expected_expr);
}
}
// Test basic streaming functionality
TEST_F(Qwen25StreamingTest, BasicStreamingParsing) {
FunctionCallParser parser(tools_, "qwen25");
// Simulate streaming chunks
std::vector<std::string> chunks = {
"I need to check the weather ",
"<tool_call>\n",
"{\"name\": \"get_current_weather\", ",
"\"arguments\": {\"location\": \"Beijing\", ",
"\"unit\": \"celsius\"}}\n",
"</tool_call>"};
std::string accumulated_normal_text;
std::vector<ToolCallItem> accumulated_calls;
for (const auto& chunk : chunks) {
auto result = parser.parse_streaming_increment(chunk);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
for (const auto& call : result.calls) {
accumulated_calls.push_back(call);
}
}
// Verify results
EXPECT_EQ(accumulated_normal_text, "I need to check the weather ");
EXPECT_GT(accumulated_calls.size(), 0);
// Find the complete tool call
bool found_complete_call = false;
for (const auto& call : accumulated_calls) {
if (call.name.has_value() && call.name.value() == "get_current_weather") {
found_complete_call = true;
break;
}
}
EXPECT_TRUE(found_complete_call);
}
// Test multiple tool calls streaming
TEST_F(Qwen25StreamingTest, MultipleToolCallsStreaming) {
FunctionCallParser parser(tools_, "qwen25");
// Simulate realistic token-level streaming chunks with multiple tool calls
std::vector<std::string> chunks = {"Let",
" me",
" help",
" you",
" with",
" weather",
" and",
" calculation",
" ",
"<tool_call>",
"\n",
"{",
"\"name\"",
":",
" \"",
"get_current_weather",
"\",",
" ",
"\"arguments\"",
":",
" {",
"\"location\"",
":",
" \"",
"Shanghai",
"\"}}\n",
"</tool_call>",
"\n",
"<tool_call>",
"\n",
"{",
"\"name\"",
":",
" \"",
"calculate",
"\",",
" ",
"\"arguments\"",
":",
" {",
"\"expression\"",
":",
" \"",
"2",
" +",
" ",
"3",
"\"}}\n",
"</tool_call>"};
std::string accumulated_normal_text;
std::vector<ToolCallItem> accumulated_calls;
for (const auto& chunk : chunks) {
auto result = parser.parse_streaming_increment(chunk);
// std::cerr << "buffer_: " << (*parser.detector_).buffer_ << std::endl;
// std::cerr << " -> Normal text: " << result.normal_text << std::endl;
// std::cerr << " -> Calls count: " << result.calls.size() << std::endl;
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
for (const auto& call : result.calls) {
accumulated_calls.push_back(call);
}
}
// Verify results
EXPECT_EQ(accumulated_normal_text,
"Let me help you with weather and calculation ");
EXPECT_GT(accumulated_calls.size(), 0);
// Check for both tool calls
bool found_weather = false;
bool found_calculator = false;
for (const auto& call : accumulated_calls) {
if (call.name.has_value()) {
if (call.name.value() == "get_current_weather") {
found_weather = true;
} else if (call.name.value() == "calculate") {
found_calculator = true;
}
}
}
EXPECT_TRUE(found_weather);
EXPECT_TRUE(found_calculator);
}
// Test partial token handling
TEST_F(Qwen25StreamingTest, PartialTokenHandling) {
FunctionCallParser parser(tools_, "qwen25");
// Simulate realistic partial tokens being streamed - testing edge cases where
// tokens are split
std::vector<std::string> chunks = {"Testing",
" partial",
" tokens",
" ",
"<tool_call>",
"\n",
"{",
"\"name\"",
":",
" \"",
"get_current_weather",
"\",",
" ",
"\"arguments\"",
":",
" {",
"\"location\"",
":",
" \"",
"Tokyo",
"\"}}",
"\n",
"</tool_call>"};
std::string accumulated_normal_text;
std::vector<ToolCallItem> accumulated_calls;
for (const auto& chunk : chunks) {
auto result = parser.parse_streaming_increment(chunk);
if (!result.normal_text.empty()) {
accumulated_normal_text += result.normal_text;
}
for (const auto& call : result.calls) {
accumulated_calls.push_back(call);
}
}
// Verify results
EXPECT_EQ(accumulated_normal_text, "Testing partial tokens ");
EXPECT_GT(accumulated_calls.size(), 0);
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,718 @@
/* Copyright 2026 The xLLM Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://github.com/jd-opensource/xllm/blob/main/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include "qwen3_coder_detector.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include "core_types.h"
#include "function_call_parser.h"
namespace xllm {
namespace function_call {
namespace {
struct StreamCallAccumulator {
std::string name;
std::string parameters;
};
void merge_stream_result(const StreamingParseResult& result,
std::string* normal_text,
std::vector<StreamCallAccumulator>* calls) {
if (normal_text != nullptr && !result.normal_text.empty()) {
*normal_text += result.normal_text;
}
if (calls == nullptr) {
return;
}
for (const auto& call : result.calls) {
if (call.tool_index < 0) {
continue;
}
while (calls->size() <= static_cast<size_t>(call.tool_index)) {
calls->push_back(StreamCallAccumulator());
}
auto& acc = (*calls)[call.tool_index];
if (call.name.has_value()) {
acc.name = call.name.value();
}
if (!call.parameters.empty()) {
acc.parameters += call.parameters;
}
}
}
} // namespace
class Qwen3CoderDetectorTest : public ::testing::Test {
protected:
void SetUp() override {
detector_ = std::make_unique<Qwen3CoderDetector>();
nlohmann::json weather_params = {
{"type", "object"},
{"properties",
{{"location", {{"type", "string"}}},
{"unit", {{"type", "string"}, {"enum", {"celsius", "fahrenheit"}}}},
{"days", {{"type", "integer"}}},
{"temperature", {{"type", "number"}}},
{"metadata", {{"type", "object"}}}}}};
tools_.emplace_back(
"function",
JsonFunction(
"get_current_weather", "Get weather info", weather_params));
nlohmann::json sql_params = {{"type", "object"},
{"properties",
{{"query", {{"type", "string"}}},
{"dry_run", {{"type", "boolean"}}}}}};
tools_.emplace_back("function",
JsonFunction("sql_interpreter", "Run SQL", sql_params));
nlohmann::json todo_params = {
{"type", "object"}, {"properties", {{"todos", {{"type", "array"}}}}}};
tools_.emplace_back(
"function", JsonFunction("TodoWrite", "Write TODO items", todo_params));
}
std::unique_ptr<Qwen3CoderDetector> detector_;
std::vector<JsonTool> tools_;
};
// -----------------------------------------------------------------------------
// Basic behavior
// -----------------------------------------------------------------------------
TEST_F(Qwen3CoderDetectorTest, ConstructorInitializesCorrectly) {
ASSERT_NE(detector_, nullptr);
std::string text_with_tool_call =
"Some text <tool_call><function=test></function></tool_call>";
std::string text_without_tool_call =
"Just normal text without any tool calls";
EXPECT_TRUE(detector_->has_tool_call(text_with_tool_call));
EXPECT_FALSE(detector_->has_tool_call(text_without_tool_call));
}
TEST_F(Qwen3CoderDetectorTest, HasToolCallDetection) {
EXPECT_TRUE(detector_->has_tool_call("<tool_call>"));
EXPECT_TRUE(
detector_->has_tool_call("prefix <tool_call> middle </tool_call>"));
EXPECT_FALSE(detector_->has_tool_call(""));
EXPECT_FALSE(detector_->has_tool_call("regular text only"));
EXPECT_FALSE(detector_->has_tool_call("<function=get_current_weather>"));
}
TEST_F(Qwen3CoderDetectorTest, PlainTextOnly) {
std::string text = "This is plain text without any tool calls.";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, text);
EXPECT_TRUE(result.calls.empty());
}
TEST_F(Qwen3CoderDetectorTest, SingleToolCallParsing) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>Boston</parameter>\n"
"<parameter=unit>celsius</parameter>\n"
"<parameter=days>3</parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
const auto& call = result.calls[0];
EXPECT_EQ(call.tool_index, 0);
ASSERT_TRUE(call.name.has_value());
EXPECT_EQ(call.name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(call.parameters);
EXPECT_EQ(params["location"], "Boston");
EXPECT_EQ(params["unit"], "celsius");
EXPECT_EQ(params["days"], 3);
}
TEST_F(Qwen3CoderDetectorTest, SingleToolCallWithTextPrefix) {
std::string text =
"Let me check this for you.\n\n"
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>New York</parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Let me check this for you.\n\n");
ASSERT_EQ(result.calls.size(), 1);
ASSERT_TRUE(result.calls[0].name.has_value());
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
}
TEST_F(Qwen3CoderDetectorTest, MultipleToolCallsParsing) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>New York</parameter>\n"
"</function>\n"
"</tool_call>\n"
"<tool_call>\n"
"<function=sql_interpreter>\n"
"<parameter=query>SELECT * FROM users</parameter>\n"
"<parameter=dry_run>True</parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 2);
ASSERT_TRUE(result.calls[0].name.has_value());
ASSERT_TRUE(result.calls[1].name.has_value());
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
EXPECT_EQ(result.calls[1].name.value(), "sql_interpreter");
nlohmann::json params1 = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_EQ(params1["location"], "New York");
nlohmann::json params2 = nlohmann::json::parse(result.calls[1].parameters);
EXPECT_EQ(params2["query"], "SELECT * FROM users");
EXPECT_EQ(params2["dry_run"], true);
}
TEST_F(Qwen3CoderDetectorTest, MultipleFunctionsInOneToolCallBlock) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>Paris</parameter>\n"
"</function>\n"
"<function=sql_interpreter>\n"
"<parameter=query>SELECT 1</parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 2);
EXPECT_EQ(result.calls[0].name.value_or(""), "get_current_weather");
EXPECT_EQ(result.calls[1].name.value_or(""), "sql_interpreter");
}
TEST_F(Qwen3CoderDetectorTest, ParseWithoutToolCallWrapperFallback) {
std::string text =
"Prefix text\n"
"<function=get_current_weather>\n"
"<parameter=location>Tokyo</parameter>\n"
"</function>";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Prefix text\n");
ASSERT_EQ(result.calls.size(), 1);
ASSERT_TRUE(result.calls[0].name.has_value());
EXPECT_EQ(result.calls[0].name.value(), "get_current_weather");
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_EQ(params["location"], "Tokyo");
}
TEST_F(Qwen3CoderDetectorTest, UnknownToolIsStillParsedLikeSgLang) {
std::string text =
"<tool_call>\n"
"<function=unknown_tool>\n"
"<parameter=x>42</parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
EXPECT_EQ(result.calls[0].name.value_or(""), "unknown_tool");
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
// Unknown schema defaults to string type.
EXPECT_EQ(params["x"], "42");
}
TEST_F(Qwen3CoderDetectorTest, EmptyParameterValue) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location></parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_EQ(params["location"], "");
}
TEST_F(Qwen3CoderDetectorTest, SpecialCharactersInParameterValue) {
std::string text =
"<tool_call>\n"
"<function=sql_interpreter>\n"
"<parameter=query>SELECT * FROM users WHERE name = 'John \"Doe\"'</"
"parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_NE(params["query"].get<std::string>().find("John"), std::string::npos);
EXPECT_NE(params["query"].get<std::string>().find("Doe"), std::string::npos);
}
TEST_F(Qwen3CoderDetectorTest, IncompleteToolCallDoesNotCrash) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>London";
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_GE(result.calls.size(), 0U);
}
// -----------------------------------------------------------------------------
// Type conversion behavior
// -----------------------------------------------------------------------------
TEST_F(Qwen3CoderDetectorTest, IntegerParameterConversion) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Tokyo</parameter>"
"<parameter=days>5</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["days"].is_number_integer());
EXPECT_EQ(params["days"], 5);
}
TEST_F(Qwen3CoderDetectorTest, InvalidIntegerFallsBackToString) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Tokyo</parameter>"
"<parameter=days>five</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["days"].is_string());
EXPECT_EQ(params["days"], "five");
}
TEST_F(Qwen3CoderDetectorTest, NumberParameterConversion) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Tokyo</parameter>"
"<parameter=temperature>12.5</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["temperature"].is_number_float());
EXPECT_DOUBLE_EQ(params["temperature"], 12.5);
}
TEST_F(Qwen3CoderDetectorTest, NumberIntegerLikeStringConvertedToInt) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Tokyo</parameter>"
"<parameter=temperature>12</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["temperature"].is_number_integer());
EXPECT_EQ(params["temperature"], 12);
}
TEST_F(Qwen3CoderDetectorTest, BooleanParameterConversion) {
std::string text =
"<tool_call><function=sql_interpreter>"
"<parameter=query>SELECT 1</parameter>"
"<parameter=dry_run>True</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["dry_run"].is_boolean());
EXPECT_EQ(params["dry_run"], true);
}
TEST_F(Qwen3CoderDetectorTest, InvalidBooleanFallsBackToFalse) {
std::string text =
"<tool_call><function=sql_interpreter>"
"<parameter=query>SELECT 1</parameter>"
"<parameter=dry_run>not_bool</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["dry_run"].is_boolean());
EXPECT_EQ(params["dry_run"], false);
}
TEST_F(Qwen3CoderDetectorTest, NullValueConversion) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>null</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["location"].is_null());
}
TEST_F(Qwen3CoderDetectorTest, ObjectAndArrayConversion) {
std::string text =
"<tool_call>\n"
"<function=get_current_weather>\n"
"<parameter=location>Beijing</parameter>\n"
"<parameter=metadata>{\"source\":\"api\",\"retry\":1}</parameter>\n"
"</function>\n"
"</tool_call>\n"
"<tool_call>\n"
"<function=TodoWrite>\n"
"<parameter=todos>[{\"content\":\"A\",\"status\":\"pending\"}]</"
"parameter>\n"
"</function>\n"
"</tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 2);
nlohmann::json params1 = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params1["metadata"].is_object());
EXPECT_EQ(params1["metadata"]["source"], "api");
nlohmann::json params2 = nlohmann::json::parse(result.calls[1].parameters);
EXPECT_TRUE(params2["todos"].is_array());
EXPECT_EQ(params2["todos"][0]["content"], "A");
}
TEST_F(Qwen3CoderDetectorTest, InvalidObjectFallsBackToString) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Beijing</parameter>"
"<parameter=metadata>{invalid_json}</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["metadata"].is_string());
EXPECT_EQ(params["metadata"], "{invalid_json}");
}
TEST_F(Qwen3CoderDetectorTest, UnknownParameterFallsBackToRawString) {
std::string text =
"<tool_call><function=get_current_weather>"
"<parameter=location>Berlin</parameter>"
"<parameter=unexpected_field>123</parameter>"
"</function></tool_call>";
auto result = detector_->detect_and_parse(text, tools_);
ASSERT_EQ(result.calls.size(), 1);
nlohmann::json params = nlohmann::json::parse(result.calls[0].parameters);
EXPECT_TRUE(params["unexpected_field"].is_string());
EXPECT_EQ(params["unexpected_field"], "123");
}
// -----------------------------------------------------------------------------
// Streaming behavior
// -----------------------------------------------------------------------------
TEST_F(Qwen3CoderDetectorTest, StreamingSingleToolCall) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {"<tool_call>",
"<function=get_current_weather>",
"<parameter=location>",
"Boston",
"</parameter>",
"<parameter=unit>celsius</parameter>",
"<parameter=days>3</parameter>",
"</function>",
"</tool_call>"};
std::string normal_text;
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(
parser.parse_streaming_increment(chunk), &normal_text, &acc);
}
EXPECT_TRUE(normal_text.empty());
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "get_current_weather");
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Boston");
EXPECT_EQ(params["unit"], "celsius");
EXPECT_EQ(params["days"], 3);
}
TEST_F(Qwen3CoderDetectorTest, StreamingMultipleToolCalls) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {
"<tool_call><function=get_current_weather><parameter=location>Paris</"
"parameter></function></tool_call>",
"<tool_call><function=sql_interpreter><parameter=query>SELECT 1</"
"parameter><parameter=dry_run>false</parameter></function></tool_call>"};
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
}
ASSERT_EQ(acc.size(), 2);
EXPECT_EQ(acc[0].name, "get_current_weather");
EXPECT_EQ(acc[1].name, "sql_interpreter");
nlohmann::json params0 = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params0["location"], "Paris");
nlohmann::json params1 = nlohmann::json::parse(acc[1].parameters);
EXPECT_EQ(params1["query"], "SELECT 1");
EXPECT_EQ(params1["dry_run"], false);
}
TEST_F(Qwen3CoderDetectorTest, StreamingTextAndToolCall) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {"Let me ",
"help you.\n\n",
"<tool_call>",
"<function=get_current_weather>",
"<parameter=location>Paris</parameter>",
"</function>",
"</tool_call>"};
std::string normal_text;
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(
parser.parse_streaming_increment(chunk), &normal_text, &acc);
}
EXPECT_EQ(normal_text, "Let me help you.\n\n");
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "get_current_weather");
}
TEST_F(Qwen3CoderDetectorTest, StreamingParameterEndedByNextParameter) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::string chunk =
"<tool_call><function=get_current_weather>"
"<parameter=location>Boston"
"<parameter=unit>celsius</parameter>"
"</function></tool_call>";
std::vector<StreamCallAccumulator> acc;
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
ASSERT_EQ(acc.size(), 1);
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Boston");
EXPECT_EQ(params["unit"], "celsius");
}
TEST_F(Qwen3CoderDetectorTest, StreamingParameterEndedByFunctionEnd) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::string chunk =
"<tool_call><function=get_current_weather>"
"<parameter=location>Boston"
"</function></tool_call>";
std::vector<StreamCallAccumulator> acc;
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
ASSERT_EQ(acc.size(), 1);
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Boston");
}
TEST_F(Qwen3CoderDetectorTest,
StreamingFunctionWithoutParametersEmitsEmptyJson) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::string chunk =
"<tool_call><function=sql_interpreter></function></tool_call>";
std::vector<StreamCallAccumulator> acc;
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "sql_interpreter");
EXPECT_EQ(acc[0].parameters, "{}");
}
TEST_F(Qwen3CoderDetectorTest, StreamingIgnoresTextInsideToolCallRegion) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {"before ",
"<tool_call>\n",
"THIS_SHOULD_BE_IGNORED",
"<function=get_current_weather>",
"<parameter=location>Rome</parameter>",
"</function>",
"</tool_call>",
" after"};
std::string normal_text;
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(
parser.parse_streaming_increment(chunk), &normal_text, &acc);
}
EXPECT_EQ(normal_text, "before after");
ASSERT_EQ(acc.size(), 1);
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Rome");
}
TEST_F(Qwen3CoderDetectorTest,
StreamingKeepsLiteralAngleBracketOutsideToolCall) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {"2 < 3 and ", "5 > 4"};
std::string normal_text;
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(
parser.parse_streaming_increment(chunk), &normal_text, &acc);
}
EXPECT_EQ(normal_text, "2 < 3 and 5 > 4");
EXPECT_TRUE(acc.empty());
}
TEST_F(Qwen3CoderDetectorTest, StreamingPartialTagWaitsForMoreData) {
FunctionCallParser parser(tools_, "qwen3_coder");
auto result1 = parser.parse_streaming_increment("prefix <tool_ca");
EXPECT_EQ(result1.normal_text, "prefix ");
EXPECT_TRUE(result1.calls.empty());
auto result2 = parser.parse_streaming_increment(
"ll><function=get_current_weather><parameter=location>Paris</parameter>"
"</function></tool_call>");
std::vector<StreamCallAccumulator> acc;
merge_stream_result(result2, nullptr, &acc);
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "get_current_weather");
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Paris");
}
TEST_F(Qwen3CoderDetectorTest,
StreamingUnknownTagInsideToolCallDoesNotBreakParsing) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {
"<tool_call><unknown>abc</unknown><function=get_current_weather>",
"<parameter=location>Madrid</parameter></function></tool_call>"};
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
}
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "get_current_weather");
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Madrid");
}
TEST_F(Qwen3CoderDetectorTest,
StreamingTextAfterToolCallIsReturnedAsNormalText) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::vector<std::string> chunks = {
"<tool_call><function=get_current_weather><parameter=location>Seoul</"
"parameter></function></tool_call>",
" done"};
std::string normal_text;
std::vector<StreamCallAccumulator> acc;
for (const auto& chunk : chunks) {
merge_stream_result(
parser.parse_streaming_increment(chunk), &normal_text, &acc);
}
EXPECT_EQ(normal_text, " done");
ASSERT_EQ(acc.size(), 1);
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Seoul");
}
TEST_F(Qwen3CoderDetectorTest, StreamingCharacterByCharacter) {
FunctionCallParser parser(tools_, "qwen3_coder");
std::string text =
"<tool_call><function=get_current_weather><parameter=location>Tokyo</"
"parameter><parameter=days>2</parameter></function></tool_call>";
std::vector<StreamCallAccumulator> acc;
for (char ch : text) {
std::string chunk(1, ch);
merge_stream_result(parser.parse_streaming_increment(chunk), nullptr, &acc);
}
ASSERT_EQ(acc.size(), 1);
EXPECT_EQ(acc[0].name, "get_current_weather");
nlohmann::json params = nlohmann::json::parse(acc[0].parameters);
EXPECT_EQ(params["location"], "Tokyo");
EXPECT_EQ(params["days"], 2);
}
// -----------------------------------------------------------------------------
// Robustness / scale
// -----------------------------------------------------------------------------
TEST_F(Qwen3CoderDetectorTest, PerformanceWithManyToolCalls) {
std::string text = "Performance test ";
for (int i = 0; i < 300; ++i) {
text += "<tool_call><function=sql_interpreter><parameter=query>SELECT " +
std::to_string(i) + "</parameter></function></tool_call>";
}
auto result = detector_->detect_and_parse(text, tools_);
EXPECT_EQ(result.normal_text, "Performance test ");
ASSERT_EQ(result.calls.size(), 300);
for (int i = 0; i < 300; ++i) {
EXPECT_EQ(result.calls[i].name.value_or(""), "sql_interpreter");
}
}
} // namespace function_call
} // namespace xllm