1use crate::error::{MiniLLMError, Result};
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct ToolDefinition {
33 pub name: String,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
39 pub description: Option<String>,
40
41 pub parameters: serde_json::Value,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
50 pub strict: Option<bool>,
51}
52
53impl ToolDefinition {
54 pub fn new(
56 name: impl Into<String>,
57 description: impl Into<String>,
58 parameters: serde_json::Value,
59 ) -> Self {
60 Self {
61 name: name.into(),
62 description: Some(description.into()),
63 parameters,
64 strict: None,
65 }
66 }
67
68 pub fn with_strict(mut self, strict: bool) -> Self {
70 self.strict = Some(strict);
71 self
72 }
73
74 pub fn to_openai_value(&self) -> serde_json::Value {
76 let mut function = serde_json::json!({
77 "name": self.name,
78 "parameters": self.parameters,
79 });
80 if let Some(desc) = &self.description {
81 function["description"] = serde_json::json!(desc);
82 }
83 if let Some(strict) = self.strict {
84 function["strict"] = serde_json::json!(strict);
85 }
86 serde_json::json!({ "type": "function", "function": function })
87 }
88
89 pub fn to_anthropic_value(&self) -> serde_json::Value {
91 let mut tool = serde_json::json!({
92 "name": self.name,
93 "input_schema": self.parameters,
94 });
95 if let Some(desc) = &self.description {
96 tool["description"] = serde_json::json!(desc);
97 }
98 if let Some(strict) = self.strict {
99 tool["strict"] = serde_json::json!(strict);
100 }
101 tool
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub enum ToolChoice {
108 Auto,
111 None,
113 Required,
116 Tool(String),
118}
119
120impl ToolChoice {
121 pub fn to_openai_value(&self) -> serde_json::Value {
123 match self {
124 Self::Auto => serde_json::json!("auto"),
125 Self::None => serde_json::json!("none"),
126 Self::Required => serde_json::json!("required"),
127 Self::Tool(name) => serde_json::json!({
128 "type": "function",
129 "function": { "name": name },
130 }),
131 }
132 }
133
134 pub fn to_anthropic_value(&self) -> serde_json::Value {
138 match self {
139 Self::Auto => serde_json::json!({ "type": "auto" }),
140 Self::None => serde_json::json!({ "type": "none" }),
141 Self::Required => serde_json::json!({ "type": "any" }),
142 Self::Tool(name) => serde_json::json!({ "type": "tool", "name": name }),
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct ToolCall {
160 pub id: String,
163
164 pub name: String,
166
167 pub arguments: String,
169}
170
171impl ToolCall {
172 pub fn new(
174 id: impl Into<String>,
175 name: impl Into<String>,
176 arguments: impl Into<String>,
177 ) -> Self {
178 Self {
179 id: id.into(),
180 name: name.into(),
181 arguments: arguments.into(),
182 }
183 }
184
185 pub fn arguments_json(&self) -> Result<serde_json::Value> {
188 serde_json::from_str(&self.arguments).map_err(|e| {
189 MiniLLMError::InvalidParameter(format!(
190 "tool call '{}' ({}) carries invalid JSON arguments: {} (raw: {})",
191 self.name, self.id, e, self.arguments
192 ))
193 })
194 }
195
196 pub fn to_openai_value(&self) -> serde_json::Value {
200 serde_json::json!({
201 "id": self.id,
202 "type": "function",
203 "function": {
204 "name": self.name,
205 "arguments": self.arguments,
206 },
207 })
208 }
209
210 pub fn to_anthropic_block(&self) -> Result<serde_json::Value> {
214 Ok(serde_json::json!({
215 "type": "tool_use",
216 "id": self.id,
217 "name": self.name,
218 "input": self.arguments_json()?,
219 }))
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Default)]
232pub struct ToolCallDelta {
233 pub index: u64,
235
236 pub id: Option<String>,
238
239 pub name: Option<String>,
241
242 pub arguments_fragment: Option<String>,
244}
245
246#[derive(Debug, Default)]
252pub struct ToolCallAccumulator {
253 slots: BTreeMap<u64, PartialToolCall>,
254}
255
256#[derive(Debug, Default)]
257struct PartialToolCall {
258 id: Option<String>,
259 name: Option<String>,
260 arguments: String,
261}
262
263impl ToolCallAccumulator {
264 pub fn ingest(&mut self, deltas: &[ToolCallDelta]) {
266 for delta in deltas {
267 let slot = self.slots.entry(delta.index).or_default();
268 if let Some(id) = &delta.id {
269 slot.id = Some(id.clone());
270 }
271 if let Some(name) = &delta.name {
272 slot.name = Some(name.clone());
273 }
274 if let Some(frag) = &delta.arguments_fragment {
275 slot.arguments.push_str(frag);
276 }
277 }
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.slots.is_empty()
283 }
284
285 pub fn finish(&self) -> Vec<ToolCall> {
290 self.slots
291 .iter()
292 .filter_map(|(index, slot)| match (&slot.id, &slot.name) {
293 (Some(id), Some(name)) => {
294 Some(ToolCall::new(id.clone(), name.clone(), slot.arguments.clone()))
295 }
296 _ => {
297 tracing::warn!(
298 index,
299 has_id = slot.id.is_some(),
300 has_name = slot.name.is_some(),
301 "incomplete tool call fragment dropped (stream cancelled mid-call or malformed wire)"
302 );
303 None
304 }
305 })
306 .collect()
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn weather_tool() -> ToolDefinition {
315 ToolDefinition::new(
316 "get_weather",
317 "Get the current weather for a city",
318 serde_json::json!({
319 "type": "object",
320 "properties": { "city": { "type": "string" } },
321 "required": ["city"],
322 }),
323 )
324 }
325
326 #[test]
327 fn definition_openai_wire_shape() {
328 let v = weather_tool().with_strict(true).to_openai_value();
329 assert_eq!(v["type"], "function");
330 assert_eq!(v["function"]["name"], "get_weather");
331 assert_eq!(
332 v["function"]["description"],
333 "Get the current weather for a city"
334 );
335 assert_eq!(v["function"]["parameters"]["type"], "object");
336 assert_eq!(v["function"]["strict"], true);
337 }
338
339 #[test]
340 fn definition_anthropic_wire_shape() {
341 let v = weather_tool().to_anthropic_value();
342 assert_eq!(v["name"], "get_weather");
343 assert_eq!(v["input_schema"]["type"], "object");
344 assert!(v.get("strict").is_none(), "strict omitted when unset");
345 assert!(v.get("type").is_none());
347 assert!(v.get("parameters").is_none());
348 }
349
350 #[test]
351 fn choice_openai_wire_values() {
352 assert_eq!(ToolChoice::Auto.to_openai_value(), "auto");
353 assert_eq!(ToolChoice::None.to_openai_value(), "none");
354 assert_eq!(ToolChoice::Required.to_openai_value(), "required");
355 let forced = ToolChoice::Tool("get_weather".into()).to_openai_value();
356 assert_eq!(forced["type"], "function");
357 assert_eq!(forced["function"]["name"], "get_weather");
358 }
359
360 #[test]
361 fn choice_anthropic_wire_values() {
362 assert_eq!(ToolChoice::Auto.to_anthropic_value()["type"], "auto");
363 assert_eq!(ToolChoice::None.to_anthropic_value()["type"], "none");
364 assert_eq!(ToolChoice::Required.to_anthropic_value()["type"], "any");
366 let forced = ToolChoice::Tool("get_weather".into()).to_anthropic_value();
367 assert_eq!(forced["type"], "tool");
368 assert_eq!(forced["name"], "get_weather");
369 }
370
371 #[test]
372 fn call_arguments_json_parses_or_fails_loudly() {
373 let ok = ToolCall::new("c1", "get_weather", r#"{"city":"Paris"}"#);
374 assert_eq!(ok.arguments_json().unwrap()["city"], "Paris");
375 let bad = ToolCall::new("c2", "get_weather", "{not json");
376 assert!(bad.arguments_json().is_err());
377 }
378
379 #[test]
380 fn call_openai_wire_keeps_arguments_as_string() {
381 let v = ToolCall::new("c1", "get_weather", r#"{"city":"Paris"}"#).to_openai_value();
382 assert_eq!(v["id"], "c1");
383 assert_eq!(v["type"], "function");
384 assert_eq!(v["function"]["name"], "get_weather");
385 assert_eq!(v["function"]["arguments"], r#"{"city":"Paris"}"#);
386 assert!(v["function"]["arguments"].is_string());
387 }
388
389 #[test]
390 fn call_anthropic_block_parses_arguments_to_object() {
391 let b = ToolCall::new("c1", "get_weather", r#"{"city":"Paris"}"#)
392 .to_anthropic_block()
393 .unwrap();
394 assert_eq!(b["type"], "tool_use");
395 assert_eq!(b["id"], "c1");
396 assert_eq!(b["name"], "get_weather");
397 assert_eq!(b["input"]["city"], "Paris");
398 assert!(b["input"].is_object(), "input is an object, not a string");
399 assert!(ToolCall::new("c2", "t", "{bad")
401 .to_anthropic_block()
402 .is_err());
403 }
404
405 #[test]
406 fn accumulator_assembles_fragments_by_index() {
407 let mut acc = ToolCallAccumulator::default();
408 acc.ingest(&[ToolCallDelta {
409 index: 0,
410 id: Some("c0".into()),
411 name: Some("search".into()),
412 arguments_fragment: Some("{\"q\":".into()),
413 }]);
414 acc.ingest(&[ToolCallDelta {
415 index: 0,
416 arguments_fragment: Some("\"rust\"}".into()),
417 ..Default::default()
418 }]);
419 let calls = acc.finish();
420 assert_eq!(calls.len(), 1);
421 assert_eq!(calls[0].id, "c0");
422 assert_eq!(calls[0].name, "search");
423 assert_eq!(calls[0].arguments, r#"{"q":"rust"}"#);
424 }
425
426 #[test]
427 fn accumulator_handles_sparse_and_interleaved_indices() {
428 let mut acc = ToolCallAccumulator::default();
432 acc.ingest(&[
433 ToolCallDelta {
434 index: 3,
435 id: Some("c3".into()),
436 name: Some("b".into()),
437 ..Default::default()
438 },
439 ToolCallDelta {
440 index: 1,
441 id: Some("c1".into()),
442 name: Some("a".into()),
443 ..Default::default()
444 },
445 ]);
446 acc.ingest(&[
447 ToolCallDelta {
448 index: 1,
449 arguments_fragment: Some("{}".into()),
450 ..Default::default()
451 },
452 ToolCallDelta {
453 index: 3,
454 arguments_fragment: Some("{}".into()),
455 ..Default::default()
456 },
457 ]);
458 let calls = acc.finish();
459 assert_eq!(calls.len(), 2);
460 assert_eq!(calls[0].id, "c1", "index order preserved");
461 assert_eq!(calls[1].id, "c3");
462 }
463
464 #[test]
465 fn accumulator_drops_incomplete_slots_and_never_allocates_by_index() {
466 let mut acc = ToolCallAccumulator::default();
467 acc.ingest(&[ToolCallDelta {
469 index: 4_000_000_000,
470 arguments_fragment: Some("junk".into()),
471 ..Default::default()
472 }]);
473 assert!(acc.finish().is_empty());
475 }
476
477 #[test]
478 fn tool_call_round_trips_serde() {
479 let call = ToolCall::new("c1", "get_weather", r#"{"city":"Paris"}"#);
482 let json = serde_json::to_string(&call).unwrap();
483 let back: ToolCall = serde_json::from_str(&json).unwrap();
484 assert_eq!(back, call);
485 }
486}