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