1use async_trait::async_trait;
4use openai_protocol::common::Tool;
5use serde_json::{json, Map, Value};
6
7use crate::{
8 errors::{ParserError, ParserResult},
9 traits::ToolParser,
10 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
11};
12
13mod streaming;
14
15const LIMIT: usize = 4 * 1024 * 1024;
16const START: &str = "<tool_calls";
17
18#[derive(Default)]
19pub struct HyV4Parser {
20 buffer: String,
21 suffix: Option<String>,
22 index: usize,
23 opening: String,
24 stream: streaming::StreamParser,
25}
26
27impl HyV4Parser {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 fn call(body: &str, suffix: &str, tools: &[Tool]) -> Option<ToolCall> {
33 let key_start = format!("<arg_key{suffix}>");
34 let key_end = format!("</arg_key{suffix}>");
35 let val_start = format!("<arg_value{suffix}>");
36 let val_end = format!("</arg_value{suffix}>");
37 let (name, mut rest) = body
38 .find(&key_start)
39 .map_or_else(|| (body.trim(), ""), |i| (body[..i].trim(), &body[i..]));
40 if name.is_empty() || name.contains(['<', '>']) {
41 return None;
42 }
43 let props = tools
44 .iter()
45 .find(|t| t.function.name == name)
46 .and_then(|t| t.function.parameters.get("properties"));
47 let mut args = Map::new();
48 while !rest.trim().is_empty() {
49 rest = rest.trim_start().strip_prefix(&key_start)?;
50 let (key, tail) = rest.split_once(&key_end)?;
51 let key = key.trim();
52 if key.is_empty() || args.contains_key(key) {
53 return None;
54 }
55 rest = tail.trim_start().strip_prefix(&val_start)?;
56 let (value, tail) = rest.split_once(&val_end)?;
57 args.insert(
58 key.to_owned(),
59 coerce(value, props.and_then(|p| p.get(key))),
60 );
61 rest = tail;
62 }
63 Some(ToolCall {
64 function: FunctionCall {
65 name: name.to_owned(),
66 arguments: Value::Object(args).to_string(),
67 },
68 })
69 }
70
71 fn drain(&mut self, tools: &[Tool]) -> StreamingParseResult {
72 let mut out = StreamingParseResult::default();
73 loop {
74 let Some(suffix) = self.suffix.clone() else {
75 if let Some(i) = self.buffer.find(START) {
76 out.normal_text.push_str(&self.buffer[..i]);
77 self.buffer.drain(..i);
78 let Some(end) = self.buffer.find('>') else {
79 break;
80 };
81 let suffix = &self.buffer[START.len()..end];
82 if !suffix.is_empty()
83 && (!suffix.starts_with(':')
84 || suffix.len() == 1
85 || suffix.chars().any(|c| c.is_whitespace() || c == '<'))
86 {
87 out.normal_text.push('<');
88 self.buffer.drain(..1);
89 continue;
90 }
91 self.suffix = Some(suffix.to_owned());
92 self.opening = self.buffer[..end + 1].to_owned();
93 self.buffer.drain(..end + 1);
94 continue;
95 }
96 let keep = (1..START.len())
97 .rev()
98 .find(|&n| self.buffer.ends_with(&START[..n]))
99 .unwrap_or(0);
100 let end = self.buffer.len() - keep;
101 out.normal_text.push_str(&self.buffer[..end]);
102 self.buffer.drain(..end);
103 break;
104 };
105 let close = format!("</tool_calls{suffix}>");
106 let begin = format!("<tool_call{suffix}>");
107 let end = format!("</tool_call{suffix}>");
108 let leading = self.buffer.len() - self.buffer.trim_start().len();
109 if self.buffer[leading..].starts_with(&close) {
110 self.buffer.drain(..leading + close.len());
111 self.opening.clear();
112 self.suffix = None;
113 continue;
114 }
115 if !self.buffer[leading..].starts_with(&begin) {
116 if begin.starts_with(&self.buffer[leading..])
117 || close.starts_with(&self.buffer[leading..])
118 {
119 break;
120 }
121 out.normal_text.push_str(&std::mem::take(&mut self.opening));
122 out.normal_text.push_str(&self.buffer);
123 self.buffer.clear();
124 self.suffix = None;
125 break;
126 }
127 let body_start = leading + begin.len();
128 let value_begin = format!("<arg_value{suffix}>");
130 let value_end = format!("</arg_value{suffix}>");
131 let mut cursor = body_start;
132 let end_index = loop {
133 let Some(rel) = self.buffer[cursor..].find(&end) else {
134 break None;
135 };
136 let candidate = cursor + rel;
137 if let Some(v) = self.buffer[cursor..candidate].find(&value_begin) {
138 let value_pos = cursor + v + value_begin.len();
139 let Some(vend) = self.buffer[value_pos..].find(&value_end) else {
140 break None;
141 };
142 cursor = value_pos + vend + value_end.len();
143 } else {
144 break Some(candidate);
145 }
146 };
147 let Some(i) = end_index else {
148 break;
149 };
150 if let Some(call) = Self::call(&self.buffer[body_start..i], &suffix, tools) {
151 self.opening.clear();
152 out.calls.push(ToolCallItem {
153 tool_index: self.index,
154 name: Some(call.function.name),
155 parameters: call.function.arguments,
156 });
157 self.index += 1;
158 } else {
159 out.normal_text.push_str(&std::mem::take(&mut self.opening));
160 out.normal_text.push_str(&self.buffer[..i + end.len()]);
161 self.suffix = None;
162 }
163 self.buffer.drain(..i + end.len());
164 }
165 out
166 }
167
168 pub fn build_structural_tag(tools: &[Tool], at_least_one: bool) -> Value {
174 let calls: Vec<Value> = tools
175 .iter()
176 .filter(|tool| !tool.function.name.is_empty())
177 .map(|tool| {
178 json!({
179 "type": "tag",
180 "begin": format!("<tool_call>{}", tool.function.name),
181 "content": {
182 "type": "json_schema",
183 "json_schema": tool.function.parameters,
184 "style": "glm_xml",
185 },
186 "end": "</tool_call>",
187 })
188 })
189 .collect();
190 let content = if calls.is_empty() {
193 json!({ "type": "const_string", "value": "" })
194 } else {
195 json!({ "type": "plus", "content": { "type": "or", "elements": calls } })
196 };
197
198 json!({
199 "format": {
200 "type": "triggered_tags",
201 "triggers": ["<tool_calls>"],
202 "tags": [{
203 "begin": "<tool_calls>",
204 "content": content,
205 "end": "</tool_calls>",
206 }],
207 "at_least_one": at_least_one,
208 }
209 })
210 }
211
212 pub fn reasoning_prefix() -> Value {
215 json!({
216 "type": "tag",
217 "begin": "",
218 "content": {
219 "type": "any_text",
220 "excludes": [
221 "<think", "</think",
222 "<tool_call", "</tool_call",
223 "<arg_key", "</arg_key", "<arg_value", "</arg_value",
224 ],
225 },
226 "end": "</think>",
227 })
228 }
229}
230
231fn types(schema: Option<&Value>) -> Vec<&str> {
232 let Some(s) = schema else {
233 return vec!["string"];
234 };
235 if let Some(t) = s.get("type") {
236 if let Some(t) = t.as_str() {
237 return vec![t];
238 }
239 if let Some(ts) = t.as_array() {
240 return ts.iter().filter_map(Value::as_str).collect();
241 }
242 }
243 if let Some(v) = s
244 .get("anyOf")
245 .or_else(|| s.get("oneOf"))
246 .and_then(Value::as_array)
247 {
248 return v.iter().flat_map(|s| types(Some(s))).collect();
249 }
250 vec!["string"]
251}
252
253fn coerce(raw: &str, schema: Option<&Value>) -> Value {
254 let ts = types(schema);
255 if ts.contains(&"boolean") {
256 match raw.trim().to_ascii_lowercase().as_str() {
257 "true" => return Value::Bool(true),
258 "false" => return Value::Bool(false),
259 _ => {}
260 }
261 }
262 if let Ok(v) = serde_json::from_str::<Value>(raw) {
263 let matches = ts.iter().any(|t| match *t {
264 "integer" | "int" => v.is_i64() || v.is_u64(),
265 "number" | "float" | "double" => v.is_number(),
266 "object" | "dict" | "map" => v.is_object(),
267 "array" | "list" => v.is_array(),
268 "null" => v.is_null(),
269 _ => false,
270 });
271 if matches {
272 return v;
273 }
274 }
275 Value::String(raw.to_owned())
277}
278
279#[async_trait]
280impl ToolParser for HyV4Parser {
281 async fn parse_complete(&self, output: &str) -> ParserResult<(String, Vec<ToolCall>)> {
282 self.parse_complete_with_tools(output, &[]).await
283 }
284 async fn parse_complete_with_tools(
285 &self,
286 output: &str,
287 tools: &[Tool],
288 ) -> ParserResult<(String, Vec<ToolCall>)> {
289 let mut p = Self::new();
290 if output.len() > LIMIT {
291 return Err(ParserError::ParsingFailed(
292 "Hy4 tool buffer exceeds 4 MiB".into(),
293 ));
294 }
295 p.buffer.push_str(output);
296 let mut result = p.drain(tools);
297 result
298 .normal_text
299 .push_str(&format!("{}{}", p.opening, p.buffer));
300 Ok((
301 result.normal_text,
302 result
303 .calls
304 .into_iter()
305 .map(|c| ToolCall {
306 function: FunctionCall {
307 name: c.name.unwrap_or_default(),
308 arguments: c.parameters,
309 },
310 })
311 .collect(),
312 ))
313 }
314 async fn parse_incremental(
315 &mut self,
316 chunk: &str,
317 tools: &[Tool],
318 ) -> ParserResult<StreamingParseResult> {
319 self.stream.parse(chunk, tools)
320 }
321
322 fn has_tool_markers(&self, text: &str) -> bool {
323 text.contains(START)
324 }
325 fn take_unstreamed_normal_text(&mut self) -> String {
326 self.stream.flush_text()
327 }
328
329 fn reset(&mut self) {
330 *self = Self::new();
331 }
332}