use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub struct LlmToolCall<'a> {
pub id: &'a str,
pub name: &'a str,
pub arguments_raw: Option<&'a str>,
pub arguments: Value,
}
pub fn parse_tool_calls(response: &Value) -> Vec<LlmToolCall<'_>> {
let mut results = Vec::new();
if let Some(calls) = response.get("tool_calls").and_then(|v| v.as_array()) {
results.extend(parse_tool_calls_array(calls));
} else if let Some(choices) = response.get("choices").and_then(|v| v.as_array()) {
for choice in choices {
if let Some(msg) = choice.get("message")
&& let Some(calls) = msg.get("tool_calls").and_then(|v| v.as_array())
{
results.extend(parse_tool_calls_array(calls));
}
}
}
results
}
fn parse_tool_calls_array(calls: &[Value]) -> Vec<LlmToolCall<'_>> {
calls.iter().filter_map(parse_tool_call).collect()
}
pub fn parse_tool_calls_from_message(message: &Value) -> Vec<LlmToolCall<'_>> {
let Some(calls) = message.get("tool_calls").and_then(|v| v.as_array()) else {
return Vec::new();
};
parse_tool_calls_array(calls)
}
fn parse_tool_call(tool_call: &Value) -> Option<LlmToolCall<'_>> {
let id = tool_call.get("id")?.as_str()?;
let function = tool_call.get("function")?.as_object()?;
let name = function.get("name")?.as_str()?;
let (arguments_raw, arguments) = match function.get("arguments") {
Some(Value::String(raw)) => (
Some(raw.as_str()),
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.clone())),
),
Some(value) => (None, value.clone()),
None => (None, Value::Null),
};
Some(LlmToolCall {
id,
name,
arguments_raw,
arguments,
})
}
pub fn parse_first_tool_call(response: &Value) -> Option<LlmToolCall<'_>> {
parse_tool_calls(response).into_iter().next()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_parse_tool_calls_from_message() {
let message = json!({
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Shenzhen\"}"
}
}
]
});
let calls = parse_tool_calls_from_message(&message);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].id, "call_123");
assert_eq!(calls[0].name, "get_weather");
}
#[test]
fn test_parse_tool_calls_from_message_object_args() {
let message = json!({
"tool_calls": [
{
"id": "call_456",
"type": "function",
"function": {
"name": "calc",
"arguments": {"op": "add", "a": 1, "b": 2}
}
}
]
});
let calls = parse_tool_calls_from_message(&message);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "calc");
assert_eq!(calls[0].arguments, json!({"op": "add", "a": 1, "b": 2}));
}
#[test]
fn test_parse_tool_calls_from_response() {
let response = json!({
"choices": [
{
"message": {
"tool_calls": [
{
"id": "call_789",
"function": {
"name": "test_tool",
"arguments": "{\"input\": \"test\"}"
}
}
]
}
}
]
});
let calls = parse_tool_calls(&response);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "test_tool");
}
#[test]
fn test_parse_first_tool_call() {
let response = json!({
"choices": [
{
"message": {
"tool_calls": [
{
"id": "call_first",
"function": {
"name": "first_tool",
"arguments": "{}"
}
},
{
"id": "call_second",
"function": {
"name": "second_tool",
"arguments": "{}"
}
}
]
}
}
]
});
let first = parse_first_tool_call(&response);
assert!(first.is_some());
assert_eq!(first.unwrap().name, "first_tool");
}
#[test]
fn test_parse_tool_calls_direct() {
let response = json!({
"tool_calls": [
{
"id": "call_direct",
"function": {
"name": "direct_tool",
"arguments": "{}"
}
}
]
});
let calls = parse_tool_calls(&response);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "direct_tool");
}
#[test]
fn test_parse_multiple_tool_calls() {
let message = json!({
"tool_calls": [
{
"id": "call_1",
"function": {
"name": "tool_a",
"arguments": "{\"x\": 1}"
}
},
{
"id": "call_2",
"function": {
"name": "tool_b",
"arguments": "{\"y\": 2}"
}
}
]
});
let calls = parse_tool_calls_from_message(&message);
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "tool_a");
assert_eq!(calls[1].name, "tool_b");
}
}