Skip to main content

llama_cpp_bindings/tool_call_format/
json_object.rs

1use 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
55/// # Errors
56///
57/// Returns [`JsonObjectFailure`] when the body contains a JSON object that
58/// looks like a tool call (matches the open brace at start) but the JSON itself
59/// is malformed.
60pub 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}