1+ /* Copyright 2025 The xLLM Authors. All Rights Reserved.
2+
3+ Licensed under the Apache License, Version 2.0 (the "License");
4+ you may not use this file except in compliance with the License.
5+ You may obtain a copy of the License at
6+
7+ https://github.com/jd-opensource/xllm/blob/main/LICENSE
8+
9+ Unless required by applicable law or agreed to in writing, software
10+ distributed under the License is distributed on an "AS IS" BASIS,
11+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ See the License for the specific language governing permissions and
13+ limitations under the License.
14+ ==============================================================================*/
15+
16+ #include " glm45_detector.h"
17+
18+ #include < algorithm>
19+ #include < iostream>
20+ #include < sstream>
21+
22+ namespace xllm {
23+ namespace function_call {
24+
25+ Glm45Detector::Glm45Detector () : BaseFormatDetector() {
26+ bot_token_ = " <tool_call>" ;
27+ eot_token_ = " </tool_call>" ;
28+
29+ // Regex patterns for GLM-4.5 format
30+ func_call_regex_ = std::regex (" <tool_call>[\\ s\\ S]*?</tool_call>" ,
31+ std::regex_constants::ECMAScript);
32+ func_detail_regex_ =
33+ std::regex (" <tool_call>([^\\ n]*)\\ n([\\ s\\ S]*?)</tool_call>" ,
34+ std::regex_constants::ECMAScript);
35+ func_arg_regex_ = std::regex (
36+ " <arg_key>([\\ s\\ S]*?)</arg_key>\\ s*<arg_value>([\\ s\\ S]*?)</arg_value>" ,
37+ std::regex_constants::ECMAScript);
38+ }
39+
40+ std::string Glm45Detector::trim_whitespace (std::string_view str) const {
41+ const char * whitespace = " \t\n\r " ;
42+
43+ size_t start = str.find_first_not_of (whitespace);
44+ if (start == std::string_view::npos) {
45+ return std::string{};
46+ }
47+
48+ size_t end = str.find_last_not_of (whitespace);
49+
50+ return std::string (str.substr (start, end - start + 1 ));
51+ }
52+
53+ bool Glm45Detector::has_tool_call (const std::string& text) {
54+ return text.find (bot_token_) != std::string::npos;
55+ }
56+
57+ StreamingParseResult Glm45Detector::detect_and_parse (
58+ const std::string& text,
59+ const std::vector<JsonTool>& tools) {
60+ size_t idx = text.find (bot_token_);
61+ std::string normal_text =
62+ (idx != std::string::npos) ? text.substr (0 , idx) : text;
63+
64+ // Trim normal text
65+ if (!normal_text.empty ()) {
66+ normal_text = trim_whitespace (normal_text);
67+ }
68+
69+ if (idx == std::string::npos) {
70+ return StreamingParseResult (normal_text, {});
71+ }
72+
73+ std::vector<ToolCallItem> calls;
74+
75+ try {
76+ std::sregex_iterator iter (text.begin (), text.end (), func_call_regex_);
77+ std::sregex_iterator end;
78+
79+ for (; iter != end; ++iter) {
80+ std::smatch match = *iter;
81+ std::string match_result = match.str ();
82+
83+ // Parse function name and arguments
84+ std::smatch func_detail;
85+ if (std::regex_search (match_result, func_detail, func_detail_regex_)) {
86+ std::string func_name = func_detail[1 ].str ();
87+ std::string func_args = func_detail[2 ].str ();
88+
89+ // Parse arguments using regex
90+ std::unordered_map<std::string, nlohmann::json> arguments;
91+ std::sregex_iterator arg_iter (
92+ func_args.begin (), func_args.end (), func_arg_regex_);
93+ std::sregex_iterator arg_end;
94+
95+ for (; arg_iter != arg_end; ++arg_iter) {
96+ std::smatch arg_match = *arg_iter;
97+ if (arg_match.size () >= 3 ) {
98+ std::string arg_key = arg_match[1 ].str ();
99+ std::string arg_value = arg_match[2 ].str ();
100+
101+ arg_key = trim_whitespace (arg_key);
102+
103+ arg_value = trim_whitespace (arg_value);
104+
105+ try {
106+ nlohmann::json parsed_value = nlohmann::json::parse (arg_value);
107+ arguments[arg_key] = parsed_value;
108+ } catch (const nlohmann::json::parse_error&) {
109+ arguments[arg_key] = nlohmann::json (arg_value);
110+ }
111+ }
112+ }
113+
114+ // Create JSON object for parse_base_json
115+ nlohmann::json match_json;
116+ match_json[" name" ] = func_name;
117+ match_json[" parameters" ] = arguments;
118+
119+ auto parsed_calls = parse_base_json (match_json, tools);
120+ calls.insert (calls.end (), parsed_calls.begin (), parsed_calls.end ());
121+ }
122+ }
123+
124+ return StreamingParseResult (normal_text, calls);
125+
126+ } catch (const std::exception& e) {
127+ LOG (ERROR) << " Error in GLM-4.5 detect_and_parse: " << e.what ();
128+ return StreamingParseResult (text, {});
129+ }
130+ }
131+
132+ StreamingParseResult Glm45Detector::parse_streaming_increment (
133+ const std::string& new_text,
134+ const std::vector<JsonTool>& tools) {
135+ buffer_ += new_text;
136+ std::string current_text = buffer_;
137+
138+ size_t start = current_text.find (bot_token_);
139+ if (start == std::string::npos) {
140+ buffer_.clear ();
141+ if (current_tool_id_ > 0 ) {
142+ current_text = " " ;
143+ }
144+ return StreamingParseResult (current_text, {});
145+ }
146+
147+ // Look for complete tool call
148+ size_t end = current_text.find (eot_token_);
149+ if (end != std::string::npos) {
150+ // Initialize state if this is the first tool call
151+ if (current_tool_id_ == -1 ) {
152+ current_tool_id_ = 0 ;
153+ prev_tool_call_arr_.clear ();
154+ streamed_args_for_tool_.clear ();
155+ streamed_args_for_tool_.push_back (" " );
156+ }
157+
158+ // Ensure we have enough entries in tracking arrays
159+ while (prev_tool_call_arr_.size () <= current_tool_id_) {
160+ prev_tool_call_arr_.push_back ({});
161+ }
162+ while (streamed_args_for_tool_.size () <= current_tool_id_) {
163+ streamed_args_for_tool_.push_back (" " );
164+ }
165+
166+ // Parse the complete tool call
167+ std::string complete_call =
168+ current_text.substr (0 , end + eot_token_.length ());
169+ StreamingParseResult result = detect_and_parse (complete_call, tools);
170+
171+ if (!result.calls .empty ()) {
172+ // Store tool call info for serving layer
173+ prev_tool_call_arr_[current_tool_id_][" name" ] =
174+ result.calls [0 ].name .value_or (" " );
175+ prev_tool_call_arr_[current_tool_id_][" arguments" ] =
176+ result.calls [0 ].parameters ;
177+ streamed_args_for_tool_[current_tool_id_] = result.calls [0 ].parameters ;
178+
179+ // Update tool index
180+ result.calls [0 ].tool_index = current_tool_id_;
181+ current_tool_id_++;
182+ }
183+
184+ // Update buffer with remaining text
185+ buffer_ = current_text.substr (end + eot_token_.length ());
186+ return result;
187+ }
188+
189+ // Return normal text before tool call start
190+ std::string normal_text = current_text.substr (0 , start);
191+ buffer_ = current_text.substr (start);
192+ return StreamingParseResult (normal_text, {});
193+ }
194+
195+ } // namespace function_call
196+ } // namespace xllm
0 commit comments