edgequake_llm/
stream_tools.rs1use std::collections::BTreeMap;
4
5use crate::error::{LlmError, Result};
6use crate::traits::{FunctionCall, ToolCall};
7
8#[derive(Debug, Clone, Default)]
10pub struct PartialStreamToolCall {
11 pub id: Option<String>,
12 pub function_name: Option<String>,
13 pub arguments: String,
14 pub thought_signature: Option<String>,
15}
16
17#[derive(Debug, Clone, Copy, Default)]
19pub struct FinalizeStreamToolCallsOptions {
20 pub empty_args_fallback: bool,
22}
23
24pub fn finalize_streamed_tool_calls_with_repair(
29 partials: BTreeMap<usize, PartialStreamToolCall>,
30 options: FinalizeStreamToolCallsOptions,
31 repair: Option<fn(&str) -> String>,
32) -> Result<Vec<ToolCall>> {
33 let mut out = Vec::with_capacity(partials.len());
34 for (index, partial) in partials {
35 let name = partial.function_name.unwrap_or_default();
36 if name.trim().is_empty() {
37 return Err(LlmError::InvalidRequest(format!(
38 "streamed tool call at index {index} missing function name"
39 )));
40 }
41 let id = partial
42 .id
43 .filter(|s| !s.trim().is_empty())
44 .unwrap_or_else(|| format!("call_{index}"));
45
46 let mut args = partial.arguments;
47 if let Some(repair_fn) = repair {
48 args = repair_fn(&args);
49 } else if options.empty_args_fallback {
50 let trimmed = args.trim();
51 if trimmed.is_empty() || trimmed == "null" || trimmed == "None" {
52 args = "{}".to_string();
53 }
54 }
55
56 let trimmed = args.trim();
57 if trimmed.is_empty() || trimmed == "null" || trimmed == "None" {
58 if options.empty_args_fallback {
59 args = "{}".to_string();
60 } else {
61 return Err(LlmError::InvalidRequest(format!(
62 "streamed tool call at index {index} finished without arguments"
63 )));
64 }
65 } else if !options.empty_args_fallback
66 && serde_json::from_str::<serde_json::Value>(trimmed).is_err()
67 {
68 return Err(LlmError::InvalidRequest(format!(
69 "streamed tool call at index {index} has invalid JSON arguments"
70 )));
71 }
72
73 out.push(ToolCall {
74 id,
75 call_type: "function".into(),
76 function: FunctionCall {
77 name,
78 arguments: args,
79 },
80 thought_signature: partial.thought_signature,
81 });
82 }
83 Ok(out)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn finalize_applies_repair_and_default_id() {
92 let mut map = BTreeMap::new();
93 map.insert(
94 0,
95 PartialStreamToolCall {
96 id: None,
97 function_name: Some("read_file".into()),
98 arguments: "null".into(),
99 thought_signature: None,
100 },
101 );
102 let calls = finalize_streamed_tool_calls_with_repair(
103 map,
104 FinalizeStreamToolCallsOptions {
105 empty_args_fallback: true,
106 },
107 Some(|s| {
108 if s.trim() == "null" {
109 "{}".into()
110 } else {
111 s.to_string()
112 }
113 }),
114 )
115 .expect("ok");
116 assert_eq!(calls.len(), 1);
117 assert_eq!(calls[0].id, "call_0");
118 assert_eq!(calls[0].function.arguments, "{}");
119 }
120}