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,38 @@
include(cc_library)
add_subdirectory(partial_json_parser)
cc_library (
NAME
function_call
HDRS
core_types.h
base_format_detector.h
qwen25_detector.h
qwen3_coder_detector.h
kimik2_detector.h
deepseekv3_detector.h
deepseekv32_detector.h
glm45_detector.h
glm47_detector.h
function_call_parser.h
function_call.h
utils.h
SRCS
base_format_detector.cpp
qwen25_detector.cpp
qwen3_coder_detector.cpp
kimik2_detector.cpp
deepseekv3_detector.cpp
deepseekv32_detector.cpp
glm45_detector.cpp
glm47_detector.cpp
function_call_parser.cpp
utils.cpp
DEPS
nlohmann_json::nlohmann_json
glog::glog
proto::xllm_proto
partial_json_parser
common
)

View File

@@ -0,0 +1,351 @@
/* 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 "base_format_detector.h"
#include <algorithm>
#include <iostream>
#include <regex>
#include <sstream>
namespace xllm {
namespace function_call {
BaseFormatDetector::BaseFormatDetector()
: current_tool_id_(-1),
current_tool_name_sent_(false),
bot_token_(""),
eot_token_(""),
tool_call_separator_(", ") {}
std::unordered_map<std::string, int32_t> BaseFormatDetector::get_tool_indices(
const std::vector<JsonTool>& tools) const {
std::unordered_map<std::string, int32_t> indices;
for (size_t i = 0; i < tools.size(); ++i) {
if (!tools[i].function.name.empty()) {
indices[tools[i].function.name] = static_cast<int32_t>(i);
} else {
LOG(ERROR) << "Tool at index " << i
<< " has empty function name, skipping";
}
}
return indices;
}
std::vector<ToolCallItem> BaseFormatDetector::parse_base_json(
const nlohmann::json& json_obj,
const std::vector<JsonTool>& tools) {
auto tool_indices = get_tool_indices(tools);
std::vector<ToolCallItem> results;
std::vector<nlohmann::json> actions;
if (json_obj.is_array()) {
for (const auto& item : json_obj) {
actions.emplace_back(item);
}
} else {
actions.emplace_back(json_obj);
}
for (const auto& act : actions) {
if (!act.is_object()) {
LOG(ERROR) << "Invalid tool call item, expected object, got: "
<< act.type_name();
continue;
}
std::string name;
if (act.contains("name") && act["name"].is_string()) {
name = act["name"].get<std::string>();
} else {
LOG(ERROR) << "Invalid tool call: missing 'name' field or invalid type";
continue;
}
if (tool_indices.find(name) == tool_indices.end()) {
LOG(ERROR) << "Model attempted to call undefined function: " << name;
continue;
}
nlohmann::json parameters = nlohmann::json::object();
if (act.contains("parameters")) {
parameters = act["parameters"];
} else if (act.contains("arguments")) {
parameters = act["arguments"];
} else {
LOG(ERROR) << "No parameters or arguments field found for tool: " << name;
}
if (!parameters.is_object()) {
LOG(ERROR) << "Invalid arguments type for tool: " << name
<< ", expected object, got: " << parameters.type_name();
parameters = nlohmann::json::object();
}
std::string parameters_str;
try {
parameters_str = parameters.dump(
-1, ' ', false, nlohmann::json::error_handler_t::ignore);
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to serialize arguments for tool: " << name
<< ", error: " << e.what();
parameters_str = "{}";
}
results.emplace_back(-1, name, parameters_str);
}
return results;
}
int32_t BaseFormatDetector::ends_with_partial_token(
const std::string& buffer,
const std::string& bot_token) const {
// Check if buffer ends with a partial bot_token.
// Return the length of the partial bot_token.
// For some format, the bot_token is not a token in model's vocabulary, such
// as
// `[TOOL_CALLS] [` in Mistral.
for (int32_t i = 1; i <= std::min(static_cast<int32_t>(buffer.length()),
static_cast<int32_t>(bot_token.length()));
++i) {
if (bot_token.substr(0, i) == buffer.substr(buffer.length() - i)) {
return i;
}
}
return 0;
}
StreamingParseResult BaseFormatDetector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
// Streaming incremental parsing with tool validation.
// This base implementation works best with formats where:
// 1. bot_token is followed immediately by JSON (e.g., bot_token + JSON_array)
// 2. JSON can be parsed incrementally using partial_json_loads
// 3. Multiple tool calls are separated by "; " or ", "
//
// Examples of incompatible formats (need custom implementation, may reuse
// some logic from this class):
// - Each tool call is wrapped in a separate block: See Qwen25Detector
// - Multiple separate blocks: [TOOL_CALLS] [...] \n [TOOL_CALLS] [...]
// - Tool call is Pythonic style
//
// For incompatible formats, detectors should override this method with custom
// logic.
// Append new text to buffer
buffer_ += new_text;
std::string current_text = buffer_;
// The current_text has tool_call if it is the start of a new tool call
// sequence or it is the start of a new tool call after a tool call separator,
// when there is a previous tool call
if (!(has_tool_call(current_text) ||
(current_tool_id_ > 0 &&
current_text.find(tool_call_separator_) == 0))) {
if (ends_with_partial_token(buffer_, bot_token_) == 0) {
std::string normal_text = buffer_;
buffer_.clear();
size_t eot_pos = normal_text.find(eot_token_);
if (eot_pos != std::string::npos) {
normal_text = normal_text.substr(0, eot_pos) +
normal_text.substr(eot_pos + eot_token_.length());
}
return StreamingParseResult(normal_text, {});
} else {
return StreamingParseResult();
}
}
if (tool_indices_.empty()) {
tool_indices_ = get_tool_indices(tools);
}
Allow flags =
current_tool_name_sent_ ? Allow::ALL : (Allow::ALL & ~Allow::STR);
try {
int32_t start_idx = 0;
if (current_text.find(bot_token_) == 0) {
start_idx = bot_token_.length();
} else if (current_tool_id_ > 0 &&
current_text.find(tool_call_separator_ + bot_token_) == 0) {
start_idx = tool_call_separator_.length() + bot_token_.length();
} else if (current_tool_id_ > 0 &&
current_text.find(tool_call_separator_) == 0) {
start_idx = tool_call_separator_.length();
}
if (start_idx >= static_cast<int32_t>(current_text.length())) {
return StreamingParseResult();
}
std::string json_part = current_text.substr(start_idx);
auto [obj, end_idx] = partial_json_loads(json_part, flags);
bool is_current_complete = is_complete_json(json_part.substr(0, end_idx));
if (obj.contains("name") && obj["name"].is_string()) {
std::string tool_name = obj["name"].get<std::string>();
if (tool_indices_.find(tool_name) == tool_indices_.end()) {
buffer_.clear();
current_tool_id_ = -1;
current_tool_name_sent_ = false;
if (!streamed_args_for_tool_.empty()) {
streamed_args_for_tool_.pop_back();
}
return StreamingParseResult();
}
}
nlohmann::json current_tool_call = obj;
if (current_tool_call.contains("parameters")) {
if (current_tool_call.contains("arguments")) {
LOG(ERROR) << "Model generated both parameters and arguments";
return StreamingParseResult();
}
current_tool_call["arguments"] = current_tool_call["parameters"];
}
if (current_tool_call.empty()) {
return StreamingParseResult();
}
StreamingParseResult res;
// Case 1: Handle tool name streaming
if (!current_tool_name_sent_) {
if (current_tool_call.contains("name") &&
current_tool_call["name"].is_string()) {
std::string function_name =
current_tool_call["name"].get<std::string>();
if (tool_indices_.find(function_name) != tool_indices_.end()) {
// If this is a new tool (current_tool_id was -1), initialize it
if (current_tool_id_ == -1) {
current_tool_id_ = 0;
streamed_args_for_tool_.push_back("");
}
// If this is a subsequent tool, ensure streamed_args_for_tool is
// large enough
else if (current_tool_id_ >=
static_cast<int32_t>(streamed_args_for_tool_.size())) {
while (static_cast<int32_t>(streamed_args_for_tool_.size()) <=
current_tool_id_) {
streamed_args_for_tool_.push_back("");
}
}
// Send the tool name with empty parameters
res = StreamingParseResult(
"", {ToolCallItem(current_tool_id_, function_name, "")});
current_tool_name_sent_ = true;
} else {
res = StreamingParseResult();
}
} else {
res = StreamingParseResult();
}
}
// Case 2: Handle streaming arguments
else {
if (current_tool_call.contains("arguments")) {
nlohmann::json cur_arguments = current_tool_call["arguments"];
// Calculate how much of the arguments we've already streamed
int sent = streamed_args_for_tool_[current_tool_id_].length();
std::string cur_args_json = cur_arguments.dump();
std::string argument_diff;
int completing_tool_id = current_tool_id_;
// If the current tool's JSON is complete, send all remaining arguments
if (is_current_complete) {
argument_diff = cur_args_json.substr(sent);
// Only remove the processed portion, keep unprocessed content
buffer_ = current_text.substr(start_idx + end_idx);
if (current_tool_id_ < static_cast<int>(prev_tool_call_arr_.size())) {
prev_tool_call_arr_[current_tool_id_].clear();
}
current_tool_name_sent_ = false;
streamed_args_for_tool_[current_tool_id_] = "";
current_tool_id_++;
}
// If the tool is still being parsed, send incremental changes
else if (current_tool_id_ <
static_cast<int>(prev_tool_call_arr_.size())) {
auto prev_args_it =
prev_tool_call_arr_[current_tool_id_].find("arguments");
if (prev_args_it != prev_tool_call_arr_[current_tool_id_].end()) {
std::string prev_args_json = prev_args_it->second;
if (cur_args_json != prev_args_json) {
std::string prefix =
find_common_prefix(prev_args_json, cur_args_json);
argument_diff = prefix.substr(sent);
}
}
}
if (!argument_diff.empty()) {
int tool_index_to_use =
is_current_complete ? completing_tool_id : current_tool_id_;
res = StreamingParseResult(
"",
{ToolCallItem(tool_index_to_use, std::nullopt, argument_diff)});
if (!is_current_complete) {
streamed_args_for_tool_[current_tool_id_] += argument_diff;
}
} else {
res = StreamingParseResult();
}
} else {
res = StreamingParseResult();
}
}
if (current_tool_id_ >= 0) {
while (static_cast<int>(prev_tool_call_arr_.size()) <= current_tool_id_) {
prev_tool_call_arr_.push_back({});
}
std::unordered_map<std::string, std::string> tool_call_map;
if (current_tool_call.contains("name") &&
current_tool_call["name"].is_string()) {
tool_call_map["name"] = current_tool_call["name"].get<std::string>();
}
if (current_tool_call.contains("arguments")) {
tool_call_map["arguments"] = current_tool_call["arguments"].dump();
}
prev_tool_call_arr_[current_tool_id_] = tool_call_map;
}
return res;
} catch (const std::exception& e) {
return StreamingParseResult();
}
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,79 @@
/* 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.
==============================================================================*/
#pragma once
#include <glog/logging.h>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include "chat.pb.h"
#include "core_types.h"
#include "utils.h"
namespace xllm {
namespace function_call {
class BaseFormatDetector {
public:
BaseFormatDetector();
virtual ~BaseFormatDetector() = default;
BaseFormatDetector(const BaseFormatDetector&) = delete;
BaseFormatDetector& operator=(const BaseFormatDetector&) = delete;
std::unordered_map<std::string, int32_t> get_tool_indices(
const std::vector<JsonTool>& tools) const;
std::vector<ToolCallItem> parse_base_json(const nlohmann::json& json_obj,
const std::vector<JsonTool>& tools);
virtual StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) = 0;
virtual bool has_tool_call(const std::string& text) = 0;
virtual StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools);
std::vector<std::unordered_map<std::string, std::string>> prev_tool_call_arr_;
std::vector<std::string> streamed_args_for_tool_;
protected:
std::string buffer_;
int32_t current_tool_id_;
bool current_tool_name_sent_;
std::string bot_token_;
std::string eot_token_;
std::string tool_call_separator_;
int32_t ends_with_partial_token(const std::string& buffer,
const std::string& bot_token) const;
std::unordered_map<std::string, int32_t> tool_indices_;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,84 @@
/* 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.
==============================================================================*/
#pragma once
#include <functional>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include <vector>
#include "core/common/types.h"
namespace xllm {
namespace function_call {
using JsonFunction = xllm::JsonFunction;
using JsonTool = xllm::JsonTool;
struct ToolCallItem {
int32_t tool_index;
std::optional<std::string> name;
std::string parameters; // JSON string
ToolCallItem() : tool_index(-1), parameters("") {}
ToolCallItem(int32_t index,
const std::optional<std::string>& func_name,
const std::string& params)
: tool_index(index), name(func_name), parameters(params) {}
};
struct StreamingParseResult {
std::string normal_text;
std::vector<ToolCallItem> calls;
StreamingParseResult() = default;
explicit StreamingParseResult(std::string text)
: normal_text(std::move(text)) {}
explicit StreamingParseResult(std::vector<ToolCallItem> tool_calls)
: calls(std::move(tool_calls)) {}
StreamingParseResult(std::string text, std::vector<ToolCallItem> tool_calls)
: normal_text(std::move(text)), calls(std::move(tool_calls)) {}
bool has_calls() const { return !calls.empty(); }
void clear() {
normal_text.clear();
calls.clear();
}
};
struct StructureInfo {
std::string begin;
std::string end;
std::string trigger;
StructureInfo() = default;
StructureInfo(const std::string& begin_str,
const std::string& end_str,
const std::string& trigger_str)
: begin(begin_str), end(end_str), trigger(trigger_str) {}
};
using GetInfoFunc = std::function<StructureInfo(const std::string&)>;
} // namespace function_call
} // namespace xllm

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
/* 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.
==============================================================================*/
#pragma once
#include <regex>
#include <string>
#include <string_view>
#include <utility>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
class DeepSeekV32Detector : public BaseFormatDetector {
public:
DeepSeekV32Detector();
virtual ~DeepSeekV32Detector() = default;
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
std::regex function_calls_regex_;
std::regex invoke_regex_;
// For streaming: matches invoke with optional closing tag (group 3 =
// "</DSMLinvoke>" or "")
std::regex streaming_invoke_regex_;
std::regex parameter_regex_;
std::regex partial_parameter_regex_;
std::string invoke_end_token_;
std::vector<std::string> prefix_parameter_end_call_;
std::string utf8_buffer_;
std::string trim_whitespace(std::string_view str) const;
std::pair<std::string, std::string> split_incomplete_utf8(
const std::string& str) const;
std::unordered_map<std::string, nlohmann::json> parse_parameters_from_xml(
const std::string& invoke_content,
bool allow_partial = false) const;
std::vector<ToolCallItem> parse_json_tool_calls(
const std::string& text,
const std::vector<JsonTool>& tools);
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,317 @@
/* 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 <algorithm>
#include <iostream>
#include <nlohmann/json.hpp>
#include <regex>
#include <string_view>
namespace xllm {
namespace function_call {
DeepSeekV3Detector::DeepSeekV3Detector() : BaseFormatDetector() {
bot_token_ = "<tool▁calls▁begin>";
eot_token_ = "<tool▁calls▁end>";
func_call_regex_ = "<tool▁call▁begin>.*?<tool▁call▁end>";
func_detail_regex_ =
"<tool▁call▁begin>(.*)<tool▁sep>(.*)\n```json\n(.*)\n```<"
"tool▁call▁end>";
last_arguments_ = "";
current_tool_id_ = -1;
}
bool DeepSeekV3Detector::has_tool_call(const std::string& text) {
return text.find(bot_token_) != std::string::npos;
}
std::string_view DeepSeekV3Detector::trim_whitespace(
std::string_view str) const {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string_view::npos) {
return std::string_view{};
}
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
std::vector<std::pair<size_t, size_t>>
DeepSeekV3Detector::find_tool_call_ranges(const std::string& text) const {
std::vector<std::pair<size_t, size_t>> ranges;
ranges.reserve(4);
const std::string call_begin = "<tool▁call▁begin>";
const std::string call_end = "<tool▁call▁end>";
size_t search_pos = 0;
const size_t call_begin_len = call_begin.length();
const size_t call_end_len = call_end.length();
while (search_pos < text.length()) {
size_t start_pos = text.find(call_begin, search_pos);
if (start_pos == std::string::npos) {
break;
}
size_t content_start = start_pos + call_begin_len;
size_t end_pos = text.find(call_end, content_start);
if (end_pos == std::string::npos) {
break;
}
ranges.emplace_back(content_start, end_pos);
search_pos = end_pos + call_end_len;
}
return ranges;
}
StreamingParseResult DeepSeekV3Detector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
size_t bot_token_pos = text.find(bot_token_);
std::string normal_text;
if (bot_token_pos != std::string::npos) {
std::string_view normal_text_view(text.data(), bot_token_pos);
std::string_view trimmed = trim_whitespace(normal_text_view);
normal_text = std::string(trimmed);
} else {
std::string_view trimmed = trim_whitespace(text);
normal_text = std::string(trimmed);
return StreamingParseResult(normal_text);
}
auto tool_call_ranges = find_tool_call_ranges(text);
std::vector<ToolCallItem> calls;
calls.reserve(tool_call_ranges.size());
for (const auto& range : tool_call_ranges) {
std::string_view content_view(text.data() + range.first,
range.second - range.first);
std::string_view trimmed_content = trim_whitespace(content_view);
if (trimmed_content.empty()) {
continue;
}
try {
// Parse DeepSeek V3 format: <tool_sep>function_name\n```json\n{args}\n```
const std::string tool_sep = "<tool▁sep>";
size_t sep_pos = trimmed_content.find(tool_sep);
if (sep_pos == std::string_view::npos) {
LOG(ERROR) << "Failed to find tool separator in: "
<< std::string(trimmed_content);
continue;
}
// Extract function name (between tool_sep and first newline)
size_t name_start = sep_pos + tool_sep.length();
size_t name_end = trimmed_content.find('\n', name_start);
if (name_end == std::string_view::npos) {
LOG(ERROR) << "Failed to find function name end in: "
<< std::string(trimmed_content);
continue;
}
std::string_view func_name_view =
trimmed_content.substr(name_start, name_end - name_start);
std::string_view func_name_trimmed = trim_whitespace(func_name_view);
std::string func_name(func_name_trimmed);
// Find JSON block (between ```json\n and \n```)
const std::string json_start = "```json\n";
const std::string json_end = "\n```";
size_t json_start_pos = trimmed_content.find(json_start, name_end);
if (json_start_pos == std::string_view::npos) {
LOG(ERROR) << "Failed to find JSON start in: "
<< std::string(trimmed_content);
continue;
}
size_t json_content_start = json_start_pos + json_start.length();
size_t json_end_pos = trimmed_content.find(json_end, json_content_start);
if (json_end_pos == std::string_view::npos) {
LOG(ERROR) << "Failed to find JSON end in: "
<< std::string(trimmed_content);
continue;
}
std::string_view json_view = trimmed_content.substr(
json_content_start, json_end_pos - json_content_start);
std::string_view json_trimmed = trim_whitespace(json_view);
// Parse JSON arguments
nlohmann::json func_args;
try {
std::string json_content(json_trimmed);
func_args = nlohmann::json::parse(json_content);
} catch (const nlohmann::json::parse_error& e) {
LOG(ERROR) << "Failed to parse JSON arguments: "
<< std::string(json_trimmed)
<< ", JSON parse error: " << e.what();
continue;
}
// Create JSON object for parse_base_json
nlohmann::json match_json;
match_json["name"] = func_name;
match_json["parameters"] = func_args;
auto parsed_calls = parse_base_json(match_json, tools);
calls.insert(calls.end(),
std::make_move_iterator(parsed_calls.begin()),
std::make_move_iterator(parsed_calls.end()));
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to parse tool call: "
<< std::string(trimmed_content) << ", error: " << e.what();
continue;
}
}
return StreamingParseResult(std::move(normal_text), std::move(calls));
}
StreamingParseResult DeepSeekV3Detector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
buffer_ += new_text;
std::string current_text = buffer_;
bool has_tool_call =
(current_text.find(bot_token_) != std::string::npos ||
current_text.find("<tool▁call▁begin>") != std::string::npos);
if (!has_tool_call) {
buffer_.clear();
std::string result_text = new_text;
std::vector<std::string> end_tokens = {
eot_token_, "```", "<tool▁call▁end>"};
for (const auto& e_token : end_tokens) {
size_t pos = result_text.find(e_token);
if (pos != std::string::npos) {
result_text = result_text.substr(0, pos) +
result_text.substr(pos + e_token.length());
}
}
return StreamingParseResult(result_text);
}
if (tool_indices_.empty()) {
tool_indices_ = get_tool_indices(tools);
}
std::vector<ToolCallItem> calls;
try {
std::regex partial_match_regex(
R"(<tool▁call▁begin>(.*)<tool▁sep>(.*)\n```json\n(.*)\n```.*)");
std::smatch match;
if (std::regex_search(current_text, match, partial_match_regex)) {
std::string func_name = match[2].str();
// Trim whitespace
func_name.erase(0, func_name.find_first_not_of(" \t\n\r"));
func_name.erase(func_name.find_last_not_of(" \t\n\r") + 1);
std::string func_args_raw = match[3].str();
// Trim whitespace
func_args_raw.erase(0, func_args_raw.find_first_not_of(" \t\n\r"));
func_args_raw.erase(func_args_raw.find_last_not_of(" \t\n\r") + 1);
if (current_tool_id_ == -1) {
current_tool_id_ = 0;
prev_tool_call_arr_.clear();
streamed_args_for_tool_ = {""};
}
while (static_cast<int>(prev_tool_call_arr_.size()) <= current_tool_id_) {
prev_tool_call_arr_.push_back({});
}
while (static_cast<int>(streamed_args_for_tool_.size()) <=
current_tool_id_) {
streamed_args_for_tool_.push_back("");
}
if (!current_tool_name_sent_) {
calls.push_back(ToolCallItem(current_tool_id_, func_name, ""));
current_tool_name_sent_ = true;
prev_tool_call_arr_[current_tool_id_]["name"] = func_name;
prev_tool_call_arr_[current_tool_id_]["arguments"] = "{}";
} else {
std::string argument_diff;
if (func_args_raw.length() > last_arguments_.length() &&
func_args_raw.substr(0, last_arguments_.length()) ==
last_arguments_) {
argument_diff = func_args_raw.substr(last_arguments_.length());
} else {
argument_diff = func_args_raw;
}
if (!argument_diff.empty()) {
calls.push_back(
ToolCallItem(current_tool_id_, std::nullopt, argument_diff));
last_arguments_ += argument_diff;
streamed_args_for_tool_[current_tool_id_] += argument_diff;
}
if (is_complete_json(func_args_raw)) {
try {
nlohmann::json parsed_args = nlohmann::json::parse(func_args_raw);
prev_tool_call_arr_[current_tool_id_]["arguments"] =
parsed_args.dump();
} catch (const nlohmann::json::parse_error&) {
// Ignore parse errors for partial JSON
}
std::regex tool_call_end_pattern(
R"(<tool▁call▁begin>.*?<tool▁call▁end>)");
std::smatch end_match;
if (std::regex_search(
current_text, end_match, tool_call_end_pattern)) {
buffer_ =
current_text.substr(end_match.position() + end_match.length());
} else {
buffer_.clear();
}
StreamingParseResult result("", calls);
current_tool_id_++;
last_arguments_.clear();
current_tool_name_sent_ = false;
return result;
}
}
}
return StreamingParseResult("", calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in parse_streaming_increment: " << e.what();
return StreamingParseResult(current_text);
}
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,53 @@
/* 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.
==============================================================================*/
#pragma once
#include <regex>
#include <string>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
class DeepSeekV3Detector : public BaseFormatDetector {
public:
DeepSeekV3Detector();
virtual ~DeepSeekV3Detector() = default;
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
std::string func_call_regex_;
std::string func_detail_regex_;
std::string last_arguments_;
std::string_view trim_whitespace(std::string_view str) const;
std::vector<std::pair<size_t, size_t>> find_tool_call_ranges(
const std::string& text) const;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,42 @@
/* 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.
==============================================================================*/
#pragma once
#include "base_format_detector.h"
#include "core_types.h"
#include "deepseekv3_detector.h"
#include "function_call_parser.h"
#include "glm45_detector.h"
#include "kimik2_detector.h"
#include "qwen25_detector.h"
#include "qwen3_coder_detector.h"
namespace xllm {
namespace function_call {
inline std::vector<ToolCallItem> parse(const std::string& text,
const std::vector<JsonTool>& tools,
const std::string& format = "qwen25") {
return utils::parse_function_calls(text, tools, format);
}
inline bool has_calls(const std::string& text,
const std::string& format = "qwen25") {
return utils::has_function_calls(text, format);
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,212 @@
/* 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 "function_call_parser.h"
#include <iostream>
#include <stdexcept>
#include <unordered_map>
#include "absl/strings/str_join.h"
#include "core/util/uuid.h"
#include "deepseekv32_detector.h"
#include "deepseekv3_detector.h"
#include "glm45_detector.h"
#include "glm47_detector.h"
#include "kimik2_detector.h"
#include "qwen25_detector.h"
#include "qwen3_coder_detector.h"
namespace xllm {
namespace function_call {
namespace {
const std::unordered_map<std::string, std::vector<std::string>> auto_paser_map =
{
{"qwen25", {"qwen2", "qwen3"}},
{"qwen3_coder", {"qwen3_coder", "qwen35"}},
{"kimi_k2", {"kimi_k2"}},
{"deepseekv3", {"deepseek_v3"}},
{"deepseekv32", {"deepseek_v32"}},
// GLM-4.5 and GLM-4.7 are not supported for tool call parser
// auto-selection
// {"glm45", {"glm4_moe"}},
// {"glm47", {"glm4_moe"}},
};
std::string get_auto_paser_map_supported() {
std::vector<std::string> keys;
for (const auto& [key, value] : auto_paser_map) {
for (const auto& v : value) {
keys.push_back(v);
}
}
return absl::StrJoin(keys, ", ");
}
const std::unordered_map<std::string,
std::function<std::unique_ptr<BaseFormatDetector>()>>
detector_factories = {
{"qwen25", [] { return std::make_unique<Qwen25Detector>(); }},
{"qwen3_coder", [] { return std::make_unique<Qwen3CoderDetector>(); }},
{"kimi_k2", [] { return std::make_unique<KimiK2Detector>(); }},
{"deepseekv3", [] { return std::make_unique<DeepSeekV3Detector>(); }},
{"deepseekv32", [] { return std::make_unique<DeepSeekV32Detector>(); }},
{"glm45", [] { return std::make_unique<Glm45Detector>(); }},
{"glm47", [] { return std::make_unique<Glm47Detector>(); }},
// glm5 use glm47 detector
{"glm5", [] { return std::make_unique<Glm47Detector>(); }},
};
std::string get_supported_detector_factories() {
std::vector<std::string> keys;
for (const auto& [key, value] : detector_factories) {
keys.push_back(key);
}
return absl::StrJoin(keys, ", ");
}
} // namespace
std::string FunctionCallParser::get_parser_auto(const std::string& parser,
const std::string& model_type) {
if (parser.empty()) {
return "";
}
if (parser == "auto") {
// find the tool call parser that supports the model type
for (const auto& [key, value] : auto_paser_map) {
if (std::find(value.begin(), value.end(), model_type) != value.end()) {
LOG(INFO) << "Using tool call parser: " << key
<< " for model type: " << model_type;
return key;
}
}
LOG(FATAL) << "Unsupported model type for auto tool call parser: "
<< model_type << ". Supported model types are: "
<< get_auto_paser_map_supported();
return "";
} else {
// check if the tool call parser is supported
if (parser == "qwen2" || parser == "qwen3") {
return "qwen25";
}
if (parser == "qwen35") {
return "qwen3_coder";
}
if (detector_factories.find(parser) != detector_factories.end()) {
return parser;
}
LOG(FATAL) << "Unsupported tool call parser: " << parser
<< ". Supported parsers are: "
<< get_supported_detector_factories();
return "";
}
}
FunctionCallParser::FunctionCallParser(const std::vector<JsonTool>& tools,
const std::string& tool_call_parser)
: tools_(tools) {
detector_ = create_detector(tool_call_parser);
CHECK(detector_ != nullptr)
<< "Unsupported tool_call_parser: " << tool_call_parser;
}
bool FunctionCallParser::has_tool_call(const std::string& text) const {
return detector_->has_tool_call(text);
}
std::tuple<std::string, std::vector<ToolCallItem>>
FunctionCallParser::parse_non_stream(const std::string& full_text) {
StreamingParseResult parsed_result =
detector_->detect_and_parse(full_text, tools_);
if (!parsed_result.calls.empty()) {
return std::make_tuple(parsed_result.normal_text, parsed_result.calls);
} else {
return std::make_tuple(full_text, std::vector<ToolCallItem>());
}
}
StreamingParseResult FunctionCallParser::parse_streaming_increment(
const std::string& new_text) {
return detector_->parse_streaming_increment(new_text, tools_);
}
std::unique_ptr<BaseFormatDetector> FunctionCallParser::create_detector(
const std::string& tool_call_parser) {
if (tool_call_parser.empty()) {
return nullptr;
}
auto it = detector_factories.find(tool_call_parser);
if (it != detector_factories.end()) {
return it->second();
}
LOG(ERROR) << "Unsupported tool call parser: " << tool_call_parser;
return nullptr;
}
namespace utils {
std::vector<ToolCallItem> parse_function_calls(
const std::string& text,
const std::vector<JsonTool>& tools,
const std::string& parser_type) {
try {
FunctionCallParser parser(tools, parser_type);
auto [normal_text, calls] = parser.parse_non_stream(text);
return calls;
} catch (const std::exception& e) {
LOG(ERROR) << "Error parsing function calls: " << e.what();
return {};
}
}
bool has_function_calls(const std::string& text,
const std::string& parser_type) {
try {
FunctionCallParser parser({}, parser_type);
return parser.has_tool_call(text);
} catch (const std::exception& e) {
LOG(ERROR) << "Error checking function calls: " << e.what();
return false;
}
}
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools,
const std::string& parser_type) {
try {
FunctionCallParser parser(tools, parser_type);
return parser.parse_streaming_increment(new_text);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in streaming parsing: " << e.what();
return StreamingParseResult();
}
}
thread_local ShortUUID short_uuid;
std::string generate_tool_call_id() { return "call_" + short_uuid.random(); }
} // namespace utils
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,84 @@
/* 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.
==============================================================================*/
#pragma once
#include <memory>
#include <string>
#include <tuple>
#include <vector>
#include "base_format_detector.h"
#include "core_types.h"
namespace xllm {
namespace function_call {
class FunctionCallParser {
public:
FunctionCallParser(const std::vector<JsonTool>& tools,
const std::string& tool_call_parser);
~FunctionCallParser() = default;
FunctionCallParser(const FunctionCallParser&) = delete;
FunctionCallParser& operator=(const FunctionCallParser&) = delete;
bool has_tool_call(const std::string& text) const;
std::tuple<std::string, std::vector<ToolCallItem>> parse_non_stream(
const std::string& full_text);
// Streaming incremental parsing method
StreamingParseResult parse_streaming_increment(const std::string& new_text);
// StructuralTagResponseFormat get_structure_tag();
// std::tuple<std::string, std::any> get_structure_constraint(const
// std::string& tool_choice);
BaseFormatDetector* get_detector() const { return detector_.get(); }
static std::string get_parser_auto(const std::string& parser,
const std::string& model_type);
private:
std::unique_ptr<BaseFormatDetector> create_detector(
const std::string& tool_call_parser);
std::unique_ptr<BaseFormatDetector> detector_;
std::vector<JsonTool> tools_;
};
namespace utils {
std::vector<ToolCallItem> parse_function_calls(
const std::string& text,
const std::vector<JsonTool>& tools,
const std::string& parser_type = "qwen25");
bool has_function_calls(const std::string& text,
const std::string& parser_type = "qwen25");
// Streaming parsing utility function
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools,
const std::string& parser_type = "qwen25");
std::string generate_tool_call_id();
} // namespace utils
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,198 @@
/* 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 <algorithm>
#include <iostream>
#include <sstream>
namespace xllm {
namespace function_call {
Glm45Detector::Glm45Detector() : BaseFormatDetector() {
bot_token_ = "<tool_call>";
eot_token_ = "</tool_call>";
// Regex patterns for GLM-4.5 format
func_call_regex_ = std::regex("<tool_call>[\\s\\S]*?</tool_call>",
std::regex_constants::ECMAScript);
func_detail_regex_ =
std::regex("<tool_call>([^\\n]*)\\n([\\s\\S]*?)</tool_call>",
std::regex_constants::ECMAScript);
func_arg_regex_ = std::regex(
"<arg_key>([\\s\\S]*?)</arg_key>\\s*<arg_value>([\\s\\S]*?)</arg_value>",
std::regex_constants::ECMAScript);
}
std::string Glm45Detector::trim_whitespace(std::string_view str) const {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string_view::npos) {
return std::string{};
}
size_t end = str.find_last_not_of(whitespace);
return std::string(str.substr(start, end - start + 1));
}
bool Glm45Detector::has_tool_call(const std::string& text) {
return text.find(bot_token_) != std::string::npos;
}
StreamingParseResult Glm45Detector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
size_t idx = text.find(bot_token_);
std::string normal_text =
(idx != std::string::npos) ? text.substr(0, idx) : text;
// Trim normal text
if (!normal_text.empty()) {
normal_text = trim_whitespace(normal_text);
}
if (idx == std::string::npos) {
return StreamingParseResult(normal_text, {});
}
std::vector<ToolCallItem> calls;
try {
std::sregex_iterator iter(text.begin(), text.end(), func_call_regex_);
std::sregex_iterator end;
for (; iter != end; ++iter) {
std::smatch match = *iter;
std::string match_result = match.str();
// Parse function name and arguments
std::smatch func_detail;
if (std::regex_search(match_result, func_detail, func_detail_regex_)) {
std::string func_name = func_detail[1].str();
std::string func_args = func_detail[2].str();
// Parse arguments using regex
std::unordered_map<std::string, nlohmann::json> arguments;
std::sregex_iterator arg_iter(
func_args.begin(), func_args.end(), func_arg_regex_);
std::sregex_iterator arg_end;
for (; arg_iter != arg_end; ++arg_iter) {
std::smatch arg_match = *arg_iter;
if (arg_match.size() >= 3) {
std::string arg_key = arg_match[1].str();
std::string arg_value = arg_match[2].str();
arg_key = trim_whitespace(arg_key);
arg_value = trim_whitespace(arg_value);
try {
nlohmann::json parsed_value = nlohmann::json::parse(arg_value);
arguments[arg_key] = parsed_value;
} catch (const nlohmann::json::parse_error&) {
arguments[arg_key] = nlohmann::json(arg_value);
}
}
}
// Create JSON object for parse_base_json
nlohmann::json match_json;
match_json["name"] = func_name;
match_json["parameters"] = arguments;
auto parsed_calls = parse_base_json(match_json, tools);
calls.insert(calls.end(),
std::make_move_iterator(parsed_calls.begin()),
std::make_move_iterator(parsed_calls.end()));
}
}
return StreamingParseResult(normal_text, calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in GLM-4.5 detect_and_parse: " << e.what();
return StreamingParseResult(text, {});
}
}
StreamingParseResult Glm45Detector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
buffer_ += new_text;
std::string current_text = buffer_;
size_t start = current_text.find(bot_token_);
if (start == std::string::npos) {
buffer_.clear();
if (current_tool_id_ > 0) {
current_text = "";
}
return StreamingParseResult(current_text, {});
}
// Look for complete tool call
size_t end = current_text.find(eot_token_);
if (end != std::string::npos) {
// Initialize state if this is the first tool call
if (current_tool_id_ == -1) {
current_tool_id_ = 0;
prev_tool_call_arr_.clear();
streamed_args_for_tool_.clear();
streamed_args_for_tool_.push_back("");
}
// Ensure we have enough entries in tracking arrays
while (prev_tool_call_arr_.size() <= current_tool_id_) {
prev_tool_call_arr_.push_back({});
}
while (streamed_args_for_tool_.size() <= current_tool_id_) {
streamed_args_for_tool_.push_back("");
}
// Parse the complete tool call
std::string complete_call =
current_text.substr(0, end + eot_token_.length());
StreamingParseResult result = detect_and_parse(complete_call, tools);
if (!result.calls.empty()) {
// Store tool call info for serving layer
prev_tool_call_arr_[current_tool_id_]["name"] =
result.calls[0].name.value_or("");
prev_tool_call_arr_[current_tool_id_]["arguments"] =
result.calls[0].parameters;
streamed_args_for_tool_[current_tool_id_] = result.calls[0].parameters;
// Update tool index
result.calls[0].tool_index = current_tool_id_;
current_tool_id_++;
}
// Update buffer with remaining text
buffer_ = current_text.substr(end + eot_token_.length());
return result;
}
// Return normal text before tool call start
std::string normal_text = current_text.substr(0, start);
buffer_ = current_text.substr(start);
return StreamingParseResult(normal_text, {});
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,75 @@
/* 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.
==============================================================================*/
#pragma once
#include <regex>
#include <string>
#include <string_view>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
/**
* Detector for GLM-4.5 model function call format.
*
* Format Structure:
* ```
* <tool_call>function_name
* <arg_key>param1</arg_key>
* <arg_value>value1</arg_value>
* <arg_key>param2</arg_key>
* <arg_value>value2</arg_value>
* </tool_call>
* ```
*
* Example:
* ```
* <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>
* ```
*/
class Glm45Detector : public BaseFormatDetector {
public:
Glm45Detector();
virtual ~Glm45Detector() = default;
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
std::regex func_call_regex_;
std::regex func_detail_regex_;
std::regex func_arg_regex_;
std::string trim_whitespace(std::string_view str) const;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,771 @@
/* 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 <algorithm>
#include <iostream>
#include <sstream>
namespace xllm {
namespace function_call {
Glm47Detector::Glm47Detector() : BaseFormatDetector() {
bot_token_ = "<tool_call>";
eot_token_ = "</tool_call>";
last_arguments_ = "";
streamed_raw_length_ = 0;
reset_streaming_state();
}
void Glm47Detector::reset_streaming_state() {
stream_state_ = StreamState::INIT;
current_key_ = "";
current_value_ = "";
xml_tag_buffer_ = "";
is_first_param_ = true;
value_started_ = false;
cached_value_type_ = "";
utf8_buffer_ = "";
}
std::pair<std::string, std::string> Glm47Detector::split_incomplete_utf8(
const std::string& str) const {
if (str.empty()) {
return {"", ""};
}
size_t len = str.length();
// Find the start of the last potential character by scanning backwards.
size_t start_pos = len;
for (size_t i = 1; i <= len && i <= 4; ++i) {
if ((static_cast<unsigned char>(str[len - i]) & 0xC0) != 0x80) {
start_pos = len - i;
break;
}
}
if (start_pos == len) {
// String ends with more than 3 continuation bytes, or is empty.
// This is invalid; do not buffer to prevent DoS via unbounded growth.
return {str, ""};
}
// Check the character starting at start_pos
const unsigned char start_byte = str[start_pos];
size_t needed;
if ((start_byte & 0x80) == 0) {
needed = 1;
} else if ((start_byte & 0xE0) == 0xC0) {
needed = 2;
} else if ((start_byte & 0xF0) == 0xE0) {
needed = 3;
} else if ((start_byte & 0xF8) == 0xF0) {
needed = 4;
} else {
// Invalid start byte. The fragment from start_pos is corrupt.
// Do not buffer to prevent DoS via unbounded growth.
return {str, ""};
}
const size_t available = len - start_pos;
if (available == needed) {
// The last character has the correct number of bytes. Assume it's complete.
return {str, ""};
} else {
// The last character is incomplete or has extra bytes. Treat as incomplete.
return {str.substr(0, start_pos), str.substr(start_pos)};
}
}
std::string Glm47Detector::trim_whitespace(std::string_view str) const {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string_view::npos) {
return std::string{};
}
size_t end = str.find_last_not_of(whitespace);
return std::string(str.substr(start, end - start + 1));
}
bool Glm47Detector::has_tool_call(const std::string& text) {
return text.find(bot_token_) != std::string::npos;
}
std::vector<std::pair<size_t, size_t>> Glm47Detector::find_tool_call_ranges(
const std::string& text) const {
std::vector<std::pair<size_t, size_t>> ranges;
// Pre-allocate for typical case: most requests have 1-4 tool calls
ranges.reserve(4);
size_t search_pos = 0;
const size_t bot_len = bot_token_.length();
const size_t eot_len = eot_token_.length();
while (search_pos < text.length()) {
size_t start_pos = text.find(bot_token_, search_pos);
if (start_pos == std::string::npos) break;
size_t content_start = start_pos + bot_len;
size_t end_pos = text.find(eot_token_, content_start);
if (end_pos == std::string::npos) break;
ranges.emplace_back(content_start, end_pos);
search_pos = end_pos + eot_len;
}
return ranges;
}
std::pair<std::string, std::string> Glm47Detector::parse_tool_call_content(
const std::string& content) const {
const std::string arg_key_tag = "<arg_key>";
size_t arg_pos = content.find(arg_key_tag);
if (arg_pos == std::string::npos) {
// No arguments, entire content is function name
return {trim_whitespace(content), ""};
}
std::string func_name = trim_whitespace(content.substr(0, arg_pos));
std::string args_raw = content.substr(arg_pos);
return {func_name, args_raw};
}
std::vector<std::pair<std::string, std::string>>
Glm47Detector::extract_argument_pairs(const std::string& args_raw) const {
std::vector<std::pair<std::string, std::string>> pairs;
const std::string key_open = "<arg_key>";
const std::string key_close = "</arg_key>";
const std::string val_open = "<arg_value>";
const std::string val_close = "</arg_value>";
size_t pos = 0;
while (pos < args_raw.length()) {
size_t key_start = args_raw.find(key_open, pos);
if (key_start == std::string::npos) break;
key_start += key_open.length();
size_t key_end = args_raw.find(key_close, key_start);
if (key_end == std::string::npos) break;
size_t val_start = args_raw.find(val_open, key_end);
if (val_start == std::string::npos) break;
// Check for an intervening key tag, which indicates a malformed pair where
// a key is missing its value.
size_t next_key_start =
args_raw.find(key_open, key_end + key_close.length());
if (next_key_start != std::string::npos && next_key_start < val_start) {
// Skip to the next key, as this one is missing a value.
pos = next_key_start;
continue;
}
val_start += val_open.length();
size_t val_end = args_raw.find(val_close, val_start);
if (val_end == std::string::npos) break;
std::string key = args_raw.substr(key_start, key_end - key_start);
std::string value = args_raw.substr(val_start, val_end - val_start);
pairs.emplace_back(key, value);
pos = val_end + val_close.length();
}
return pairs;
}
std::string Glm47Detector::get_argument_type(
const std::string& func_name,
const std::string& arg_key,
const std::vector<JsonTool>& tools) const {
// Build name to tool map
std::unordered_map<std::string, const JsonTool*> name2tool;
for (const auto& tool : tools) {
name2tool[tool.function.name] = &tool;
}
auto it = name2tool.find(func_name);
if (it == name2tool.end()) {
return "";
}
const JsonTool* tool = it->second;
if (!tool->function.parameters.contains("properties")) {
return "";
}
const auto& properties = tool->function.parameters["properties"];
if (!properties.is_object() || !properties.contains(arg_key)) {
return "";
}
const auto& prop = properties[arg_key];
if (prop.contains("type") && prop["type"].is_string()) {
return prop["type"].get<std::string>();
}
return "";
}
nlohmann::json Glm47Detector::convert_to_number(
const std::string& value) const {
try {
std::string trimmed = trim_whitespace(value);
if (trimmed.find('.') != std::string::npos ||
trimmed.find('e') != std::string::npos ||
trimmed.find('E') != std::string::npos) {
return nlohmann::json(std::stod(trimmed));
} else {
return nlohmann::json(std::stoll(trimmed));
}
} catch (const std::exception&) {
return nlohmann::json(value);
}
}
std::pair<nlohmann::json, bool> Glm47Detector::parse_arguments(
const std::string& json_value,
const std::string& arg_type) const {
// Strategy 1: Direct JSON parsing
try {
nlohmann::json parsed_value = nlohmann::json::parse(json_value);
// Type coercion for number type
if (arg_type == "number" && parsed_value.is_string()) {
parsed_value = convert_to_number(parsed_value.get<std::string>());
}
return {parsed_value, true};
} catch (const nlohmann::json::parse_error&) {
// Continue to next strategy
}
// Strategy 2: Unescape and parse
try {
std::string wrapped = "{\"tmp\": \"" + json_value + "\"}";
nlohmann::json temp = nlohmann::json::parse(wrapped);
nlohmann::json parsed_value =
nlohmann::json::parse(temp["tmp"].get<std::string>());
if (arg_type == "number" && parsed_value.is_string()) {
parsed_value = convert_to_number(parsed_value.get<std::string>());
}
return {parsed_value, true};
} catch (const nlohmann::json::parse_error&) {
// Continue to next strategy
} catch (const std::exception&) {
// Continue to next strategy
}
// Strategy 3: Treat as string
try {
return {nlohmann::json(json_value), true};
} catch (const std::exception&) {
return {nlohmann::json(json_value), false};
}
}
std::unordered_map<std::string, nlohmann::json>
Glm47Detector::parse_argument_pairs(
const std::vector<std::pair<std::string, std::string>>& pairs,
const std::string& func_name,
const std::vector<JsonTool>& tools) const {
std::unordered_map<std::string, nlohmann::json> arguments;
for (const auto& [arg_key, arg_value] : pairs) {
std::string key = trim_whitespace(arg_key);
std::string value = trim_whitespace(arg_value);
std::string arg_type = get_argument_type(func_name, key, tools);
auto [parsed_value, is_good_json] = parse_arguments(value, arg_type);
if (arg_type == "string") {
// Only convert to string if explicitly defined as string type
if (parsed_value.is_string()) {
arguments[key] = parsed_value;
} else if (parsed_value.is_object() || parsed_value.is_array()) {
// If parsed as dict/list but schema says string, convert to JSON string
arguments[key] = parsed_value.dump();
} else {
arguments[key] = parsed_value.dump();
}
} else if (arg_type.empty()) {
// If type is not defined, keep the parsed value as-is
arguments[key] = is_good_json ? parsed_value : nlohmann::json(value);
} else {
// For other types (number, object, array, etc.), use parsed value
arguments[key] = is_good_json ? parsed_value : nlohmann::json(value);
}
}
return arguments;
}
StreamingParseResult Glm47Detector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
size_t idx = text.find(bot_token_);
std::string normal_text =
(idx != std::string::npos) ? text.substr(0, idx) : text;
// Trim normal text
if (!normal_text.empty()) {
normal_text = trim_whitespace(normal_text);
}
if (idx == std::string::npos) {
return StreamingParseResult(normal_text, {});
}
std::vector<ToolCallItem> calls;
try {
// Use string-based parsing instead of regex to avoid stack overflow
auto ranges = find_tool_call_ranges(text);
for (const auto& range : ranges) {
std::string content =
text.substr(range.first, range.second - range.first);
auto [func_name, args_raw] = parse_tool_call_content(content);
auto pairs = extract_argument_pairs(args_raw);
auto arguments = parse_argument_pairs(pairs, func_name, tools);
// Create JSON object for parse_base_json
nlohmann::json match_json;
match_json["name"] = func_name;
match_json["parameters"] = arguments;
auto parsed_calls = parse_base_json(match_json, tools);
calls.insert(calls.end(),
std::make_move_iterator(parsed_calls.begin()),
std::make_move_iterator(parsed_calls.end()));
}
return StreamingParseResult(normal_text, calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in GLM-4.7 detect_and_parse: " << e.what();
return StreamingParseResult(text, {});
}
}
std::string Glm47Detector::get_value_type(
const std::string& func_name,
const std::string& key,
const std::vector<JsonTool>& tools) const {
std::string arg_type = get_argument_type(func_name, key, tools);
if (!arg_type.empty()) {
return arg_type;
}
// Auto-detect type from value (best effort)
std::string first_chars = trim_whitespace(current_value_);
if (first_chars.length() >= 10) {
first_chars = first_chars.substr(0, 10);
}
if (!first_chars.empty()) {
char first_char = first_chars[0];
if (std::isdigit(first_char) || first_char == '-' || first_char == '.') {
return "number";
} else if (first_char == '{' || first_char == '[') {
return "object";
}
}
return "string";
}
std::string Glm47Detector::format_value_complete(
const std::string& value,
const std::string& value_type) const {
if (value_type == "string") {
// Ensure proper JSON string formatting with quotes
return nlohmann::json(value).dump();
} else if (value_type == "number") {
try {
nlohmann::json num = convert_to_number(trim_whitespace(value));
return num.dump();
} catch (const std::exception&) {
// Fallback to string if not a valid number
LOG(WARNING) << "Failed to parse '" << value
<< "' as number, treating as string";
return nlohmann::json(value).dump();
}
} else {
// For object/array types, return as-is (should already be valid JSON)
return value;
}
}
std::string Glm47Detector::process_xml_to_json_streaming(
const std::string& raw_increment,
const std::string& func_name,
const std::vector<JsonTool>& tools) {
std::string json_output;
for (char ch : raw_increment) {
xml_tag_buffer_ += ch;
if (stream_state_ == StreamState::INIT ||
stream_state_ == StreamState::BETWEEN) {
if (xml_tag_buffer_.size() >= 9 &&
xml_tag_buffer_.substr(xml_tag_buffer_.size() - 9) == "<arg_key>") {
stream_state_ = StreamState::IN_KEY;
current_key_ = "";
xml_tag_buffer_ = "";
json_output += is_first_param_ ? "{" : ", ";
is_first_param_ = false;
}
} else if (stream_state_ == StreamState::IN_KEY) {
if (xml_tag_buffer_.size() >= 10 &&
xml_tag_buffer_.substr(xml_tag_buffer_.size() - 10) == "</arg_key>") {
current_key_ = xml_tag_buffer_.substr(0, xml_tag_buffer_.size() - 10);
current_key_ = trim_whitespace(current_key_);
xml_tag_buffer_ = "";
stream_state_ = StreamState::WAITING_VALUE;
json_output += nlohmann::json(current_key_).dump() + ": ";
}
} else if (stream_state_ == StreamState::WAITING_VALUE) {
if (xml_tag_buffer_.size() >= 11 &&
xml_tag_buffer_.substr(xml_tag_buffer_.size() - 11) ==
"<arg_value>") {
stream_state_ = StreamState::IN_VALUE;
current_value_ = "";
xml_tag_buffer_ = "";
value_started_ = false;
// Determine and cache the value type at the start
cached_value_type_ = get_value_type(func_name, current_key_, tools);
}
} else if (stream_state_ == StreamState::IN_VALUE) {
if (xml_tag_buffer_.size() >= 12 &&
xml_tag_buffer_.substr(xml_tag_buffer_.size() - 12) ==
"</arg_value>") {
std::string final_value =
xml_tag_buffer_.substr(0, xml_tag_buffer_.size() - 12);
current_value_ += final_value;
// Use cached value type for consistency
std::string value_type =
cached_value_type_.empty() ? "string" : cached_value_type_;
if (value_started_) {
// Output any remaining content (including buffered UTF-8 bytes)
std::string full_final = utf8_buffer_ + final_value;
utf8_buffer_ = "";
if (!full_final.empty()) {
if (value_type == "string") {
try {
std::string escaped = nlohmann::json(full_final).dump();
json_output += escaped.substr(1, escaped.size() - 2);
} catch (const std::exception& e) {
// If JSON parsing fails, log and output with JSON escaping
LOG(WARNING) << "Failed to escape final content: " << e.what();
for (unsigned char c : full_final) {
if (c == '"')
json_output += "\\\"";
else if (c == '\\')
json_output += "\\\\";
else if (c < 0x20) {
// Escape control characters as \uXXXX
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", c);
json_output += buf;
} else
json_output += static_cast<char>(c);
}
}
} else {
json_output += full_final;
}
}
// Always output closing quote for string type when value was started
if (value_type == "string") {
json_output += "\"";
}
} else {
// Value was never started (empty or complete in one chunk)
json_output += format_value_complete(current_value_, value_type);
}
xml_tag_buffer_ = "";
stream_state_ = StreamState::BETWEEN;
current_value_ = "";
value_started_ = false;
cached_value_type_ = "";
} else {
// Check if buffer could be start of closing tag
std::string closing_tag = "</arg_value>";
bool is_potential_closing =
xml_tag_buffer_.size() <= closing_tag.size() &&
closing_tag.substr(0, xml_tag_buffer_.size()) == xml_tag_buffer_;
if (!is_potential_closing) {
std::string content = xml_tag_buffer_;
// Use cached value type for consistency
std::string value_type =
cached_value_type_.empty() ? "string" : cached_value_type_;
if (value_type == "string") {
if (!value_started_) {
json_output += "\"";
value_started_ = true;
}
if (!content.empty()) {
// Prepend any buffered UTF-8 bytes from previous chunk
std::string full_content = utf8_buffer_ + content;
// Split into complete UTF-8 and incomplete tail
auto [complete_utf8, incomplete_tail] =
split_incomplete_utf8(full_content);
if (!complete_utf8.empty()) {
try {
std::string escaped = nlohmann::json(complete_utf8).dump();
json_output += escaped.substr(1, escaped.size() - 2);
current_value_ += complete_utf8;
} catch (const std::exception& e) {
// If JSON parsing still fails, log and output with JSON
// escaping
LOG(WARNING) << "Failed to escape content: " << e.what();
for (unsigned char c : complete_utf8) {
if (c == '"')
json_output += "\\\"";
else if (c == '\\')
json_output += "\\\\";
else if (c < 0x20) {
// Escape control characters as \uXXXX
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", c);
json_output += buf;
} else
json_output += static_cast<char>(c);
}
current_value_ += complete_utf8;
}
}
// Buffer the incomplete UTF-8 tail for next chunk
utf8_buffer_ = incomplete_tail;
xml_tag_buffer_ = "";
}
} else if (value_type == "number") {
if (!content.empty()) {
if (!value_started_) {
value_started_ = true;
}
json_output += content;
current_value_ += content;
xml_tag_buffer_ = "";
}
} else {
// For object/array types, output as-is
if (!content.empty()) {
if (!value_started_) {
value_started_ = true;
}
json_output += content;
current_value_ += content;
xml_tag_buffer_ = "";
}
}
}
}
}
}
return json_output;
}
StreamingParseResult Glm47Detector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
buffer_ += new_text;
std::string current_text = buffer_;
// Check if we have a tool call
bool has_tool_call_marker =
current_text.find(bot_token_) != std::string::npos;
if (!has_tool_call_marker) {
// Check if buffer could be the start of a tool call
bool is_potential_start = false;
for (size_t i = 1; i <= std::min(current_text.size(), bot_token_.size());
++i) {
if (current_text.size() >= i &&
bot_token_.substr(0, i) ==
current_text.substr(current_text.size() - i)) {
is_potential_start = true;
break;
}
}
if (!is_potential_start) {
// Not a potential tool call, return as normal text
std::string output_text = current_text;
buffer_.clear();
// Remove any stray closing tags
size_t pos = 0;
while ((pos = output_text.find(eot_token_, pos)) != std::string::npos) {
output_text.erase(pos, eot_token_.length());
}
return StreamingParseResult(output_text, {});
} else {
// Could be start of tool call, keep buffering
return StreamingParseResult("", {});
}
}
// Initialize tool indices if needed
if (tool_indices_.empty()) {
tool_indices_ = get_tool_indices(tools);
}
std::vector<ToolCallItem> calls;
try {
// Use string-based parsing instead of regex to avoid stack overflow
size_t bot_pos = current_text.find(bot_token_);
if (bot_pos == std::string::npos) {
return StreamingParseResult("", {});
}
size_t content_start = bot_pos + bot_token_.length();
size_t eot_pos = current_text.find(eot_token_, content_start);
bool is_tool_end_flag = (eot_pos != std::string::npos);
// Extract content (partial or complete)
std::string content =
is_tool_end_flag
? current_text.substr(content_start, eot_pos - content_start)
: current_text.substr(content_start);
// Parse function name and args
auto [func_name, func_args_raw] = parse_tool_call_content(content);
// Initialize state if this is the first tool call
if (current_tool_id_ == -1) {
current_tool_id_ = 0;
prev_tool_call_arr_.clear();
streamed_args_for_tool_.clear();
streamed_args_for_tool_.push_back("");
streamed_raw_length_ = 0;
current_tool_name_sent_ = false;
reset_streaming_state();
}
// Ensure we have enough entries in our tracking arrays
while (prev_tool_call_arr_.size() <=
static_cast<size_t>(current_tool_id_)) {
prev_tool_call_arr_.push_back({});
}
while (streamed_args_for_tool_.size() <=
static_cast<size_t>(current_tool_id_)) {
streamed_args_for_tool_.push_back("");
}
// Send tool name first if not sent yet
if (!current_tool_name_sent_) {
// Only send function name when we're sure it's complete:
// - Either we have <arg_key> (arguments started)
// - Or we have </tool_call> (tool call ended with no args)
if (func_name.empty() || (func_args_raw.empty() && !is_tool_end_flag)) {
// Function name not yet complete, wait for more data
return StreamingParseResult("", {});
}
calls.push_back(ToolCallItem(current_tool_id_, func_name, ""));
current_tool_name_sent_ = true;
streamed_raw_length_ = 0;
reset_streaming_state();
// Store the tool call info
prev_tool_call_arr_[current_tool_id_]["name"] = func_name;
prev_tool_call_arr_[current_tool_id_]["arguments"] = "";
} else {
// Process XML to JSON streaming
size_t current_raw_length = func_args_raw.size();
if (current_raw_length > streamed_raw_length_) {
// Get the new raw XML content
std::string raw_increment = func_args_raw.substr(streamed_raw_length_);
// Convert XML increment to JSON increment using state machine
std::string json_increment =
process_xml_to_json_streaming(raw_increment, func_name, tools);
if (!json_increment.empty()) {
calls.push_back(
ToolCallItem(current_tool_id_, std::nullopt, json_increment));
last_arguments_ += json_increment;
streamed_args_for_tool_[current_tool_id_] += json_increment;
}
// Update the streamed length
streamed_raw_length_ = current_raw_length;
}
if (is_tool_end_flag) {
if (is_first_param_) {
std::string empty_object = "{}";
calls.push_back(
ToolCallItem(current_tool_id_, std::nullopt, empty_object));
last_arguments_ += empty_object;
} else if (last_arguments_.empty() || last_arguments_.back() != '}') {
std::string closing_brace = "}";
calls.push_back(
ToolCallItem(current_tool_id_, std::nullopt, closing_brace));
last_arguments_ += closing_brace;
streamed_args_for_tool_[current_tool_id_] += closing_brace;
}
// Use string-based argument extraction
auto pairs = extract_argument_pairs(func_args_raw);
if (!pairs.empty()) {
auto arguments = parse_argument_pairs(pairs, func_name, tools);
nlohmann::json args_json = arguments;
prev_tool_call_arr_[current_tool_id_]["arguments"] = args_json.dump();
}
// Remove the completed tool call from buffer
buffer_ = current_text.substr(eot_pos + eot_token_.length());
StreamingParseResult result("", calls);
current_tool_id_++;
last_arguments_ = "";
current_tool_name_sent_ = false;
streamed_raw_length_ = 0;
reset_streaming_state();
return result;
}
}
return StreamingParseResult("", calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in parse_streaming_increment: " << e.what();
return StreamingParseResult(current_text, {});
}
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,123 @@
/* 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.
==============================================================================*/
#pragma once
#include <string>
#include <string_view>
#include <utility>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
enum class StreamState {
INIT, // Initial state
BETWEEN, // Between key-value pairs
IN_KEY, // Reading key content
WAITING_VALUE, // Waiting for value tag
IN_VALUE // Reading value content
};
/**
* Detector for GLM-4.7 and GLM-5 models function call format.
*
* Format Structure (compact, no newlines):
* ```
* <tool_call>function_name<arg_key>param1</arg_key><arg_value>value1</arg_value><arg_key>param2</arg_key><arg_value>value2</arg_value></tool_call>
* ```
*
* Example:
* ```
* <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>
* ```
*/
class Glm47Detector : public BaseFormatDetector {
public:
Glm47Detector();
virtual ~Glm47Detector() = default;
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
// String-based parsing helpers (replaces regex to avoid stack overflow)
std::vector<std::pair<size_t, size_t>> find_tool_call_ranges(
const std::string& text) const;
std::pair<std::string, std::string> parse_tool_call_content(
const std::string& content) const;
std::vector<std::pair<std::string, std::string>> extract_argument_pairs(
const std::string& args_raw) const;
StreamState stream_state_;
std::string current_key_;
std::string current_value_;
std::string xml_tag_buffer_;
bool is_first_param_;
bool value_started_;
std::string cached_value_type_;
std::string utf8_buffer_; // Buffer for incomplete UTF-8 sequences
std::string last_arguments_;
size_t streamed_raw_length_;
std::string trim_whitespace(std::string_view str) const;
std::string get_argument_type(const std::string& func_name,
const std::string& arg_key,
const std::vector<JsonTool>& tools) const;
nlohmann::json convert_to_number(const std::string& value) const;
std::pair<nlohmann::json, bool> parse_arguments(
const std::string& json_value,
const std::string& arg_type) const;
std::unordered_map<std::string, nlohmann::json> parse_argument_pairs(
const std::vector<std::pair<std::string, std::string>>& pairs,
const std::string& func_name,
const std::vector<JsonTool>& tools) const;
std::string get_value_type(const std::string& func_name,
const std::string& key,
const std::vector<JsonTool>& tools) const;
std::string format_value_complete(const std::string& value,
const std::string& value_type) const;
std::string process_xml_to_json_streaming(const std::string& raw_increment,
const std::string& func_name,
const std::vector<JsonTool>& tools);
void reset_streaming_state();
// Helper to split string into complete UTF-8 part and incomplete tail
// Returns: {complete_utf8_string, incomplete_tail}
std::pair<std::string, std::string> split_incomplete_utf8(
const std::string& str) const;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,293 @@
/* 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 <iostream>
#include <regex>
#include <stdexcept>
namespace xllm {
namespace function_call {
KimiK2Detector::KimiK2Detector() : BaseFormatDetector() {
// Initialize KimiK2 specific tokens
bot_token_ = "<|tool_calls_section_begin|>";
eot_token_ = "<|tool_calls_section_end|>";
tool_call_start_token_ = "<|tool_call_begin|>";
tool_call_end_token_ = "<|tool_call_end|>";
tool_call_argument_begin_token_ = "<|tool_call_argument_begin|>";
// Regex pattern for parsing tool calls with the following format:
// <|tool_call_begin|>functions.{func_name}:{index}
// <|tool_call_argument_begin|>{json_args}<|tool_call_end|>
// Note: C++ regex doesn't support named groups, so we use numbered groups:
// Group 1: tool_call_id (functions.{func_name}:{index})
// Group 2: function_arguments ({json_args})
std::string pattern =
R"(<\|tool_call_begin\|>\s*([\w\.]+:\d+)\s*<\|tool_call_argument_begin\|>\s*(\{.*?\})\s*<\|tool_call_end\|>)";
tool_call_regex_ = std::regex(pattern, std::regex_constants::ECMAScript);
// Regex pattern for streaming parsing (partial tool calls)
std::string stream_pattern =
R"(<\|tool_call_begin\|>\s*([\w\.]+:\d+)\s*<\|tool_call_argument_begin\|>\s*(\{.*))";
stream_tool_call_portion_regex_ =
std::regex(stream_pattern, std::regex_constants::ECMAScript);
last_arguments_ = "";
}
bool KimiK2Detector::has_tool_call(const std::string& text) {
// Check if the text contains the KimiK2 tool call section begin token
return text.find(bot_token_) != std::string::npos;
}
StreamingParseResult KimiK2Detector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
size_t bot_pos = text.find(bot_token_);
std::string normal_text =
(bot_pos != std::string::npos) ? text.substr(0, bot_pos) : text;
if (bot_pos == std::string::npos) {
return StreamingParseResult(normal_text);
}
std::vector<ToolCallItem> calls;
try {
std::sregex_iterator iter(
text.begin() + bot_pos, text.end(), tool_call_regex_);
std::sregex_iterator end;
for (; iter != end; ++iter) {
std::smatch match = *iter;
if (match.size() >= 3) {
std::string tool_call_id = match[1].str();
std::string function_arguments = match[2].str();
std::string function_name = extract_function_name(tool_call_id);
int32_t function_index = extract_function_index(tool_call_id);
calls.emplace_back(function_index, // Use the call index in the
// response, not tool position
function_name, // Function name
function_arguments // JSON parameters
);
}
}
return StreamingParseResult(normal_text, calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in KimiK2 detect_and_parse: " << e.what();
// Return the normal text if parsing fails
return StreamingParseResult(normal_text);
}
}
std::string KimiK2Detector::extract_function_name(
const std::string& tool_call_id) const {
// tool_call_id format: functions.{func_name}:{index}
// Example: functions.get_weather:0
try {
// Find the position of "functions."
size_t functions_pos = tool_call_id.find("functions.");
if (functions_pos == std::string::npos) {
LOG(WARNING)
<< "Invalid tool_call_id format, missing 'functions.' prefix: "
<< tool_call_id;
return "";
}
// Skip "functions." (10 characters)
size_t start_pos = functions_pos + 10;
// Find the position of the last colon
size_t colon_pos = tool_call_id.find_last_of(':');
if (colon_pos == std::string::npos || colon_pos <= start_pos) {
LOG(WARNING) << "Invalid tool_call_id format, missing ':' separator: "
<< tool_call_id;
return "";
}
// Extract function name between "functions." and ":"
return tool_call_id.substr(start_pos, colon_pos - start_pos);
} catch (const std::exception& e) {
LOG(ERROR) << "Error extracting function name from tool_call_id: "
<< tool_call_id << ", error: " << e.what();
return "";
}
}
int32_t KimiK2Detector::extract_function_index(
const std::string& tool_call_id) const {
// tool_call_id format: functions.{func_name}:{index}
// Example: functions.get_weather:0
try {
// Find the position of the last colon
size_t colon_pos = tool_call_id.find_last_of(':');
if (colon_pos == std::string::npos) {
LOG(WARNING) << "Invalid tool_call_id format, missing ':' separator: "
<< tool_call_id;
return 0;
}
// Extract index string after the colon
std::string index_str = tool_call_id.substr(colon_pos + 1);
// Convert to integer
return std::stoi(index_str);
} catch (const std::exception& e) {
LOG(ERROR) << "Error extracting function index from tool_call_id: "
<< tool_call_id << ", error: " << e.what();
return 0;
}
}
StreamingParseResult KimiK2Detector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
buffer_ += new_text;
std::string current_text = buffer_;
bool has_tool_call_marker =
(current_text.find(bot_token_) != std::string::npos ||
current_text.find(tool_call_start_token_) != std::string::npos);
if (!has_tool_call_marker) {
buffer_.clear();
std::string cleaned_text = new_text;
std::vector<std::string> end_tokens = {eot_token_, tool_call_end_token_};
for (const auto& e_token : end_tokens) {
size_t pos = cleaned_text.find(e_token);
if (pos != std::string::npos) {
cleaned_text.erase(pos, e_token.length());
}
}
return StreamingParseResult(cleaned_text, {});
}
if (tool_indices_.empty()) {
tool_indices_ = get_tool_indices(tools);
}
std::vector<ToolCallItem> calls;
try {
std::smatch match;
if (std::regex_search(
current_text, match, stream_tool_call_portion_regex_)) {
std::string function_id = match[1].str();
std::string function_args = match[2].str();
std::string function_name = extract_function_name(function_id);
if (current_tool_id_ == -1) {
current_tool_id_ = 0;
prev_tool_call_arr_.clear();
streamed_args_for_tool_.clear();
streamed_args_for_tool_.push_back("");
}
while (static_cast<int>(prev_tool_call_arr_.size()) <= current_tool_id_) {
prev_tool_call_arr_.push_back(
std::unordered_map<std::string, std::string>());
}
while (static_cast<int>(streamed_args_for_tool_.size()) <=
current_tool_id_) {
streamed_args_for_tool_.push_back("");
}
if (!current_tool_name_sent_) {
calls.emplace_back(current_tool_id_, function_name, "");
current_tool_name_sent_ = true;
prev_tool_call_arr_[current_tool_id_]["name"] = function_name;
prev_tool_call_arr_[current_tool_id_]["arguments"] = "{}";
} else {
std::string argument_diff;
if (function_args.length() > last_arguments_.length() &&
function_args.substr(0, last_arguments_.length()) ==
last_arguments_) {
argument_diff = function_args.substr(last_arguments_.length());
} else {
argument_diff = function_args;
}
size_t end_pos = argument_diff.find(tool_call_end_token_);
if (end_pos != std::string::npos) {
argument_diff = argument_diff.substr(0, end_pos);
}
if (!argument_diff.empty()) {
calls.emplace_back(current_tool_id_, std::nullopt, argument_diff);
last_arguments_ += argument_diff;
streamed_args_for_tool_[current_tool_id_] += argument_diff;
}
std::string parsed_args = function_args;
end_pos = parsed_args.find(tool_call_end_token_);
if (end_pos != std::string::npos) {
parsed_args = parsed_args.substr(0, end_pos);
}
if (is_complete_json(parsed_args)) {
try {
auto parsed_json = nlohmann::json::parse(parsed_args);
prev_tool_call_arr_[current_tool_id_]["arguments"] =
parsed_json.dump();
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to parse JSON arguments: " << e.what();
}
std::regex tool_call_end_pattern(
R"(<\|tool_call_begin\|>.*?<\|tool_call_end\|>)",
std::regex_constants::ECMAScript);
std::smatch end_match;
if (std::regex_search(
current_text, end_match, tool_call_end_pattern)) {
buffer_ =
current_text.substr(end_match.position() + end_match.length());
} else {
buffer_.clear();
}
StreamingParseResult result("", calls);
current_tool_id_++;
last_arguments_.clear();
current_tool_name_sent_ = false;
return result;
}
}
}
return StreamingParseResult("", calls);
} catch (const std::exception& e) {
LOG(ERROR) << "Error in KimiK2 parse_streaming_increment: " << e.what();
return StreamingParseResult(current_text, {});
}
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,74 @@
/* 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.
==============================================================================*/
#pragma once
#include <regex>
#include <string>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
/**
* Detector for Kimi K2 model function call format.
*
* Format Structure:
* ```
* <|tool_calls_section_begin|>
* <|tool_call_begin|>functions.{func_name}:{index}
* <|tool_call_argument_begin|>{json_args}<|tool_call_end|>
* <|tool_calls_section_end|>
* ```
*
* Reference:
* https://huggingface.co/moonshotai/Kimi-K2-Instruct/blob/main/docs/tool_call_guidance.md
*/
class KimiK2Detector : public BaseFormatDetector {
public:
KimiK2Detector();
virtual ~KimiK2Detector() = default;
private:
std::string tool_call_start_token_;
std::string tool_call_end_token_;
std::string tool_call_argument_begin_token_;
std::regex tool_call_regex_;
std::regex stream_tool_call_portion_regex_;
std::string last_arguments_;
public:
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
std::string extract_function_name(const std::string& tool_call_id) const;
int32_t extract_function_index(const std::string& tool_call_id) const;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,12 @@
include(cc_library)
cc_library(
NAME
partial_json_parser
SRCS
src/parser.cpp
INCLUDES
include
DEPS
nlohmann_json::nlohmann_json
)

View File

@@ -0,0 +1,28 @@
#ifndef PARTIAL_JSON_PARSER_OPTIONS_H
#define PARTIAL_JSON_PARSER_OPTIONS_H
namespace partial_json_parser {
enum TypeOptions {
STR = 1 << 0, // 1
NUM = 1 << 1, // 2
ARR = 1 << 2, // 4
OBJ = 1 << 3, // 8
NULL_TYPE = 1 << 4, // 16 (using NULL_TYPE to avoid conflict with NULL macro)
BOOL = 1 << 5, // 32
NAN_TYPE = 1 << 6, // 64 (using NAN_TYPE to avoid conflict with NAN macro)
INFINITY_TYPE =
1
<< 7, // 128 (using INFINITY_TYPE to avoid conflict with INFINITY macro)
NEG_INFINITY = 1 << 8, // 256
INF = INFINITY_TYPE | NEG_INFINITY,
SPECIAL = NULL_TYPE | BOOL | INF | NAN_TYPE,
ATOM = STR | NUM | SPECIAL,
COLLECTION = ARR | OBJ,
ALL = ATOM | COLLECTION
};
} // namespace partial_json_parser
#endif // PARTIAL_JSON_PARSER_OPTIONS_H

View File

@@ -0,0 +1,48 @@
#ifndef PARTIAL_JSON_PARSER_PARSER_H
#define PARTIAL_JSON_PARSER_PARSER_H
#include <stdexcept>
#include <string>
#include "options.h"
namespace partial_json_parser {
class MalformedJSONException : public std::runtime_error {
public:
explicit MalformedJSONException(const std::string& message)
: std::runtime_error(message) {}
};
struct JsonCompletion {
int32_t index;
std::string string;
JsonCompletion(int32_t idx = 0, const std::string& str = "")
: index(idx), string(str) {}
};
std::string parse_malformed_string(const std::string& malformed,
TypeOptions options,
bool format = false);
std::string parse_json(const std::string& json_string, TypeOptions allowed);
JsonCompletion complete_any(const std::string& json_string,
TypeOptions allowed,
bool top_level);
JsonCompletion complete_string(const std::string& json_string,
TypeOptions allowed);
JsonCompletion complete_array(const std::string& json_string,
TypeOptions allowed);
JsonCompletion complete_object(const std::string& json_string,
TypeOptions allowed);
JsonCompletion complete_number(const std::string& json_string,
TypeOptions allowed,
bool top_level);
int32_t skip_blank(const std::string& text, int32_t index);
std::string format_json(const std::string& json_string);
} // namespace partial_json_parser
#endif // PARTIAL_JSON_PARSER_PARSER_H

View File

@@ -0,0 +1,464 @@
/**
* Partial JSON Parser - C++ Implementation
*
* Based on:
* - Python: https://github.com/promplate/partial-json-parser
* - Go: https://github.com/blaze2305/partial-json-parser
*
* Thanks to the original authors for their excellent work.
*/
#include "partial_json_parser/parser.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <nlohmann/json.hpp>
#include <sstream>
namespace partial_json_parser {
std::string format_json(const std::string& json_string) {
try {
auto json = nlohmann::json::parse(json_string);
return json.dump(1);
} catch (const std::exception& e) {
return json_string;
}
}
std::string parse_malformed_string(const std::string& malformed,
TypeOptions options,
bool format) {
std::string str = malformed;
// Trim whitespace
str.erase(str.begin(),
std::find_if(str.begin(), str.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
str.erase(std::find_if(str.rbegin(),
str.rend(),
[](unsigned char ch) { return !std::isspace(ch); })
.base(),
str.end());
if (str.empty()) {
throw MalformedJSONException("string is empty; cannot parse");
}
std::string json_string = parse_json(malformed, options);
if (format) {
return format_json(json_string);
} else {
return json_string;
}
}
int32_t skip_blank(const std::string& text, int32_t index) {
int32_t i = index;
while (
i < static_cast<int>(text.length()) &&
(std::isspace(static_cast<unsigned char>(text[i])) || text[i] == '\n')) {
i += 1;
}
return i;
}
std::string parse_json(const std::string& json_string, TypeOptions allowed) {
int32_t i = skip_blank(json_string, 0);
std::string value = json_string.substr(i);
JsonCompletion completion = complete_any(value, allowed, true);
if (completion.index == 0 && completion.string.empty()) {
throw MalformedJSONException("no valid JSON content found");
}
return value.substr(0, completion.index) + completion.string;
}
JsonCompletion complete_any(const std::string& json_string,
TypeOptions allowed,
bool top_level) {
if (json_string.empty()) {
throw MalformedJSONException("empty string");
}
char first_char = json_string[0];
switch (first_char) {
case '"':
return complete_string(json_string, allowed);
case '[':
return complete_array(json_string, allowed);
case '{':
return complete_object(json_string, allowed);
case '-': // handles negative numbers
if (json_string.length() == 1) {
throw MalformedJSONException("cannot parse singular '-'");
} else if (json_string.length() > 1 &&
json_string[1] != 'I') { // not negative infinity
return complete_number(json_string, allowed, top_level);
}
break;
default:
if (std::isdigit(static_cast<unsigned char>(first_char))) {
return complete_number(json_string, allowed, top_level);
}
break;
}
// Handle NULL
if (json_string.substr(
0, std::min(4, static_cast<int>(json_string.length()))) == "null") {
return JsonCompletion(4, "");
}
if (json_string.length() < 4 &&
std::string("null").substr(0, json_string.length()) == json_string) {
if ((NULL_TYPE | allowed) == allowed) {
return JsonCompletion(0, "null");
}
throw MalformedJSONException("cannot parse null with given options");
}
// Handle boolean true
if (json_string.substr(
0, std::min(4, static_cast<int>(json_string.length()))) == "true") {
return JsonCompletion(4, "");
}
if (json_string.length() < 4 &&
std::string("true").substr(0, json_string.length()) == json_string) {
if ((BOOL | allowed) == allowed) {
return JsonCompletion(0, "true");
}
throw MalformedJSONException("cannot parse bool with given options");
}
// Handle boolean false
if (json_string.substr(
0, std::min(5, static_cast<int>(json_string.length()))) == "false") {
return JsonCompletion(5, "");
}
if (json_string.length() < 5 &&
std::string("false").substr(0, json_string.length()) == json_string) {
if ((BOOL | allowed) == allowed) {
return JsonCompletion(0, "false");
}
throw MalformedJSONException("cannot parse bool with given options");
}
// Handle infinity
if (json_string.substr(0,
std::min(8, static_cast<int>(json_string.length()))) ==
"Infinity") {
return JsonCompletion(8, "");
}
if (json_string.length() < 8 &&
std::string("Infinity").substr(0, json_string.length()) == json_string) {
if ((INFINITY_TYPE | allowed) == allowed) {
return JsonCompletion(0, "Infinity");
}
throw MalformedJSONException("cannot parse Infinity with given options");
}
// Handle negative infinity
if (json_string.substr(0,
std::min(9, static_cast<int>(json_string.length()))) ==
"-Infinity") {
return JsonCompletion(9, "");
}
if (json_string.length() < 9 &&
std::string("-Infinity").substr(0, json_string.length()) == json_string) {
if ((NEG_INFINITY | allowed) == allowed) {
return JsonCompletion(0, "-Infinity");
}
throw MalformedJSONException("cannot parse -Infinity with given options");
}
// Handle NaN
if (json_string.substr(
0, std::min(3, static_cast<int>(json_string.length()))) == "NaN") {
return JsonCompletion(3, "");
}
if (json_string.length() < 3 &&
std::string("NaN").substr(0, json_string.length()) == json_string) {
if ((NAN_TYPE | allowed) == allowed) {
return JsonCompletion(0, "NaN");
}
throw MalformedJSONException("cannot parse NaN with given options");
}
throw MalformedJSONException(std::string("MalformedJSON(unexpected char ") +
first_char + ")");
}
JsonCompletion complete_string(const std::string& json_string,
TypeOptions allowed) {
if (json_string.empty() || json_string[0] != '"') {
throw MalformedJSONException("string must start with quote");
}
int32_t index = 1;
bool char_escaped = false;
int32_t string_length = static_cast<int32_t>(json_string.length());
while (index < string_length && (json_string[index] != '"' || char_escaped)) {
if (json_string[index] == '\\') {
char_escaped = !char_escaped;
} else {
char_escaped = false;
}
index += 1;
}
if (index < string_length) {
return JsonCompletion(index + 1, "");
}
if ((STR | allowed) != allowed) {
throw MalformedJSONException("cannot complete malformed json");
}
// Handle unicode and hex strings
// Handle \uXXXX
size_t u_index = json_string.rfind("\\u");
if (u_index != std::string::npos) {
if (static_cast<int>(u_index) + 6 == string_length) {
return JsonCompletion(static_cast<int>(u_index) + 6, "\"");
}
return JsonCompletion(static_cast<int>(u_index) + 2, "\"");
}
// Handle \UXXXXXXXX
size_t U_index = json_string.rfind("\\U");
if (U_index != std::string::npos) {
if (static_cast<int>(U_index) + 10 == string_length) {
return JsonCompletion(static_cast<int>(U_index) + 10, "\"");
}
return JsonCompletion(static_cast<int>(U_index) + 2, "\"");
}
// Handle \xXX
size_t x_index = json_string.rfind("\\x");
if (x_index != std::string::npos) {
if (static_cast<int>(x_index) + 4 == string_length) {
return JsonCompletion(static_cast<int>(x_index) + 4, "\"");
}
return JsonCompletion(static_cast<int>(x_index) + 2, "\"");
}
if (char_escaped) {
return JsonCompletion(index - 1, "\"");
}
return JsonCompletion(index, "\"");
}
JsonCompletion complete_array(const std::string& json_string,
TypeOptions allowed) {
int32_t i = 1;
int32_t j = 1;
int32_t length = static_cast<int32_t>(json_string.length());
while (j < length) {
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
if (json_string[j] == ']') {
return JsonCompletion(j + 1, "");
}
try {
JsonCompletion result =
complete_any(json_string.substr(j), allowed, false);
// If the string in the result has some char in it, complete the array
if (!result.string.empty()) {
if ((ARR | allowed) == allowed) {
return JsonCompletion(j + result.index, result.string + "]");
}
throw MalformedJSONException("cannot parse array with given options");
}
// First item in array is fine, check other items
j += result.index;
i = j;
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
if (json_string[j] == ',') {
j += 1;
} else if (json_string[j] == ']') {
return JsonCompletion(j + 1, "");
} else {
throw MalformedJSONException(
std::string("MalformedJSON(expected \",\" or \"]\" got ") +
json_string[j] + ")");
}
} catch (const MalformedJSONException&) {
// Can't complete the array, make it empty
if ((ARR | allowed) == allowed) {
return JsonCompletion(i, "]");
}
throw MalformedJSONException("cannot parse array with given options");
}
}
// Reached end of string, close array at last known good point
if ((ARR | allowed) == allowed) {
return JsonCompletion(i, "]");
}
throw MalformedJSONException("cannot parse array with given options");
}
JsonCompletion complete_object(const std::string& json_string,
TypeOptions allowed) {
int32_t i = 1;
int32_t j = 1;
int32_t length = static_cast<int32_t>(json_string.length());
while (j < length) {
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
if (json_string[j] == '}') {
return JsonCompletion(j + 1, "");
}
try {
JsonCompletion key = complete_string(json_string.substr(j), allowed);
if (!key.string.empty()) {
// Can't parse the key or key is incomplete
if ((OBJ | allowed) == allowed) {
return JsonCompletion(i, "}");
}
throw MalformedJSONException("cannot parse object with given options");
}
// Move index by key length
j += key.index;
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
if (json_string[j] != ':') {
throw MalformedJSONException(
std::string("MalformedJSON( expected \":\" got ") + json_string[j] +
")");
}
j += 1;
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
JsonCompletion result =
complete_any(json_string.substr(j), allowed, false);
// If the string in the result has some char in it, complete the object
if (!result.string.empty()) {
if ((OBJ | allowed) == allowed) {
return JsonCompletion(j + result.index, result.string + "}");
}
throw MalformedJSONException("cannot parse object with given options");
}
// First key-value pair is fine, check other items
j += result.index;
i = j;
j = skip_blank(json_string, j);
if (j >= length) {
break;
}
if (json_string[j] == ',') {
j += 1;
} else if (json_string[j] == '}') {
return JsonCompletion(j + 1, "");
} else {
throw MalformedJSONException(
std::string("MalformedJSON(expected \",\" or \"}\" got ") +
json_string[j] + ")");
}
} catch (const MalformedJSONException& e) {
// Check if this is a case where the key is not a valid string start
// For cases like "{0", we should throw the exception
if (j < length && json_string[j] != '"' && json_string[j] != '}') {
// This is an invalid key (not starting with quote), re-throw exception
throw e;
}
// For other cases (like incomplete but valid objects), return empty
// object
if ((OBJ | allowed) == allowed) {
return JsonCompletion(i, "}");
}
throw MalformedJSONException("cannot parse object with given options");
}
}
// Reached end of string, close object at last known good point
if ((OBJ | allowed) == allowed) {
return JsonCompletion(i, "}");
}
throw MalformedJSONException("cannot parse object with given options");
}
JsonCompletion complete_number(const std::string& json_string,
TypeOptions allowed,
bool top_level) {
int32_t i = 1;
int32_t length = static_cast<int32_t>(json_string.length());
// Move forwards while we still have numbers, including exponents and decimals
while (i < length &&
(std::isdigit(static_cast<unsigned char>(json_string[i])) ||
json_string[i] == '.' || json_string[i] == '+' ||
json_string[i] == '-' || json_string[i] == 'e' ||
json_string[i] == 'E')) {
i += 1;
}
bool special_num = false;
// no boundary check initially
while (i >= 1 && (json_string[i - 1] == '.' || json_string[i - 1] == '-' ||
json_string[i - 1] == '+' || json_string[i - 1] == 'e' ||
json_string[i - 1] == 'E')) {
i -= 1;
special_num = true;
// If we've gone to position 0, we need to check if we can continue
if (i == 0) {
throw MalformedJSONException("string index out of range");
}
}
if (special_num || (i == length && !top_level)) {
if ((NUM | allowed) == allowed) {
return JsonCompletion(i, "");
}
throw MalformedJSONException("cannot parse number with given options");
}
return JsonCompletion(i, "");
}
} // namespace partial_json_parser

View File

@@ -0,0 +1,176 @@
/* 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 <algorithm>
#include <iostream>
#include <string_view>
namespace xllm {
namespace function_call {
Qwen25Detector::Qwen25Detector() : BaseFormatDetector() {
bot_token_ = "<tool_call>\n";
eot_token_ = "\n</tool_call>";
tool_call_separator_ = "\n";
std::string pattern = bot_token_ + "([\\s\\S]*?)" + eot_token_;
tool_call_regex_ = std::regex(
pattern,
std::regex_constants::ECMAScript | std::regex_constants::optimize);
}
bool Qwen25Detector::has_tool_call(const std::string& text) {
return text.find(bot_token_) != std::string::npos;
}
std::string_view Qwen25Detector::trim_whitespace(std::string_view str) const {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string_view::npos) {
return std::string_view{};
}
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
std::vector<std::pair<size_t, size_t>> Qwen25Detector::find_tool_call_ranges(
const std::string& text) const {
std::vector<std::pair<size_t, size_t>> ranges;
ranges.reserve(4);
size_t search_pos = 0;
const size_t bot_token_len = bot_token_.length();
const size_t eot_token_len = eot_token_.length();
while (search_pos < text.length()) {
size_t start_pos = text.find(bot_token_, search_pos);
if (start_pos == std::string::npos) {
break;
}
size_t content_start = start_pos + bot_token_len;
size_t end_pos = text.find(eot_token_, content_start);
if (end_pos == std::string::npos) {
break;
}
ranges.emplace_back(content_start, end_pos);
search_pos = end_pos + eot_token_len;
}
return ranges;
}
StreamingParseResult Qwen25Detector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
size_t bot_token_pos = text.find(bot_token_);
std::string normal_text;
if (bot_token_pos != std::string::npos) {
std::string_view normal_text_view(text.data(), bot_token_pos);
std::string_view trimmed = trim_whitespace(normal_text_view);
normal_text = std::string(trimmed);
} else {
std::string_view trimmed = trim_whitespace(text);
normal_text = std::string(trimmed);
return StreamingParseResult(normal_text);
}
auto tool_call_ranges = find_tool_call_ranges(text);
std::vector<ToolCallItem> calls;
calls.reserve(tool_call_ranges.size());
for (const auto& range : tool_call_ranges) {
std::string_view content_view(text.data() + range.first,
range.second - range.first);
std::string_view trimmed_content = trim_whitespace(content_view);
if (trimmed_content.empty()) {
continue;
}
try {
std::string json_content(trimmed_content);
auto json_obj = nlohmann::json::parse(json_content);
auto parsed_calls = parse_base_json(json_obj, tools);
calls.insert(calls.end(),
std::make_move_iterator(parsed_calls.begin()),
std::make_move_iterator(parsed_calls.end()));
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to parse JSON part: "
<< std::string(trimmed_content)
<< ", JSON parse error: " << e.what();
continue;
}
}
return StreamingParseResult(std::move(normal_text), std::move(calls));
}
StreamingParseResult Qwen25Detector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
// Streaming incremental parsing for Qwen 2.5/3 tool calls.
// Uses base class implementation with buffering to handle partial end tokens.
StreamingParseResult result =
BaseFormatDetector::parse_streaming_increment(new_text, tools);
// Handle partial end tokens that are streamed character by character
if (!result.normal_text.empty()) {
normal_text_buffer_ += result.normal_text;
// Check if buffer contains complete end token (without leading newline)
std::string end_token_without_newline =
eot_token_.substr(1); // "</tool_call>"
size_t end_token_pos = normal_text_buffer_.find(end_token_without_newline);
if (end_token_pos != std::string::npos) {
std::string cleaned_text = normal_text_buffer_;
// Remove the end token
cleaned_text.erase(end_token_pos, end_token_without_newline.length());
normal_text_buffer_.clear();
result.normal_text = cleaned_text;
} else {
// Check if buffer might contain partial end token at the end
int32_t partial_match_len = ends_with_partial_token(
normal_text_buffer_, end_token_without_newline);
if (partial_match_len > 0) {
// Keep potential partial match in buffer, return the rest
result.normal_text = normal_text_buffer_.substr(
0, normal_text_buffer_.length() - partial_match_len);
normal_text_buffer_ = normal_text_buffer_.substr(
normal_text_buffer_.length() - partial_match_len);
} else {
// No partial match, return all buffered text
result.normal_text = normal_text_buffer_;
normal_text_buffer_.clear();
}
}
}
return result;
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,58 @@
/* 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.
==============================================================================*/
#pragma once
#include <regex>
#include <string>
#include <string_view>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
class Qwen25Detector : public BaseFormatDetector {
public:
Qwen25Detector();
virtual ~Qwen25Detector() = default;
private:
std::string normal_text_buffer_;
std::regex tool_call_regex_;
std::string_view trim_whitespace(std::string_view str) const;
std::vector<std::pair<size_t, size_t>> find_tool_call_ranges(
const std::string& text) const;
public:
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
// Streaming incremental parsing for Qwen 2.5/3 tool calls
// parse_streaming_increment
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,613 @@
/* 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 <algorithm>
#include <cctype>
#include <cmath>
#include <limits>
#include <optional>
#include <string_view>
#include <vector>
namespace xllm {
namespace function_call {
namespace {
bool try_parse_int64(const std::string& text, int64_t* out) {
if (out == nullptr) {
return false;
}
try {
size_t idx = 0;
long long value = std::stoll(text, &idx);
if (idx != text.length()) {
return false;
}
*out = static_cast<int64_t>(value);
return true;
} catch (const std::exception&) {
return false;
}
}
bool try_parse_double(const std::string& text, double* out) {
if (out == nullptr) {
return false;
}
try {
size_t idx = 0;
double value = std::stod(text, &idx);
if (idx != text.length()) {
return false;
}
*out = value;
return true;
} catch (const std::exception&) {
return false;
}
}
} // namespace
Qwen3CoderDetector::Qwen3CoderDetector()
: BaseFormatDetector(),
parsed_pos_(0),
current_tool_param_count_(0),
json_started_(false),
is_inside_tool_call_(false) {
tool_call_start_token_ = "<tool_call>";
tool_call_end_token_ = "</tool_call>";
tool_call_prefix_ = "<function=";
function_end_token_ = "</function>";
parameter_prefix_ = "<parameter=";
parameter_end_token_ = "</parameter>";
}
bool Qwen3CoderDetector::starts_with(std::string_view text,
std::string_view prefix) {
return text.length() >= prefix.length() &&
text.substr(0, prefix.length()) == prefix;
}
std::string Qwen3CoderDetector::to_lower_copy(const std::string& input) {
std::string lowered = input;
std::transform(
lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) {
return std::tolower(c);
});
return lowered;
}
std::string Qwen3CoderDetector::trim_ascii_whitespace(std::string_view input) {
const char* whitespace = " \t\n\r\f\v";
size_t start = input.find_first_not_of(whitespace);
if (start == std::string_view::npos) {
return "";
}
size_t end = input.find_last_not_of(whitespace);
return std::string(input.substr(start, end - start + 1));
}
bool Qwen3CoderDetector::has_tool_call(const std::string& text) {
return text.find(tool_call_start_token_) != std::string::npos;
}
nlohmann::json Qwen3CoderDetector::get_arguments_config(
const std::string& func_name,
const std::vector<JsonTool>& tools) const {
for (const auto& tool : tools) {
if (tool.type == "function" && tool.function.name == func_name) {
const auto& params = tool.function.parameters;
if (params.is_object() && params.contains("properties") &&
params["properties"].is_object()) {
return params["properties"];
}
if (params.is_object()) {
return params;
}
return nlohmann::json::object();
}
}
LOG(WARNING) << "Tool '" << func_name
<< "' is not defined in the tools list.";
return nlohmann::json::object();
}
nlohmann::json Qwen3CoderDetector::convert_param_value(
const std::string& param_value,
const std::string& param_name,
const nlohmann::json& param_config,
const std::string& func_name) const {
std::string trimmed = trim_ascii_whitespace(param_value);
std::string lower_value = to_lower_copy(trimmed);
if (lower_value == "null") {
return nullptr;
}
if (!param_config.empty() && !param_config.contains(param_name)) {
LOG(WARNING) << "Parsed parameter '" << param_name
<< "' is not defined in tool '" << func_name
<< "', directly returning string value.";
return param_value;
}
std::string param_type = "string";
if (param_config.contains(param_name) &&
param_config[param_name].is_object() &&
param_config[param_name].contains("type") &&
param_config[param_name]["type"].is_string()) {
param_type =
to_lower_copy(param_config[param_name]["type"].get<std::string>());
}
if (param_type == "string" || param_type == "str" || param_type == "text" ||
param_type == "varchar" || param_type == "char" || param_type == "enum") {
return param_value;
}
if (starts_with(param_type, "int") || starts_with(param_type, "uint") ||
starts_with(param_type, "long") || starts_with(param_type, "short") ||
starts_with(param_type, "unsigned")) {
int64_t int_value = 0;
if (try_parse_int64(trimmed, &int_value)) {
return int_value;
}
LOG(WARNING) << "Parsed value '" << param_value << "' of parameter '"
<< param_name << "' is not an integer in tool '" << func_name
<< "', degrading to string.";
return param_value;
}
if (starts_with(param_type, "num") || starts_with(param_type, "float")) {
double float_value = 0.0;
if (try_parse_double(trimmed, &float_value)) {
bool maybe_convert = trimmed.find('.') == std::string::npos &&
trimmed.find('e') == std::string::npos &&
trimmed.find('E') == std::string::npos;
if (maybe_convert && std::isfinite(float_value)) {
double rounded = std::round(float_value);
if (std::abs(float_value - rounded) <=
std::numeric_limits<double>::epsilon() &&
rounded >=
static_cast<double>(std::numeric_limits<int64_t>::min()) &&
rounded <=
static_cast<double>(std::numeric_limits<int64_t>::max())) {
return static_cast<int64_t>(rounded);
}
}
return float_value;
}
LOG(WARNING) << "Parsed value '" << param_value << "' of parameter '"
<< param_name << "' is not a float in tool '" << func_name
<< "', degrading to string.";
return param_value;
}
if (param_type == "boolean" || param_type == "bool" ||
param_type == "binary") {
if (lower_value != "true" && lower_value != "false") {
LOG(WARNING) << "Parsed value '" << param_value << "' of parameter '"
<< param_name
<< "' is not a boolean (`true` or `false`) in tool '"
<< func_name << "', degrading to false.";
}
return lower_value == "true";
}
if (param_type == "object" || param_type == "array" || param_type == "arr" ||
starts_with(param_type, "dict") || starts_with(param_type, "list")) {
try {
return nlohmann::json::parse(param_value);
} catch (const std::exception&) {
LOG(WARNING) << "Parsed value '" << param_value << "' of parameter '"
<< param_name << "' cannot be parsed by json.loads in tool '"
<< func_name << "', degrading to string.";
return param_value;
}
}
// Best-effort fallback similar to ast.literal_eval behavior.
if (lower_value == "true") {
return true;
}
if (lower_value == "false") {
return false;
}
int64_t int_value = 0;
if (try_parse_int64(trimmed, &int_value)) {
return int_value;
}
double float_value = 0.0;
if (try_parse_double(trimmed, &float_value)) {
return float_value;
}
try {
return nlohmann::json::parse(param_value);
} catch (const std::exception&) {
// Ignore and fallback to string.
}
if (param_value.length() >= 2 && param_value.front() == '\'' &&
param_value.back() == '\'') {
return param_value.substr(1, param_value.length() - 2);
}
LOG(WARNING) << "Parsed value '" << param_value << "' of parameter '"
<< param_name
<< "' cannot be converted with fallback rules in tool '"
<< func_name << "', degrading to string.";
return param_value;
}
void Qwen3CoderDetector::parse_parameters(const std::string& params_text,
const std::string& func_name,
const std::vector<JsonTool>& tools,
nlohmann::json* parsed_params) const {
if (parsed_params == nullptr) {
return;
}
const nlohmann::json param_config = get_arguments_config(func_name, tools);
size_t pos = 0;
while (pos < params_text.length()) {
size_t param_start = params_text.find(parameter_prefix_, pos);
if (param_start == std::string::npos) {
break;
}
size_t name_start = param_start + parameter_prefix_.length();
size_t name_end = params_text.find('>', name_start);
if (name_end == std::string::npos) {
break;
}
size_t value_start = name_end + 1;
size_t cand_end_param = params_text.find(parameter_end_token_, value_start);
size_t cand_next_param = params_text.find(parameter_prefix_, value_start);
size_t cand_end_func = params_text.find(function_end_token_, value_start);
size_t end_pos = std::string::npos;
size_t end_token_len = 0;
if (cand_end_param != std::string::npos) {
end_pos = cand_end_param;
end_token_len = parameter_end_token_.length();
}
if (cand_next_param != std::string::npos && cand_next_param < end_pos) {
end_pos = cand_next_param;
end_token_len = 0;
}
if (cand_end_func != std::string::npos && cand_end_func < end_pos) {
end_pos = cand_end_func;
end_token_len = 0;
}
if (end_pos == std::string::npos) {
break;
}
std::string param_name =
params_text.substr(name_start, name_end - name_start);
std::string raw_value =
params_text.substr(value_start, end_pos - value_start);
if (!raw_value.empty() && raw_value.front() == '\n') {
raw_value.erase(raw_value.begin());
}
if (!raw_value.empty() && raw_value.back() == '\n') {
raw_value.pop_back();
}
(*parsed_params)[param_name] =
convert_param_value(raw_value, param_name, param_config, func_name);
pos = end_pos + end_token_len;
}
}
void Qwen3CoderDetector::parse_tool_call_content(
const std::string& tool_content,
const std::vector<JsonTool>& tools,
int32_t* tool_idx,
std::vector<ToolCallItem>* calls) const {
if (tool_idx == nullptr || calls == nullptr) {
return;
}
size_t pos = 0;
while (pos < tool_content.length()) {
size_t function_start = tool_content.find(tool_call_prefix_, pos);
if (function_start == std::string::npos) {
break;
}
size_t name_start = function_start + tool_call_prefix_.length();
size_t name_end = tool_content.find('>', name_start);
if (name_end == std::string::npos) {
break;
}
std::string func_name =
tool_content.substr(name_start, name_end - name_start);
size_t params_start = name_end + 1;
size_t function_end = tool_content.find(function_end_token_, params_start);
std::string params_text;
if (function_end == std::string::npos) {
params_text = tool_content.substr(params_start);
pos = tool_content.length();
} else {
params_text =
tool_content.substr(params_start, function_end - params_start);
pos = function_end + function_end_token_.length();
}
nlohmann::json parsed_params = nlohmann::json::object();
parse_parameters(params_text, func_name, tools, &parsed_params);
calls->emplace_back(*tool_idx, func_name, parsed_params.dump());
(*tool_idx)++;
if (function_end == std::string::npos) {
break;
}
}
}
StreamingParseResult Qwen3CoderDetector::detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) {
const bool has_tool_call_token =
!tool_call_start_token_.empty() &&
text.find(tool_call_start_token_) != std::string::npos;
const bool has_function_token =
text.find(tool_call_prefix_) != std::string::npos;
if (!has_tool_call_token && !has_function_token) {
return StreamingParseResult(text);
}
std::vector<std::string> raw_tool_calls;
size_t search_pos = 0;
while (search_pos < text.length()) {
size_t block_start = text.find(tool_call_start_token_, search_pos);
if (block_start == std::string::npos) {
break;
}
size_t content_start = block_start + tool_call_start_token_.length();
size_t block_end = text.find(tool_call_end_token_, content_start);
if (block_end == std::string::npos) {
break;
}
raw_tool_calls.emplace_back(
text.substr(content_start, block_end - content_start));
search_pos = block_end + tool_call_end_token_.length();
}
if (raw_tool_calls.empty() && has_function_token) {
raw_tool_calls.emplace_back(text);
}
std::vector<ToolCallItem> calls;
int32_t tool_idx = 0;
for (const auto& tool_content : raw_tool_calls) {
parse_tool_call_content(tool_content, tools, &tool_idx, &calls);
}
size_t start_idx = text.find(tool_call_start_token_);
if (start_idx == std::string::npos) {
start_idx = text.find(tool_call_prefix_);
}
std::string normal_text = (start_idx != std::string::npos && start_idx > 0)
? text.substr(0, start_idx)
: "";
return StreamingParseResult(std::move(normal_text), std::move(calls));
}
StreamingParseResult Qwen3CoderDetector::parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) {
buffer_ += new_text;
if (buffer_.empty()) {
return StreamingParseResult();
}
std::vector<ToolCallItem> calls;
std::string normal_text;
while (true) {
if (parsed_pos_ >= buffer_.length()) {
break;
}
std::string_view current_slice(buffer_.data() + parsed_pos_,
buffer_.length() - parsed_pos_);
if (current_slice.empty()) {
break;
}
if (starts_with(current_slice, tool_call_start_token_)) {
parsed_pos_ += tool_call_start_token_.length();
is_inside_tool_call_ = true;
continue;
}
if (starts_with(current_slice, tool_call_prefix_)) {
size_t end_angle = current_slice.find('>');
if (end_angle == std::string_view::npos) {
break;
}
std::string func_name(current_slice.substr(
tool_call_prefix_.length(), end_angle - tool_call_prefix_.length()));
current_tool_id_ += 1;
current_tool_name_sent_ = true;
current_tool_param_count_ = 0;
json_started_ = false;
current_func_name_ = func_name;
calls.emplace_back(current_tool_id_, func_name, "");
parsed_pos_ += end_angle + 1;
continue;
}
if (starts_with(current_slice, parameter_prefix_)) {
size_t name_end = current_slice.find('>');
if (name_end == std::string_view::npos) {
break;
}
size_t value_start = name_end + 1;
std::string_view rest = current_slice.substr(value_start);
size_t cand_end_param = rest.find(parameter_end_token_);
size_t cand_next_param = rest.find(parameter_prefix_);
size_t cand_end_func = rest.find(function_end_token_);
size_t end_pos = std::string::npos;
size_t end_token_len = 0;
if (cand_end_param != std::string::npos) {
end_pos = cand_end_param;
end_token_len = parameter_end_token_.length();
}
if (cand_next_param != std::string::npos && cand_next_param < end_pos) {
end_pos = cand_next_param;
end_token_len = 0;
}
if (cand_end_func != std::string::npos && cand_end_func < end_pos) {
end_pos = cand_end_func;
end_token_len = 0;
}
if (end_pos == std::string::npos) {
break;
}
std::string param_name(current_slice.substr(
parameter_prefix_.length(), name_end - parameter_prefix_.length()));
std::string raw_value(rest.substr(0, end_pos));
if (!raw_value.empty() && raw_value.front() == '\n') {
raw_value.erase(raw_value.begin());
}
if (!raw_value.empty() && raw_value.back() == '\n') {
raw_value.pop_back();
}
if (!json_started_) {
calls.emplace_back(current_tool_id_, std::nullopt, "{");
json_started_ = true;
}
const std::string func_name = current_func_name_.value_or("");
nlohmann::json param_config = get_arguments_config(func_name, tools);
nlohmann::json converted =
convert_param_value(raw_value, param_name, param_config, func_name);
std::string json_key_val =
nlohmann::json(param_name).dump() + ": " + converted.dump();
std::string fragment =
(current_tool_param_count_ > 0 ? ", " : "") + json_key_val;
calls.emplace_back(current_tool_id_, std::nullopt, fragment);
current_tool_param_count_ += 1;
parsed_pos_ += name_end + 1 + end_pos + end_token_len;
continue;
}
if (starts_with(current_slice, function_end_token_)) {
if (!json_started_) {
calls.emplace_back(current_tool_id_, std::nullopt, "{");
json_started_ = true;
}
calls.emplace_back(current_tool_id_, std::nullopt, "}");
parsed_pos_ += function_end_token_.length();
current_func_name_.reset();
continue;
}
if (starts_with(current_slice, tool_call_end_token_)) {
parsed_pos_ += tool_call_end_token_.length();
is_inside_tool_call_ = false;
continue;
}
size_t next_open_angle = current_slice.find('<');
if (next_open_angle == std::string::npos) {
if (!is_inside_tool_call_) {
normal_text.append(current_slice);
}
parsed_pos_ += current_slice.length();
continue;
}
if (next_open_angle == 0) {
std::vector<std::string_view> possible_tags = {tool_call_start_token_,
tool_call_end_token_,
tool_call_prefix_,
function_end_token_,
parameter_prefix_,
parameter_end_token_};
bool is_potential_tag = false;
for (const auto& tag : possible_tags) {
if (starts_with(tag, current_slice)) {
is_potential_tag = true;
break;
}
}
if (is_potential_tag) {
break;
}
if (!is_inside_tool_call_) {
normal_text.push_back('<');
}
parsed_pos_ += 1;
continue;
}
if (!is_inside_tool_call_) {
normal_text.append(current_slice.substr(0, next_open_angle));
}
parsed_pos_ += next_open_angle;
}
if (parsed_pos_ > 0) {
buffer_ = buffer_.substr(parsed_pos_);
parsed_pos_ = 0;
}
return StreamingParseResult(std::move(normal_text), std::move(calls));
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,82 @@
/* 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.
==============================================================================*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base_format_detector.h"
namespace xllm {
namespace function_call {
class Qwen3CoderDetector : public BaseFormatDetector {
public:
Qwen3CoderDetector();
virtual ~Qwen3CoderDetector() = default;
bool has_tool_call(const std::string& text) override;
StreamingParseResult detect_and_parse(
const std::string& text,
const std::vector<JsonTool>& tools) override;
StreamingParseResult parse_streaming_increment(
const std::string& new_text,
const std::vector<JsonTool>& tools) override;
private:
std::string tool_call_start_token_;
std::string tool_call_end_token_;
std::string tool_call_prefix_;
std::string function_end_token_;
std::string parameter_prefix_;
std::string parameter_end_token_;
size_t parsed_pos_;
int32_t current_tool_param_count_;
bool json_started_;
bool is_inside_tool_call_;
std::optional<std::string> current_func_name_;
static bool starts_with(std::string_view text, std::string_view prefix);
static std::string to_lower_copy(const std::string& input);
static std::string trim_ascii_whitespace(std::string_view input);
nlohmann::json get_arguments_config(const std::string& func_name,
const std::vector<JsonTool>& tools) const;
nlohmann::json convert_param_value(const std::string& param_value,
const std::string& param_name,
const nlohmann::json& param_config,
const std::string& func_name) const;
void parse_parameters(const std::string& params_text,
const std::string& func_name,
const std::vector<JsonTool>& tools,
nlohmann::json* parsed_params) const;
void parse_tool_call_content(const std::string& tool_content,
const std::vector<JsonTool>& tools,
int32_t* tool_idx,
std::vector<ToolCallItem>* calls) const;
};
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,148 @@
/* 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 "utils.h"
#include <glog/logging.h>
#include <algorithm>
#include <nlohmann/json.hpp>
#include <stdexcept>
#include "partial_json_parser/options.h"
#include "partial_json_parser/parser.h"
namespace xllm {
namespace function_call {
std::string find_common_prefix(const std::string& s1, const std::string& s2) {
std::string prefix;
size_t min_length = std::min(s1.length(), s2.length());
for (size_t i = 0; i < min_length; ++i) {
if (s1[i] == s2[i]) {
prefix += s1[i];
} else {
break;
}
}
return prefix;
}
// Convert our Allow enum to partial_json_parser TypeOptions
partial_json_parser::TypeOptions convert_allow_to_type_options(Allow flags) {
int32_t result = 0;
auto check_and_set = [&](Allow allow_flag, int32_t parser_flag) {
if (static_cast<int32_t>(flags) & static_cast<int32_t>(allow_flag)) {
result |= parser_flag;
}
};
check_and_set(Allow::STR, partial_json_parser::STR);
check_and_set(Allow::NUM, partial_json_parser::NUM);
check_and_set(Allow::ARR, partial_json_parser::ARR);
check_and_set(Allow::OBJ, partial_json_parser::OBJ);
check_and_set(Allow::NULL_TYPE, partial_json_parser::NULL_TYPE);
check_and_set(Allow::BOOL, partial_json_parser::BOOL);
check_and_set(Allow::NAN_TYPE, partial_json_parser::NAN_TYPE);
check_and_set(Allow::INFINITY_TYPE, partial_json_parser::INFINITY_TYPE);
check_and_set(Allow::NEG_INFINITY, partial_json_parser::NEG_INFINITY);
return static_cast<partial_json_parser::TypeOptions>(result);
}
std::tuple<nlohmann::json, int32_t> partial_json_loads(
const std::string& input_str,
Allow flags) {
try {
// Convert Allow flags to TypeOptions
auto type_options = convert_allow_to_type_options(flags);
// Use our C++ partial_json_parser
std::string completed_json = partial_json_parser::parse_malformed_string(
input_str, type_options, false);
// Parse the completed JSON
nlohmann::json parsed_obj = nlohmann::json::parse(completed_json);
return std::make_tuple(parsed_obj,
static_cast<int32_t>(input_str.length()));
} catch (const partial_json_parser::MalformedJSONException& e) {
// Handle malformed JSON - try standard JSON parsing for "Extra data" case
try {
nlohmann::json parsed_obj = nlohmann::json::parse(input_str);
return std::make_tuple(parsed_obj,
static_cast<int32_t>(input_str.length()));
} catch (const nlohmann::json::parse_error& json_e) {
// If it contains "Extra data", try to parse just the valid part
std::string error_msg = json_e.what();
if (error_msg.find("Extra data") != std::string::npos) {
// Find the position where valid JSON ends
size_t pos = 0;
int32_t brace_count = 0;
bool in_string = false;
bool escaped = false;
for (size_t i = 0; i < input_str.length(); ++i) {
char c = input_str[i];
if (!in_string) {
if (c == '{') {
brace_count++;
} else if (c == '}') {
brace_count--;
if (brace_count == 0) {
pos = i + 1;
break;
}
} else if (c == '"') {
in_string = true;
}
} else {
if (escaped) {
escaped = false;
} else if (c == '\\') {
escaped = true;
} else if (c == '"') {
in_string = false;
}
}
}
if (pos > 0) {
std::string valid_part = input_str.substr(0, pos);
nlohmann::json parsed_obj = nlohmann::json::parse(valid_part);
return std::make_tuple(parsed_obj, static_cast<int32_t>(pos));
}
}
throw;
}
}
}
bool is_complete_json(const std::string& input_str) {
try {
[[maybe_unused]] auto parsed = nlohmann::json::parse(input_str);
return true;
} catch (const nlohmann::json::parse_error&) {
return false;
}
}
} // namespace function_call
} // namespace xllm

View File

@@ -0,0 +1,69 @@
/* 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.
==============================================================================*/
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <tuple>
#include "core_types.h"
namespace xllm {
namespace function_call {
// Allow flags for partial JSON parsing
enum class Allow : int32_t {
STR = 1 << 0, // 1
NUM = 1 << 1, // 2
ARR = 1 << 2, // 4
OBJ = 1 << 3, // 8
NULL_TYPE = 1 << 4, // 16
BOOL = 1 << 5, // 32
NAN_TYPE = 1 << 6, // 64
INFINITY_TYPE = 1 << 7, // 128
NEG_INFINITY = 1 << 8, // 256
// Composite options
INF = INFINITY_TYPE | NEG_INFINITY,
SPECIAL = NULL_TYPE | BOOL | INF | NAN_TYPE,
ATOM = STR | NUM | SPECIAL,
COLLECTION = ARR | OBJ,
ALL = ATOM | COLLECTION
};
// Bitwise operations for Allow flags
inline Allow operator|(Allow a, Allow b) {
return static_cast<Allow>(static_cast<int32_t>(a) | static_cast<int32_t>(b));
}
inline Allow operator&(Allow a, Allow b) {
return static_cast<Allow>(static_cast<int32_t>(a) & static_cast<int32_t>(b));
}
inline Allow operator~(Allow a) {
return static_cast<Allow>(~static_cast<int32_t>(a));
}
std::string find_common_prefix(const std::string& s1, const std::string& s2);
std::tuple<nlohmann::json, int32_t> partial_json_loads(
const std::string& input_str,
Allow flags);
bool is_complete_json(const std::string& input_str);
} // namespace function_call
} // namespace xllm