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