llama_cpp_bindings/tool_call_format/
json_object.rs1use llama_cpp_bindings_types::JsonObjectShape;
2use llama_cpp_bindings_types::ParsedToolCall;
3use llama_cpp_bindings_types::ToolCallArguments;
4
5use crate::error::JsonObjectFailure;
6
7fn try_parse_one_object(
8 input: &str,
9 shape: &JsonObjectShape,
10) -> Result<Option<(ParsedToolCall, usize)>, JsonObjectFailure> {
11 let trimmed_start = input.find('{');
12 let Some(start) = trimmed_start else {
13 return Ok(None);
14 };
15
16 let mut stream =
17 serde_json::Deserializer::from_str(&input[start..]).into_iter::<serde_json::Value>();
18 let value = match stream.next() {
19 Some(Ok(value)) => value,
20 Some(Err(err)) => {
21 return Err(JsonObjectFailure::InvalidJson {
22 message: err.to_string(),
23 });
24 }
25 None => return Ok(None),
26 };
27 let consumed = stream.byte_offset();
28
29 let serde_json::Value::Object(map) = value else {
30 return Ok(None);
31 };
32
33 let Some(name_value) = map.get(&shape.name_field) else {
34 return Ok(None);
35 };
36 let serde_json::Value::String(name) = name_value else {
37 return Ok(None);
38 };
39
40 let arguments_value = map
41 .get(&shape.arguments_field)
42 .cloned()
43 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
44 let arguments = ToolCallArguments::from_string(arguments_value.to_string());
45
46 let trailing_extras = map
47 .keys()
48 .any(|key| key != &shape.name_field && key != &shape.arguments_field);
49 if trailing_extras {
50 return Ok(None);
51 }
52
53 Ok(Some((
54 ParsedToolCall::new(String::new(), name.clone(), arguments),
55 start + consumed,
56 )))
57}
58
59pub fn parse(
65 body: &str,
66 shape: &JsonObjectShape,
67) -> Result<Vec<ParsedToolCall>, JsonObjectFailure> {
68 if shape.name_field.is_empty() || shape.arguments_field.is_empty() {
69 return Ok(Vec::new());
70 }
71
72 let mut parsed = Vec::new();
73 let mut remaining = body;
74
75 while let Some((call, consumed)) = try_parse_one_object(remaining, shape)? {
76 parsed.push(call);
77 remaining = &remaining[consumed..];
78 }
79
80 Ok(parsed)
81}
82
83#[cfg(test)]
84mod tests {
85 use llama_cpp_bindings_types::JsonObjectShape;
86 use llama_cpp_bindings_types::ToolCallArguments;
87 use serde_json::json;
88
89 use super::parse;
90 use crate::error::JsonObjectFailure;
91
92 fn qwen3_shape() -> JsonObjectShape {
93 JsonObjectShape {
94 name_field: "name".to_owned(),
95 arguments_field: "arguments".to_owned(),
96 }
97 }
98
99 #[test]
100 fn parses_single_json_object_with_name_and_arguments() {
101 let parsed = parse(
102 r#"{"name": "get_weather", "arguments": {"location": "Paris"}}"#,
103 &qwen3_shape(),
104 )
105 .expect("must parse");
106
107 assert_eq!(parsed.len(), 1);
108 assert_eq!(parsed[0].name, "get_weather");
109 assert_eq!(
110 parsed[0].arguments,
111 ToolCallArguments::ValidJson(json!({"location": "Paris"})),
112 );
113 }
114
115 #[test]
116 fn parses_json_object_after_leading_whitespace_and_newlines() {
117 let parsed = parse(
118 "\n {\"name\": \"f\", \"arguments\": {\"a\": 1}}\n",
119 &qwen3_shape(),
120 )
121 .expect("must parse");
122
123 assert_eq!(parsed.len(), 1);
124 assert_eq!(parsed[0].name, "f");
125 }
126
127 #[test]
128 fn parses_two_consecutive_json_objects() {
129 let parsed = parse(
130 r#"{"name": "a", "arguments": {}}{"name": "b", "arguments": {"x": 2}}"#,
131 &qwen3_shape(),
132 )
133 .expect("must parse");
134
135 assert_eq!(parsed.len(), 2);
136 assert_eq!(parsed[0].name, "a");
137 assert_eq!(parsed[1].name, "b");
138 }
139
140 #[test]
141 fn parses_object_with_arguments_field_missing_yields_empty_arguments() {
142 let parsed = parse(r#"{"name": "ping"}"#, &qwen3_shape()).expect("must parse");
143
144 assert_eq!(parsed.len(), 1);
145 assert_eq!(parsed[0].name, "ping");
146 assert_eq!(parsed[0].arguments, ToolCallArguments::ValidJson(json!({})),);
147 }
148
149 #[test]
150 fn rejects_json_object_with_extra_unexpected_top_level_keys() {
151 let parsed = parse(
152 r#"{"name": "f", "arguments": {}, "extra": 1}"#,
153 &qwen3_shape(),
154 )
155 .expect("must parse");
156
157 assert!(parsed.is_empty(), "extra top-level key must reject");
158 }
159
160 #[test]
161 fn rejects_json_object_with_non_string_name() {
162 let parsed =
163 parse(r#"{"name": 123, "arguments": {}}"#, &qwen3_shape()).expect("must parse");
164
165 assert!(parsed.is_empty(), "non-string name must reject");
166 }
167
168 #[test]
169 fn rejects_input_without_open_brace() {
170 let parsed = parse("plain content", &qwen3_shape()).expect("must parse");
171 assert!(parsed.is_empty());
172 }
173
174 #[test]
175 fn rejects_array_instead_of_object() {
176 let parsed = parse("[1, 2, 3]", &qwen3_shape()).expect("must parse");
177 assert!(parsed.is_empty());
178 }
179
180 #[test]
181 fn returns_failure_for_malformed_json() {
182 let err = parse(r#"{"name": "f", "arguments": {"a": }"#, &qwen3_shape()).unwrap_err();
183 let JsonObjectFailure::InvalidJson { message } = err;
184
185 assert!(!message.is_empty());
186 }
187
188 #[test]
189 fn returns_empty_when_object_is_not_a_tool_call_shape() {
190 let parsed = parse("{ \"foo\": 1 }", &qwen3_shape()).expect("must parse");
191
192 assert!(parsed.is_empty());
193 }
194
195 #[test]
196 fn returns_empty_when_shape_has_empty_required_field() {
197 let mut shape = qwen3_shape();
198 shape.name_field.clear();
199 let parsed = parse(r#"{"name": "x", "arguments": {}}"#, &shape).expect("must parse");
200 assert!(parsed.is_empty());
201 }
202}