tool-parser 1.11.0

Tool/function call parser for LLM model outputs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
use std::collections::HashMap;

use openai_protocol::common::Tool;
use serde::de::{Deserialize, IgnoredAny};
use serde_json::{de::Deserializer, Value};

use crate::{
    errors::{ParserError, ParserResult},
    types::{StreamingParseResult, ToolCallItem},
};

/// `param_name -> declared JSON-schema type` for the named function (empty if the
/// function or its `properties` are absent). Lets XML-style parsers coerce by the
/// declared type instead of guessing from text (e.g. keep a numeric-looking
/// `string` as a string). Only scalar `type` is read; unions/nullable schemas
/// have no single type, so they fall back to the caller's inference.
pub fn param_types_for_function(tools: &[Tool], func_name: &str) -> HashMap<String, String> {
    let mut types = HashMap::new();
    let Some(tool) = tools.iter().find(|t| t.function.name == func_name) else {
        return types;
    };
    if let Some(props) = tool
        .function
        .parameters_schema()
        .get("properties")
        .and_then(Value::as_object)
    {
        for (key, schema) in props {
            if let Some(ty) = schema.get("type").and_then(Value::as_str) {
                types.insert(key.clone(), ty.to_string());
            }
        }
    }
    types
}

/// Coerce a raw value by its declared JSON-schema type. For `string`, a JSON
/// string literal (`"4"`) is unwrapped while bare text (`4`, `true`) is kept
/// verbatim; other types are parsed. `None` (unknown type or parse failure)
/// means the caller should infer.
pub fn coerce_by_schema_type(text: &str, declared_type: Option<&str>) -> Option<Value> {
    match declared_type? {
        "string" => Some(Value::String(
            serde_json::from_str::<String>(text).unwrap_or_else(|_| text.to_string()),
        )),
        "integer" => text
            .trim()
            .parse::<i64>()
            .ok()
            .map(|n| Value::Number(n.into())),
        // Parse as JSON so large integers keep their precision (a plain f64 parse
        // would round e.g. 9007199254740993).
        "number" => serde_json::from_str::<Value>(text.trim())
            .ok()
            .filter(Value::is_number),
        "boolean" => match text.trim() {
            "true" | "True" => Some(Value::Bool(true)),
            "false" | "False" => Some(Value::Bool(false)),
            _ => None,
        },
        "object" | "array" => serde_json::from_str::<Value>(text).ok(),
        _ => None,
    }
}

/// Get a mapping of tool names to their indices
pub fn get_tool_indices(tools: &[Tool]) -> HashMap<String, usize> {
    tools
        .iter()
        .enumerate()
        .map(|(i, tool)| (tool.function.name.clone(), i))
        .collect()
}

/// Find the common prefix of two strings
/// Used for incremental argument streaming when partial JSON returns different intermediate states
pub fn find_common_prefix(s1: &str, s2: &str) -> String {
    s1.chars()
        .zip(s2.chars())
        .take_while(|(c1, c2)| c1 == c2)
        .map(|(c1, _)| c1)
        .collect()
}

/// Get unstreamed tool call arguments
/// Returns tool call items for arguments that have been parsed but not yet streamed
/// This ensures tool calls are properly completed even if the model generates final arguments in the last chunk
pub fn get_unstreamed_args(
    prev_tool_call_arr: &[Value],
    streamed_args_for_tool: &[String],
) -> Option<Vec<ToolCallItem>> {
    // Check if we have tool calls being tracked
    if prev_tool_call_arr.is_empty() || streamed_args_for_tool.is_empty() {
        return None;
    }

    // Get the last tool call that was being processed
    let tool_index = prev_tool_call_arr.len() - 1;
    if tool_index >= streamed_args_for_tool.len() {
        return None;
    }

    // Get expected vs actual arguments
    let expected_args = prev_tool_call_arr[tool_index].get("arguments")?;
    let expected_str = serde_json::to_string(expected_args).ok()?;
    let actual_str = &streamed_args_for_tool[tool_index];

    // Check if there are remaining arguments to send
    let remaining = if expected_str.starts_with(actual_str) {
        &expected_str[actual_str.len()..]
    } else {
        return None;
    };

    if remaining.is_empty() {
        return None;
    }

    // Return the remaining arguments as a ToolCallItem
    Some(vec![ToolCallItem {
        tool_index,
        name: None, // No name for argument deltas
        parameters: remaining.to_string(),
    }])
}

/// End-of-stream flush: take any text still buffered as a *prospective* tool
/// call so the caller can emit it as normal content instead of dropping it.
///
/// Returns the buffer verbatim when no tool call was ever announced this
/// request (`current_tool_id == -1`): the buffered text was only ever a
/// tool-call *candidate* (e.g. a bare `{` prefix or a partial start marker)
/// that never materialized, so it is content. Once a tool call has been
/// announced (`current_tool_id >= 0`), the buffered tail is tool-call syntax
/// (separators, closing brackets, or a partially-streamed call whose
/// remaining arguments are recovered via [`get_unstreamed_args`]) and is
/// discarded — matching the non-streaming path, which drops trailing
/// non-JSON text once tool calls were extracted.
pub fn take_unstreamed_normal_text(buffer: &mut String, current_tool_id: i32) -> String {
    let text = std::mem::take(buffer);
    if current_tool_id == -1 {
        text
    } else {
        String::new()
    }
}

/// Check if a buffer ends with a partial occurrence of a token
/// Returns Some(length) if there's a partial match, None otherwise
pub fn ends_with_partial_token(buffer: &str, token: &str) -> Option<usize> {
    if buffer.is_empty() || token.is_empty() {
        return None;
    }

    token
        .char_indices()
        .skip(1)
        .map(|(i, _)| i)
        .find(|&i| buffer.ends_with(&token[..i]))
}

/// Reset state for the current tool being parsed (used when skipping invalid tools).
/// This preserves the parser's overall state (current_tool_id, prev_tool_call_arr)
/// but clears the state specific to the current incomplete tool.
pub fn reset_current_tool_state(
    buffer: &mut String,
    current_tool_name_sent: &mut bool,
    streamed_args_for_tool: &mut Vec<String>,
    prev_tool_call_arr: &[Value],
) {
    buffer.clear();
    *current_tool_name_sent = false;

    // Only pop if we added an entry for the current (invalid) tool
    // streamed_args_for_tool should match prev_tool_call_arr length for completed tools
    if streamed_args_for_tool.len() > prev_tool_call_arr.len() {
        streamed_args_for_tool.pop();
    }
}

/// Reset the entire parser state (used at the start of a new request).
/// Clears all accumulated tool calls and resets all state to initial values.
pub fn reset_parser_state(
    buffer: &mut String,
    prev_tool_call_arr: &mut Vec<Value>,
    current_tool_id: &mut i32,
    current_tool_name_sent: &mut bool,
    streamed_args_for_tool: &mut Vec<String>,
) {
    buffer.clear();
    prev_tool_call_arr.clear();
    *current_tool_id = -1;
    *current_tool_name_sent = false;
    streamed_args_for_tool.clear();
}

/// Ensure arrays have capacity for the given tool ID
pub fn ensure_capacity(
    current_tool_id: i32,
    prev_tool_call_arr: &mut Vec<Value>,
    streamed_args_for_tool: &mut Vec<String>,
) {
    if current_tool_id < 0 {
        return;
    }
    let needed = (current_tool_id + 1) as usize;

    if prev_tool_call_arr.len() < needed {
        prev_tool_call_arr.resize_with(needed, || Value::Null);
    }
    if streamed_args_for_tool.len() < needed {
        streamed_args_for_tool.resize_with(needed, String::new);
    }
}

/// Check if a string contains complete, valid JSON
pub fn is_complete_json(input: &str) -> bool {
    let mut de = Deserializer::from_str(input);
    IgnoredAny::deserialize(&mut de).is_ok() && de.end().is_ok()
}

/// If the object has "parameters" but not "arguments", copy parameters to arguments.
pub fn normalize_arguments_field(mut obj: Value) -> Value {
    if obj.get("arguments").is_none() {
        if let Some(params) = obj.get("parameters").cloned() {
            if let Value::Object(ref mut map) = obj {
                map.insert("arguments".to_string(), params);
            }
        }
    }
    obj
}

/// If the object has "tool_name" but not "name", copy tool_name to name.
pub fn normalize_name_field(mut obj: Value) -> Value {
    if obj.get("name").is_none() {
        if let Some(tool_name) = obj.get("tool_name").cloned() {
            if let Value::Object(ref mut map) = obj {
                map.insert("name".to_string(), tool_name);
            }
        }
    }
    obj
}

/// Normalize both the name and arguments fields (e.g. Cohere's "tool_name"/"parameters").
pub fn normalize_tool_call_fields(obj: Value) -> Value {
    let obj = normalize_name_field(obj);
    normalize_arguments_field(obj)
}

/// Handle JSON tool-call streaming (parse partial JSON, validate names, stream the
/// name then arguments, advance the buffer) for the JSON, Llama, Mistral, and Qwen
/// parsers. `start_idx` is where JSON begins in `current_text`; `current_tool_id ==
/// -1` means no active tool.
#[expect(clippy::too_many_arguments)]
pub(crate) fn handle_json_tool_streaming(
    current_text: &str,
    start_idx: usize,
    partial_json: &mut crate::partial_json::PartialJson,
    tool_indices: &HashMap<String, usize>,
    buffer: &mut String,
    current_tool_id: &mut i32,
    current_tool_name_sent: &mut bool,
    streamed_args_for_tool: &mut Vec<String>,
    prev_tool_call_arr: &mut Vec<Value>,
) -> ParserResult<StreamingParseResult> {
    // Text this call has bailed out of tool parsing (see the loop below).
    let mut normal_text = String::new();
    // Where the next JSON value starts in `current_text`.
    let mut cursor = start_idx;

    // Each iteration parses one JSON value. A value that can never become a
    // declared tool call is emitted as content and the loop advances past it,
    // so a declared call trailing a non-tool value in the same chunk still
    // becomes tool-call deltas. Looping rather than recursing keeps stack
    // depth constant however many values a single chunk carries.
    let (current_tool_call, end_idx, is_complete) = loop {
        if cursor >= current_text.len() {
            if cursor > start_idx {
                buffer.clear();
            }
            return Ok(StreamingParseResult {
                normal_text,
                calls: vec![],
            });
        }

        let json_str = &current_text[cursor..];

        // When current_tool_name_sent is false, don't allow partial strings to avoid
        // parsing incomplete tool names as empty strings
        let allow_partial_strings = *current_tool_name_sent;

        let Ok((obj, end_idx)) = partial_json.parse_value(json_str, allow_partial_strings) else {
            // Not parseable yet - keep it buffered for the next chunk.
            if cursor > start_idx {
                *buffer = json_str.to_string();
            }
            return Ok(StreamingParseResult {
                normal_text,
                calls: vec![],
            });
        };

        // Check if JSON is complete - validate only the parsed portion.
        // Ensure end_idx is on a valid UTF-8 character boundary
        let safe_end_idx = if json_str.is_char_boundary(end_idx) {
            end_idx
        } else {
            // Find the nearest valid character boundary before end_idx
            (0..end_idx)
                .rev()
                .find(|&i| json_str.is_char_boundary(i))
                .unwrap_or(0)
        };
        let is_complete = is_complete_json(&json_str[..safe_end_idx]);

        // Normalize all tool call fields first (handles tool_name -> name, parameters -> arguments)
        // This must happen before validation since different LLMs use different field names
        let current_tool_call = normalize_tool_call_fields(obj);

        // A value that can never become a declared tool call is content: a
        // complete value with no `name`, or a `name` (complete by construction,
        // since partial strings are disallowed until the name is sent) that is
        // not among the declared tools. Dropping it here is what left streams
        // with neither content nor tool-call deltas, while the non-streaming
        // path returned the same text as content.
        let consumed = match current_tool_call.get("name").and_then(|v| v.as_str()) {
            Some(name) if !tool_indices.contains_key(name) => {
                tracing::debug!(
                    "Undeclared tool name '{}' - emitting buffered text as content",
                    name
                );
                reset_current_tool_state(
                    buffer,
                    current_tool_name_sent,
                    streamed_args_for_tool,
                    prev_tool_call_arr,
                );
                if is_complete {
                    cursor + safe_end_idx
                } else {
                    current_text.len()
                }
            }
            None if is_complete => cursor + safe_end_idx,
            _ => {
                // A declared tool call: hand it to the streaming cases below,
                // leaving the buffer holding everything from this value on.
                if cursor > start_idx {
                    *buffer = current_text[cursor..].to_string();
                }
                break (current_tool_call, end_idx, is_complete);
            }
        };

        // The first bail-out emits any marker prefix too, matching the
        // non-streaming fallback which returns unparsed tool text verbatim.
        let emit_from = if cursor > start_idx { cursor } else { 0 };
        normal_text.push_str(&current_text[emit_from..consumed]);
        cursor = consumed;

        // Keep parsing only if another *bare* JSON value follows. Anything
        // else - prose, or a marker-framed call such as qwen's
        // `</tool_call><tool_call>{..}` - stays buffered: a later chunk lets
        // the parser re-run its own marker detection over it, but if this was
        // the final chunk the end-of-stream flush surfaces it as content
        // instead. Marker-aware re-entry belongs in the parsers, which own
        // their start markers; see `take_unstreamed_normal_text`.
        let tail = &current_text[cursor..];
        let value_start = tail
            .char_indices()
            .find(|(_, c)| !c.is_whitespace() && *c != ',' && *c != ';')
            .map_or(tail.len(), |(i, _)| i);
        if !tail[value_start..].starts_with(['{', '[']) {
            *buffer = tail.to_string();
            return Ok(StreamingParseResult {
                normal_text,
                calls: vec![],
            });
        }
        // Separators between adjacent values belong to the emitted text.
        normal_text.push_str(&tail[..value_start]);
        cursor += value_start;
    };

    let mut result = StreamingParseResult {
        normal_text,
        calls: vec![],
    };

    // Case 1: Handle tool name streaming
    if !*current_tool_name_sent {
        if let Some(function_name) = current_tool_call.get("name").and_then(|v| v.as_str()) {
            if tool_indices.contains_key(function_name) {
                // Initialize if first tool
                if *current_tool_id == -1 {
                    *current_tool_id = 0;
                    streamed_args_for_tool.push(String::new());
                } else if *current_tool_id as usize >= streamed_args_for_tool.len() {
                    // Ensure capacity for subsequent tools
                    ensure_capacity(*current_tool_id, prev_tool_call_arr, streamed_args_for_tool);
                }

                // Send tool name with empty parameters
                *current_tool_name_sent = true;
                result.calls.push(ToolCallItem {
                    tool_index: *current_tool_id as usize,
                    name: Some(function_name.to_string()),
                    parameters: String::new(),
                });
            }
        }
    }

    // Case 2: Handle streaming arguments. This is deliberately not an `else`:
    // when the whole call arrives in one chunk, Case 1 sends the name and the
    // arguments must still stream in the *same* invocation. There is no later
    // call to fall back on - the router stops feeding the parser once the
    // engine stream ends - so returning here would emit a tool call with an
    // empty `arguments`, and `get_unstreamed_tool_args()` could not recover
    // them either because only this branch populates `prev_tool_call_arr`.
    // `current_tool_name_sent` implies `current_tool_id >= 0`.
    if *current_tool_name_sent {
        // A complete call without arguments takes `{}`, as in the
        // non-streaming parse.
        let empty_obj = Value::Object(serde_json::Map::new());
        let Some(cur_arguments) = current_tool_call
            .get("arguments")
            .or_else(|| is_complete.then_some(&empty_obj))
        else {
            return Ok(result);
        };
        let tool_id = *current_tool_id as usize;
        let sent = streamed_args_for_tool
            .get(tool_id)
            .map(|s| s.len())
            .unwrap_or(0);
        let cur_args_json = serde_json::to_string(cur_arguments)
            .map_err(|e| ParserError::ParsingFailed(e.to_string()))?;

        // Get prev_arguments (matches Python's structure)
        let prev_arguments = if tool_id < prev_tool_call_arr.len() {
            prev_tool_call_arr[tool_id].get("arguments")
        } else {
            None
        };

        // Calculate diff: everything after we've already sent
        let mut argument_diff = None;

        if is_complete {
            // Python: argument_diff = cur_args_json[sent:]
            // Rust needs bounds check (Python returns "" automatically)
            argument_diff = if sent < cur_args_json.len() {
                Some(cur_args_json[sent..].to_string())
            } else {
                Some(String::new())
            };
        } else if let Some(prev_args) = prev_arguments {
            let prev_args_json = serde_json::to_string(prev_args)
                .map_err(|e| ParserError::ParsingFailed(e.to_string()))?;

            if cur_args_json != prev_args_json {
                let prefix = find_common_prefix(&prev_args_json, &cur_args_json);
                argument_diff = if sent < prefix.len() {
                    Some(prefix[sent..].to_string())
                } else {
                    Some(String::new())
                };
            }
        }

        // Send diff if present
        if let Some(diff) = argument_diff {
            if !diff.is_empty() {
                if tool_id < streamed_args_for_tool.len() {
                    streamed_args_for_tool[tool_id].push_str(&diff);
                }
                result.calls.push(ToolCallItem {
                    tool_index: tool_id,
                    name: None,
                    parameters: diff,
                });
            }
        }

        // Update prev_tool_call_arr with current state
        if *current_tool_id >= 0 {
            ensure_capacity(*current_tool_id, prev_tool_call_arr, streamed_args_for_tool);

            if tool_id < prev_tool_call_arr.len() {
                prev_tool_call_arr[tool_id] = current_tool_call;
            }
        }

        // If complete, advance to next tool
        if is_complete {
            *buffer = current_text[cursor + end_idx..].to_string();
            *current_tool_name_sent = false;
            *current_tool_id += 1;
        }
    }

    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ends_with_partial_token() {
        assert!(ends_with_partial_token("hello <|py", "<|python_tag|>").is_some());
        assert!(ends_with_partial_token("hello <|python_tag", "<|python_tag|>").is_some());
        assert!(ends_with_partial_token("hello <|python_tag|>", "<|python_tag|>").is_none());
        assert!(ends_with_partial_token("", "<|python_tag|>").is_none());
        assert!(ends_with_partial_token("hello world", "<|python_tag|>").is_none());
    }

    #[test]
    fn test_ends_with_partial_token_multibyte() {
        // U+FF5C "|" is 3 bytes, so token[..i] must only be sliced at char boundaries.
        let token = "<|tool_calls_begin|>";

        // Buffer not ending in '<' must not panic and yields no partial match.
        assert_eq!(
            ends_with_partial_token("some plain ascii text", token),
            None
        );

        // Ending in '<' matches a 1-byte prefix.
        assert_eq!(ends_with_partial_token("hello <", token), Some(1));

        // Ending in "<|" matches the prefix up to the next char boundary (1 + 3 bytes).
        assert_eq!(ends_with_partial_token("hello <|", token), Some(4));

        // A complete token at the end is not a partial match.
        assert_eq!(
            ends_with_partial_token("x <|tool_calls_begin|>", token),
            None
        );
    }

    #[test]
    fn test_reset_current_tool_state() {
        let mut buffer = String::from("partial json");
        let mut current_tool_name_sent = true;
        let mut streamed_args = vec!["tool0_args".to_string(), "tool1_partial".to_string()];
        let prev_tools = vec![serde_json::json!({"name": "tool0"})];

        reset_current_tool_state(
            &mut buffer,
            &mut current_tool_name_sent,
            &mut streamed_args,
            &prev_tools,
        );

        assert_eq!(buffer, "");
        assert!(!current_tool_name_sent);
        assert_eq!(streamed_args.len(), 1); // Popped the partial tool1 args
        assert_eq!(streamed_args[0], "tool0_args");
    }

    #[test]
    fn test_reset_current_tool_state_no_pop_when_synced() {
        let mut buffer = String::from("partial json");
        let mut current_tool_name_sent = true;
        let mut streamed_args = vec!["tool0_args".to_string()];
        let prev_tools = vec![serde_json::json!({"name": "tool0"})];

        reset_current_tool_state(
            &mut buffer,
            &mut current_tool_name_sent,
            &mut streamed_args,
            &prev_tools,
        );

        assert_eq!(buffer, "");
        assert!(!current_tool_name_sent);
        assert_eq!(streamed_args.len(), 1); // No pop, lengths matched
    }

    #[test]
    fn test_reset_parser_state() {
        let mut buffer = String::from("some buffer");
        let mut prev_tools = vec![serde_json::json!({"name": "tool0"})];
        let mut current_tool_id = 5;
        let mut current_tool_name_sent = true;
        let mut streamed_args = vec!["args".to_string()];

        reset_parser_state(
            &mut buffer,
            &mut prev_tools,
            &mut current_tool_id,
            &mut current_tool_name_sent,
            &mut streamed_args,
        );

        assert_eq!(buffer, "");
        assert_eq!(prev_tools.len(), 0);
        assert_eq!(current_tool_id, -1);
        assert!(!current_tool_name_sent);
        assert_eq!(streamed_args.len(), 0);
    }

    #[test]
    fn test_ensure_capacity() {
        let mut prev_tools = vec![];
        let mut streamed_args = vec![];

        ensure_capacity(2, &mut prev_tools, &mut streamed_args);

        assert_eq!(prev_tools.len(), 3);
        assert_eq!(streamed_args.len(), 3);
        assert_eq!(prev_tools[0], Value::Null);
        assert_eq!(streamed_args[0], "");
    }

    #[test]
    fn test_ensure_capacity_negative_id() {
        let mut prev_tools = vec![];
        let mut streamed_args = vec![];

        ensure_capacity(-1, &mut prev_tools, &mut streamed_args);

        // Should not resize for negative ID
        assert_eq!(prev_tools.len(), 0);
        assert_eq!(streamed_args.len(), 0);
    }

    #[test]
    fn test_is_complete_json() {
        assert!(is_complete_json(r#"{"name": "test"}"#));
        assert!(is_complete_json("[1, 2, 3]"));
        assert!(is_complete_json("42"));
        assert!(is_complete_json("true"));
        assert!(!is_complete_json(r#"{"name": "#));
        assert!(!is_complete_json("[1, 2,"));
    }

    #[test]
    fn test_normalize_arguments_field() {
        // Case 1: Has parameters, no arguments
        let obj = serde_json::json!({
            "name": "test",
            "parameters": {"key": "value"}
        });
        let normalized = normalize_arguments_field(obj);
        assert_eq!(
            normalized.get("arguments").unwrap(),
            &serde_json::json!({"key": "value"})
        );

        // Case 2: Already has arguments
        let obj = serde_json::json!({
            "name": "test",
            "arguments": {"key": "value"}
        });
        let normalized = normalize_arguments_field(obj.clone());
        assert_eq!(normalized, obj);

        // Case 3: No parameters or arguments
        let obj = serde_json::json!({"name": "test"});
        let normalized = normalize_arguments_field(obj.clone());
        assert_eq!(normalized, obj);
    }

    #[test]
    fn test_normalize_name_field() {
        // Case 1: Has tool_name, no name (Cohere format)
        let obj = serde_json::json!({
            "tool_name": "search",
            "parameters": {"query": "test"}
        });
        let normalized = normalize_name_field(obj);
        assert_eq!(normalized.get("name").unwrap(), "search");

        // Case 2: Already has name (standard format)
        let obj = serde_json::json!({
            "name": "test",
            "arguments": {"key": "value"}
        });
        let normalized = normalize_name_field(obj.clone());
        assert_eq!(normalized, obj);

        // Case 3: Has both tool_name and name - name takes precedence
        let obj = serde_json::json!({
            "tool_name": "cohere_name",
            "name": "standard_name",
            "parameters": {}
        });
        let normalized = normalize_name_field(obj);
        assert_eq!(normalized.get("name").unwrap(), "standard_name");

        // Case 4: No name or tool_name
        let obj = serde_json::json!({"parameters": {}});
        let normalized = normalize_name_field(obj.clone());
        assert!(normalized.get("name").is_none());
    }

    #[test]
    fn test_normalize_tool_call_fields() {
        // Case 1: Full Cohere format with tool_name and parameters
        let obj = serde_json::json!({
            "tool_name": "search",
            "parameters": {"query": "rust programming"}
        });
        let normalized = normalize_tool_call_fields(obj);
        assert_eq!(normalized.get("name").unwrap(), "search");
        assert_eq!(
            normalized.get("arguments").unwrap(),
            &serde_json::json!({"query": "rust programming"})
        );

        // Case 2: Standard format - should remain unchanged
        let obj = serde_json::json!({
            "name": "test",
            "arguments": {"key": "value"}
        });
        let normalized = normalize_tool_call_fields(obj.clone());
        assert_eq!(normalized, obj);

        // Case 3: Mixed format (name + parameters)
        let obj = serde_json::json!({
            "name": "test",
            "parameters": {"key": "value"}
        });
        let normalized = normalize_tool_call_fields(obj);
        assert_eq!(normalized.get("name").unwrap(), "test");
        assert_eq!(
            normalized.get("arguments").unwrap(),
            &serde_json::json!({"key": "value"})
        );
    }

    #[test]
    fn test_coerce_by_schema_type() {
        use serde_json::json;
        // string: numeric/bool/array-looking text stays a string; a JSON string
        // literal is unwrapped.
        assert_eq!(coerce_by_schema_type("4", Some("string")), Some(json!("4")));
        assert_eq!(
            coerce_by_schema_type("true", Some("string")),
            Some(json!("true"))
        );
        assert_eq!(
            coerce_by_schema_type("[60,30]", Some("string")),
            Some(json!("[60,30]"))
        );
        assert_eq!(
            coerce_by_schema_type("\"4\"", Some("string")),
            Some(json!("4"))
        );

        assert_eq!(coerce_by_schema_type("4", Some("integer")), Some(json!(4)));
        assert_eq!(
            coerce_by_schema_type("true", Some("boolean")),
            Some(json!(true))
        );
        assert_eq!(
            coerce_by_schema_type("[60,30]", Some("array")),
            Some(json!([60, 30]))
        );
        // number keeps integer precision (no f64 rounding).
        assert_eq!(
            coerce_by_schema_type("9007199254740993", Some("number")),
            Some(json!(9007199254740993i64))
        );

        // unknown type / value that fails to parse -> None (caller infers)
        assert_eq!(coerce_by_schema_type("4", None), None);
        assert_eq!(coerce_by_schema_type("abc", Some("integer")), None);
    }
}