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:
38
upstream_ref/xllm/xllm/function_call/CMakeLists.txt
Normal file
38
upstream_ref/xllm/xllm/function_call/CMakeLists.txt
Normal 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
|
||||
)
|
||||
351
upstream_ref/xllm/xllm/function_call/base_format_detector.cpp
Normal file
351
upstream_ref/xllm/xllm/function_call/base_format_detector.cpp
Normal 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
|
||||
79
upstream_ref/xllm/xllm/function_call/base_format_detector.h
Normal file
79
upstream_ref/xllm/xllm/function_call/base_format_detector.h
Normal 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
|
||||
84
upstream_ref/xllm/xllm/function_call/core_types.h
Normal file
84
upstream_ref/xllm/xllm/function_call/core_types.h
Normal 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
|
||||
1161
upstream_ref/xllm/xllm/function_call/deepseekv32_detector.cpp
Normal file
1161
upstream_ref/xllm/xllm/function_call/deepseekv32_detector.cpp
Normal file
File diff suppressed because it is too large
Load Diff
72
upstream_ref/xllm/xllm/function_call/deepseekv32_detector.h
Normal file
72
upstream_ref/xllm/xllm/function_call/deepseekv32_detector.h
Normal 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 =
|
||||
// "</|DSML|invoke>" 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
|
||||
317
upstream_ref/xllm/xllm/function_call/deepseekv3_detector.cpp
Normal file
317
upstream_ref/xllm/xllm/function_call/deepseekv3_detector.cpp
Normal 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
|
||||
53
upstream_ref/xllm/xllm/function_call/deepseekv3_detector.h
Normal file
53
upstream_ref/xllm/xllm/function_call/deepseekv3_detector.h
Normal 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
|
||||
42
upstream_ref/xllm/xllm/function_call/function_call.h
Normal file
42
upstream_ref/xllm/xllm/function_call/function_call.h
Normal 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
|
||||
212
upstream_ref/xllm/xllm/function_call/function_call_parser.cpp
Executable file
212
upstream_ref/xllm/xllm/function_call/function_call_parser.cpp
Executable 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
|
||||
84
upstream_ref/xllm/xllm/function_call/function_call_parser.h
Normal file
84
upstream_ref/xllm/xllm/function_call/function_call_parser.h
Normal 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
|
||||
198
upstream_ref/xllm/xllm/function_call/glm45_detector.cpp
Normal file
198
upstream_ref/xllm/xllm/function_call/glm45_detector.cpp
Normal 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
|
||||
75
upstream_ref/xllm/xllm/function_call/glm45_detector.h
Normal file
75
upstream_ref/xllm/xllm/function_call/glm45_detector.h
Normal 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
|
||||
771
upstream_ref/xllm/xllm/function_call/glm47_detector.cpp
Normal file
771
upstream_ref/xllm/xllm/function_call/glm47_detector.cpp
Normal 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
|
||||
123
upstream_ref/xllm/xllm/function_call/glm47_detector.h
Normal file
123
upstream_ref/xllm/xllm/function_call/glm47_detector.h
Normal 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
|
||||
293
upstream_ref/xllm/xllm/function_call/kimik2_detector.cpp
Normal file
293
upstream_ref/xllm/xllm/function_call/kimik2_detector.cpp
Normal 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
|
||||
74
upstream_ref/xllm/xllm/function_call/kimik2_detector.h
Normal file
74
upstream_ref/xllm/xllm/function_call/kimik2_detector.h
Normal 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
|
||||
@@ -0,0 +1,12 @@
|
||||
include(cc_library)
|
||||
|
||||
cc_library(
|
||||
NAME
|
||||
partial_json_parser
|
||||
SRCS
|
||||
src/parser.cpp
|
||||
INCLUDES
|
||||
include
|
||||
DEPS
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
176
upstream_ref/xllm/xllm/function_call/qwen25_detector.cpp
Normal file
176
upstream_ref/xllm/xllm/function_call/qwen25_detector.cpp
Normal 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
|
||||
58
upstream_ref/xllm/xllm/function_call/qwen25_detector.h
Normal file
58
upstream_ref/xllm/xllm/function_call/qwen25_detector.h
Normal 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
|
||||
613
upstream_ref/xllm/xllm/function_call/qwen3_coder_detector.cpp
Normal file
613
upstream_ref/xllm/xllm/function_call/qwen3_coder_detector.cpp
Normal 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
|
||||
82
upstream_ref/xllm/xllm/function_call/qwen3_coder_detector.h
Normal file
82
upstream_ref/xllm/xllm/function_call/qwen3_coder_detector.h
Normal 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
|
||||
148
upstream_ref/xllm/xllm/function_call/utils.cpp
Normal file
148
upstream_ref/xllm/xllm/function_call/utils.cpp
Normal 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
|
||||
69
upstream_ref/xllm/xllm/function_call/utils.h
Normal file
69
upstream_ref/xllm/xllm/function_call/utils.h
Normal 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
|
||||
Reference in New Issue
Block a user