Skip to main content

llama_cpp_bindings/tool_call_format/
mod.rs

1pub mod bracketed_args;
2pub mod json_object;
3pub mod key_value_xml_tags;
4pub mod paired_quote_args;
5pub mod tool_call_format_outcome;
6pub mod xml_function_tags;
7
8pub use self::tool_call_format_outcome::ToolCallFormatOutcome;
9
10use llama_cpp_bindings_types::ToolCallArgsShape;
11use llama_cpp_bindings_types::ToolCallMarkers;
12
13use crate::error::ToolCallFormatFailure;
14
15#[must_use]
16pub fn try_parse(body: &str, markers: &ToolCallMarkers) -> ToolCallFormatOutcome {
17    if markers.open.is_empty() {
18        return ToolCallFormatOutcome::NoMatch;
19    }
20
21    let parsed: Result<Vec<_>, ToolCallFormatFailure> = match &markers.args_shape {
22        ToolCallArgsShape::BracketedJson(shape) => {
23            bracketed_args::parse(body, markers, shape).map_err(Into::into)
24        }
25        ToolCallArgsShape::JsonObject(shape) => json_object::parse(body, shape).map_err(Into::into),
26        ToolCallArgsShape::KeyValueXmlTags(shape) => {
27            key_value_xml_tags::parse(body, markers, shape).map_err(Into::into)
28        }
29        ToolCallArgsShape::PairedQuote(shape) => {
30            paired_quote_args::parse(body, markers, shape).map_err(Into::into)
31        }
32        ToolCallArgsShape::XmlTags(shape) => {
33            xml_function_tags::parse(body, shape).map_err(Into::into)
34        }
35    };
36
37    match parsed {
38        Ok(parsed) if parsed.is_empty() => ToolCallFormatOutcome::NoMatch,
39        Ok(parsed) => ToolCallFormatOutcome::Parsed(parsed),
40        Err(failure) => ToolCallFormatOutcome::Failed(failure),
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use llama_cpp_bindings_types::BracketedJsonShape;
47    use llama_cpp_bindings_types::KeyValueXmlTagsShape;
48    use llama_cpp_bindings_types::PairedQuoteShape;
49    use llama_cpp_bindings_types::ToolCallArgsShape;
50    use llama_cpp_bindings_types::ToolCallArguments;
51    use llama_cpp_bindings_types::ToolCallMarkers;
52    use llama_cpp_bindings_types::ToolCallValueQuote;
53    use llama_cpp_bindings_types::XmlTagsShape;
54    use serde_json::json;
55
56    use super::ToolCallFormatOutcome;
57    use super::try_parse;
58
59    fn mistral3_markers() -> ToolCallMarkers {
60        ToolCallMarkers {
61            open: "[TOOL_CALLS]".to_owned(),
62            close: String::new(),
63            args_shape: ToolCallArgsShape::BracketedJson(BracketedJsonShape {
64                name_args_separator: "[ARGS]".to_owned(),
65            }),
66        }
67    }
68
69    fn gemma4_markers() -> ToolCallMarkers {
70        ToolCallMarkers {
71            open: "<|tool_call>call:".to_owned(),
72            close: "}".to_owned(),
73            args_shape: ToolCallArgsShape::PairedQuote(PairedQuoteShape {
74                name_args_separator: "{".to_owned(),
75                value_quote: ToolCallValueQuote {
76                    open: "<|\"|>".to_owned(),
77                    close: "<|\"|>".to_owned(),
78                },
79            }),
80        }
81    }
82
83    fn qwen35_markers() -> ToolCallMarkers {
84        ToolCallMarkers {
85            open: "<tool_call>".to_owned(),
86            close: "</tool_call>".to_owned(),
87            args_shape: ToolCallArgsShape::XmlTags(XmlTagsShape {
88                function_open_prefix: "<function=".to_owned(),
89                function_close: "</function>".to_owned(),
90                parameter_open_prefix: "<parameter=".to_owned(),
91                parameter_close: "</parameter>".to_owned(),
92            }),
93        }
94    }
95
96    fn glm47_markers() -> ToolCallMarkers {
97        ToolCallMarkers {
98            open: "<tool_call>".to_owned(),
99            close: "</tool_call>".to_owned(),
100            args_shape: ToolCallArgsShape::KeyValueXmlTags(KeyValueXmlTagsShape {
101                key_open: "<arg_key>".to_owned(),
102                key_close: "</arg_key>".to_owned(),
103                value_open: "<arg_value>".to_owned(),
104                value_close: "</arg_value>".to_owned(),
105            }),
106        }
107    }
108
109    #[test]
110    fn dispatches_to_bracketed_args_for_mistral3_shape() {
111        let outcome = try_parse(
112            "[TOOL_CALLS]get_weather[ARGS]{\"location\":\"Paris\"}",
113            &mistral3_markers(),
114        );
115
116        match outcome {
117            ToolCallFormatOutcome::Parsed(calls) => {
118                assert_eq!(calls.len(), 1);
119                assert_eq!(calls[0].name, "get_weather");
120                assert_eq!(
121                    calls[0].arguments,
122                    ToolCallArguments::ValidJson(json!({"location": "Paris"})),
123                );
124            }
125            other => panic!("expected Parsed, got {other:?}"),
126        }
127    }
128
129    #[test]
130    fn dispatches_to_paired_quote_args_for_gemma4_shape() {
131        let outcome = try_parse(
132            "<|tool_call>call:get_weather{location:<|\"|>Paris<|\"|>}",
133            &gemma4_markers(),
134        );
135
136        match outcome {
137            ToolCallFormatOutcome::Parsed(calls) => {
138                assert_eq!(calls.len(), 1);
139                assert_eq!(calls[0].name, "get_weather");
140                assert_eq!(
141                    calls[0].arguments,
142                    ToolCallArguments::ValidJson(json!({"location": "Paris"})),
143                );
144            }
145            other => panic!("expected Parsed, got {other:?}"),
146        }
147    }
148
149    #[test]
150    fn dispatches_to_key_value_xml_tags_for_glm47_shape() {
151        let outcome = try_parse(
152            "<tool_call>get_weather<arg_key>location</arg_key><arg_value>Paris</arg_value></tool_call>",
153            &glm47_markers(),
154        );
155
156        match outcome {
157            ToolCallFormatOutcome::Parsed(calls) => {
158                assert_eq!(calls.len(), 1);
159                assert_eq!(calls[0].name, "get_weather");
160                assert_eq!(
161                    calls[0].arguments,
162                    ToolCallArguments::ValidJson(json!({"location": "Paris"})),
163                );
164            }
165            other => panic!("expected Parsed, got {other:?}"),
166        }
167    }
168
169    #[test]
170    fn dispatches_to_xml_function_tags_for_qwen35_shape() {
171        let outcome = try_parse(
172            "<function=get_weather><parameter=location>Paris</parameter></function>",
173            &qwen35_markers(),
174        );
175
176        match outcome {
177            ToolCallFormatOutcome::Parsed(calls) => {
178                assert_eq!(calls.len(), 1);
179                assert_eq!(calls[0].name, "get_weather");
180                assert_eq!(
181                    calls[0].arguments,
182                    ToolCallArguments::ValidJson(json!({"location": "Paris"})),
183                );
184            }
185            other => panic!("expected Parsed, got {other:?}"),
186        }
187    }
188
189    #[test]
190    fn no_match_when_open_marker_is_empty() {
191        let markers = ToolCallMarkers {
192            open: String::new(),
193            close: String::new(),
194            args_shape: ToolCallArgsShape::BracketedJson(BracketedJsonShape {
195                name_args_separator: "[ARGS]".to_owned(),
196            }),
197        };
198
199        match try_parse("[TOOL_CALLS]get_weather[ARGS]{}", &markers) {
200            ToolCallFormatOutcome::NoMatch => {}
201            other => panic!("expected NoMatch, got {other:?}"),
202        }
203    }
204
205    #[test]
206    fn no_match_when_body_lacks_markers() {
207        match try_parse("plain text without tool calls", &mistral3_markers()) {
208            ToolCallFormatOutcome::NoMatch => {}
209            other => panic!("expected NoMatch, got {other:?}"),
210        }
211    }
212
213    #[test]
214    fn failed_when_inner_parser_returns_typed_failure() {
215        match try_parse(
216            "[TOOL_CALLS]get_weather[ARGS]{\"location\":}",
217            &mistral3_markers(),
218        ) {
219            ToolCallFormatOutcome::Failed(_) => {}
220            other => panic!("expected Failed, got {other:?}"),
221        }
222    }
223
224    #[test]
225    fn try_parse_returns_no_match_for_glm_input_under_qwen_markers() {
226        let glm_input = "<tool_call>get_weather\
227            <arg_key>location</arg_key>\
228            <arg_value>Paris</arg_value>\
229            </tool_call>";
230
231        match try_parse(glm_input, &qwen35_markers()) {
232            ToolCallFormatOutcome::NoMatch => {}
233            other => panic!("expected NoMatch for GLM input under Qwen markers, got {other:?}"),
234        }
235    }
236
237    #[test]
238    fn try_parse_returns_no_match_for_plain_content_under_every_known_shape() {
239        use crate::tool_call_template_overrides::known_marker_candidates;
240
241        let plain_content = "Sorry, I cannot help with that request.";
242
243        for candidate in known_marker_candidates() {
244            match try_parse(plain_content, &candidate) {
245                ToolCallFormatOutcome::NoMatch => {}
246                other => panic!(
247                    "expected NoMatch for plain content under candidate {candidate:?}, got {other:?}"
248                ),
249            }
250        }
251    }
252
253    #[test]
254    fn duck_type_resolves_qwen_xml_input_via_xml_tags_shape_first() {
255        use llama_cpp_bindings_types::ToolCallArguments;
256
257        use crate::tool_call_template_overrides::known_marker_candidates;
258
259        let qwen_input = "<tool_call>\n\
260            <function=get_weather>\n\
261            <parameter=location>\n\
262            Paris\n\
263            </parameter>\n\
264            </function>\n\
265            </tool_call>";
266
267        let mut resolved = None;
268        for candidate in known_marker_candidates() {
269            if let ToolCallFormatOutcome::Parsed(calls) = try_parse(qwen_input, &candidate) {
270                resolved = Some((candidate.args_shape, calls));
271                break;
272            }
273        }
274
275        let (args_shape, calls) =
276            resolved.expect("Qwen XML input must resolve via at least one duck-type candidate");
277        assert!(
278            matches!(args_shape, ToolCallArgsShape::XmlTags(_)),
279            "duck-type ordering must resolve Qwen XML via the XmlTags shape (most restrictive \
280             shape that requires `<function=`), got {args_shape:?}"
281        );
282        assert_eq!(calls.len(), 1);
283        assert_eq!(calls[0].name, "get_weather");
284        assert_eq!(
285            calls[0].arguments,
286            ToolCallArguments::ValidJson(json!({"location": "Paris"})),
287        );
288    }
289
290    #[test]
291    fn duck_type_resolves_glm_input_via_key_value_xml_tags_shape() {
292        use llama_cpp_bindings_types::ToolCallArguments;
293
294        use crate::tool_call_template_overrides::known_marker_candidates;
295
296        let glm_input = "<tool_call>get_weather\
297            <arg_key>location</arg_key>\
298            <arg_value>Paris</arg_value>\
299            </tool_call>";
300
301        let mut resolved = None;
302        for candidate in known_marker_candidates() {
303            if let ToolCallFormatOutcome::Parsed(calls) = try_parse(glm_input, &candidate) {
304                resolved = Some((candidate.args_shape, calls));
305                break;
306            }
307        }
308
309        let (args_shape, calls) =
310            resolved.expect("GLM input must resolve via at least one duck-type candidate");
311        assert!(
312            matches!(args_shape, ToolCallArgsShape::KeyValueXmlTags(_)),
313            "GLM input must resolve via the KeyValueXmlTags shape, got {args_shape:?}"
314        );
315        assert_eq!(calls.len(), 1);
316        assert_eq!(calls[0].name, "get_weather");
317        assert_eq!(
318            calls[0].arguments,
319            ToolCallArguments::ValidJson(json!({"location": "Paris"})),
320        );
321    }
322
323    #[test]
324    fn duck_type_resolves_mistral_input_via_bracketed_json_shape() {
325        use llama_cpp_bindings_types::ToolCallArguments;
326
327        use crate::tool_call_template_overrides::known_marker_candidates;
328
329        let mistral_input = r#"[TOOL_CALLS]get_weather[ARGS]{"location":"Paris"}"#;
330
331        let mut resolved = None;
332        for candidate in known_marker_candidates() {
333            if let ToolCallFormatOutcome::Parsed(calls) = try_parse(mistral_input, &candidate) {
334                resolved = Some((candidate.args_shape, calls));
335                break;
336            }
337        }
338
339        let (args_shape, calls) =
340            resolved.expect("Mistral input must resolve via at least one duck-type candidate");
341        assert!(
342            matches!(args_shape, ToolCallArgsShape::BracketedJson(_)),
343            "Mistral input must resolve via the BracketedJson shape; the candidate ordering must \
344             try BracketedJson before PairedQuote because PairedQuote's `{{` separator could \
345             greedily match Mistral's JSON args. Got {args_shape:?}"
346        );
347        assert_eq!(calls.len(), 1);
348        assert_eq!(calls[0].name, "get_weather");
349        assert_eq!(
350            calls[0].arguments,
351            ToolCallArguments::ValidJson(json!({"location": "Paris"})),
352        );
353    }
354
355    #[test]
356    fn duck_type_resolves_gemma_input_via_paired_quote_shape() {
357        use llama_cpp_bindings_types::ToolCallArguments;
358
359        use crate::tool_call_template_overrides::known_marker_candidates;
360
361        let gemma_input = "<|tool_call>call:get_weather{location:<|\"|>Paris<|\"|>}";
362
363        let mut resolved = None;
364        for candidate in known_marker_candidates() {
365            if let ToolCallFormatOutcome::Parsed(calls) = try_parse(gemma_input, &candidate) {
366                resolved = Some((candidate.args_shape, calls));
367                break;
368            }
369        }
370
371        let (args_shape, calls) =
372            resolved.expect("Gemma input must resolve via at least one duck-type candidate");
373        assert!(
374            matches!(args_shape, ToolCallArgsShape::PairedQuote(_)),
375            "Gemma input must resolve via the PairedQuote shape, got {args_shape:?}"
376        );
377        assert_eq!(calls.len(), 1);
378        assert_eq!(calls[0].name, "get_weather");
379        assert_eq!(
380            calls[0].arguments,
381            ToolCallArguments::ValidJson(json!({"location": "Paris"})),
382        );
383    }
384}