alexhancock/mcp-crate-cleanup (#4885)

This commit is contained in:
Alex Hancock
2025-09-29 17:00:17 -04:00
committed by GitHub
parent 2bd18a757b
commit cb0eca9966
78 changed files with 1039 additions and 1090 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ impl CursorAgentProvider {
MessageContent::ToolRequest(tool_request) => {
if let Ok(tool_call) = &tool_request.tool_call {
full_prompt.push_str(&format!(
"Tool Use: {} with args: {}\n",
"Tool Use: {} with args: {:?}\n",
tool_call.name, tool_call.arguments
));
}
@@ -3,8 +3,7 @@ use crate::model::ModelConfig;
use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use anyhow::{anyhow, Result};
use mcp_core::ToolCall;
use rmcp::model::{ErrorCode, ErrorData, Role, Tool};
use rmcp::model::{object, CallToolRequestParam, ErrorCode, ErrorData, Role, Tool};
use serde_json::{json, Value};
use std::collections::HashSet;
@@ -230,19 +229,24 @@ pub fn response_to_message(response: &Value) -> Result<Message> {
let name = block
.get(NAME_FIELD)
.and_then(|n| n.as_str())
.ok_or_else(|| anyhow!("Missing tool_use name"))?;
.ok_or_else(|| anyhow!("Missing tool_use name"))?
.to_string();
let input = block
.get(INPUT_FIELD)
.ok_or_else(|| anyhow!("Missing tool_use input"))?;
let tool_call = ToolCall::new(name, input.clone());
let tool_call = CallToolRequestParam {
name: name.into(),
arguments: Some(object(input.clone())),
};
message = message.with_tool_request(id, Ok(tool_call));
}
Some(THINKING_TYPE) => {
let thinking = block
.get(THINKING_TYPE)
.and_then(|t| t.as_str())
.ok_or_else(|| anyhow!("Missing thinking content"))?;
.ok_or_else(|| anyhow!("Missing thinking content"))?
.to_string();
let signature = block
.get(SIGNATURE_FIELD)
.and_then(|s| s.as_str())
@@ -589,7 +593,8 @@ where
}
};
let tool_call = ToolCall::new(&name, parsed_args);
let tool_call = CallToolRequestParam{ name: name.into(), arguments: Some(object(parsed_args)) };
let mut message = Message::new(
rmcp::model::Role::Assistant,
chrono::Utc::now().timestamp(),
@@ -750,7 +755,7 @@ mod tests {
if let MessageContent::ToolRequest(tool_request) = &message.content[0] {
let tool_call = tool_request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "calculator");
assert_eq!(tool_call.arguments, json!({"expression": "2 + 2"}));
assert_eq!(tool_call.arguments, Some(object!({"expression": "2 + 2"})));
} else {
panic!("Expected ToolRequest content");
}
@@ -992,7 +997,10 @@ mod tests {
let messages = vec![
Message::assistant().with_tool_request(
"tool_1",
Ok(ToolCall::new("calculator", json!({"expression": "2 + 2"}))),
Ok(CallToolRequestParam {
name: "calculator".into(),
arguments: Some(object!({"expression": "2 + 2"})),
}),
),
Message::user().with_tool_response(
"tool_1",
+11 -8
View File
@@ -2,13 +2,16 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use crate::mcp_utils::ToolResult;
use anyhow::{anyhow, bail, Result};
use aws_sdk_bedrockruntime::types as bedrock;
use aws_smithy_types::{Document, Number};
use base64::Engine;
use chrono::Utc;
use mcp_core::{ToolCall, ToolResult};
use rmcp::model::{Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool};
use rmcp::model::{
object, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent, ResourceContents,
Role, Tool,
};
use serde_json::Value;
use super::super::base::Usage;
@@ -57,7 +60,7 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
bedrock::ToolUseBlock::builder()
.tool_use_id(tool_use_id)
.name(call.name.to_string())
.input(to_bedrock_json(&call.arguments))
.input(to_bedrock_json(&Value::from(call.arguments.clone())))
.build()
} else {
bedrock::ToolUseBlock::builder()
@@ -72,7 +75,7 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
bedrock::ToolUseBlock::builder()
.tool_use_id(tool_use_id)
.name(call.name.to_string())
.input(to_bedrock_json(&call.arguments))
.input(to_bedrock_json(&Value::from(call.arguments.clone())))
.build()
} else {
bedrock::ToolUseBlock::builder()
@@ -282,10 +285,10 @@ pub fn from_bedrock_content_block(block: &bedrock::ContentBlock) -> Result<Messa
bedrock::ContentBlock::Text(text) => MessageContent::text(text),
bedrock::ContentBlock::ToolUse(tool_use) => MessageContent::tool_request(
tool_use.tool_use_id.to_string(),
Ok(ToolCall::new(
tool_use.name.to_string(),
from_bedrock_json(&tool_use.input)?,
)),
Ok(CallToolRequestParam {
name: tool_use.name.clone().into(),
arguments: Some(object(from_bedrock_json(&tool_use.input.clone())?)),
}),
),
bedrock::ContentBlock::ToolResult(tool_res) => MessageContent::tool_response(
tool_res.tool_use_id.to_string(),
@@ -5,9 +5,9 @@ use crate::providers::utils::{
sanitize_function_name, ImageFormat,
};
use anyhow::{anyhow, Error};
use mcp_core::ToolCall;
use rmcp::model::{
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
object, AnnotateAble, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent,
ResourceContents, Role, Tool,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -102,6 +102,12 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
match &request.tool_call {
Ok(tool_call) => {
let sanitized_name = sanitize_function_name(&tool_call.name);
let arguments_str = match &tool_call.arguments {
Some(args) => {
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
}
None => "{}".to_string(),
};
let tool_calls = converted.tool_calls.get_or_insert_default();
tool_calls.push(json!({
@@ -109,7 +115,7 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
"type": "function",
"function": {
"name": sanitized_name,
"arguments": tool_call.arguments.to_string(),
"arguments": arguments_str,
}
}));
}
@@ -286,6 +292,7 @@ pub fn format_tools(tools: &[Tool]) -> anyhow::Result<Vec<Value>> {
}
/// Convert Databricks' API response to internal Message format
#[allow(clippy::too_many_lines)]
pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
let original = &response["choices"][0]["message"];
let mut content = Vec::new();
@@ -373,7 +380,10 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
Ok(params) => {
content.push(MessageContent::tool_request(
id,
Ok(ToolCall::new(&function_name, params)),
Ok(CallToolRequestParam {
name: function_name.into(),
arguments: Some(object(params)),
}),
));
}
Err(e) => {
@@ -771,7 +781,10 @@ mod tests {
Message::user().with_text("How are you?"),
Message::assistant().with_tool_request(
"tool1",
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
Ok(CallToolRequestParam {
name: "example".into(),
arguments: Some(object!({"param1": "value1"})),
}),
),
];
@@ -807,7 +820,10 @@ mod tests {
fn test_format_messages_multiple_content() -> anyhow::Result<()> {
let mut messages = vec![Message::assistant().with_tool_request(
"tool1",
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
Ok(CallToolRequestParam {
name: "example".into(),
arguments: Some(object!({"param1": "value1"})),
}),
)];
// Get the ID from the tool request to use in the response
@@ -956,7 +972,7 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
let tool_call = request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "example_fn");
assert_eq!(tool_call.arguments, json!({"param": "value"}));
assert_eq!(tool_call.arguments, Some(object!({"param": "value"})));
} else {
panic!("Expected ToolRequest content");
}
@@ -1027,7 +1043,7 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
let tool_call = request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "example_fn");
assert_eq!(tool_call.arguments, json!({}));
assert_eq!(tool_call.arguments, Some(object!({})));
} else {
panic!("Expected ToolRequest content");
}
@@ -1226,4 +1242,65 @@ mod tests {
Ok(())
}
#[test]
fn test_format_messages_tool_request_with_none_arguments() -> anyhow::Result<()> {
// Test that tool calls with None arguments are formatted as "{}" string
let message = Message::assistant().with_tool_request(
"tool1",
Ok(CallToolRequestParam {
name: "test_tool".into(),
arguments: None, // This is the key case the fix addresses
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
let as_value = serde_json::to_value(spec)?;
let spec_array = as_value.as_array().unwrap();
assert_eq!(spec_array.len(), 1);
assert_eq!(spec_array[0]["role"], "assistant");
assert!(spec_array[0]["tool_calls"].is_array());
let tool_call = &spec_array[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "test_tool");
// This should be the string "{}", not null
assert_eq!(tool_call["function"]["arguments"], "{}");
Ok(())
}
#[test]
fn test_format_messages_tool_request_with_some_arguments() -> anyhow::Result<()> {
// Test that tool calls with Some arguments are properly JSON-serialized
let message = Message::assistant().with_tool_request(
"tool1",
Ok(CallToolRequestParam {
name: "test_tool".into(),
arguments: Some(object!({"param": "value", "number": 42})),
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
let as_value = serde_json::to_value(spec)?;
let spec_array = as_value.as_array().unwrap();
assert_eq!(spec_array.len(), 1);
assert_eq!(spec_array[0]["role"], "assistant");
assert!(spec_array[0]["tool_calls"].is_array());
let tool_call = &spec_array[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "test_tool");
// This should be a JSON string representation
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
let parsed_args: Value = serde_json::from_str(args_str)?;
assert_eq!(parsed_args["param"], "value");
assert_eq!(parsed_args["number"], 42);
Ok(())
}
}
+38 -15
View File
@@ -3,9 +3,10 @@ use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use crate::providers::utils::{is_valid_function_name, sanitize_function_name};
use anyhow::Result;
use mcp_core::ToolCall;
use rand::{distributions::Alphanumeric, Rng};
use rmcp::model::{AnnotateAble, ErrorCode, ErrorData, RawContent, Role, Tool};
use rmcp::model::{
object, AnnotateAble, CallToolRequestParam, ErrorCode, ErrorData, RawContent, Role, Tool,
};
use std::borrow::Cow;
use crate::conversation::message::{Message, MessageContent};
@@ -43,12 +44,14 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
"name".to_string(),
json!(sanitize_function_name(&tool_call.name)),
);
if tool_call.arguments.is_object()
&& !tool_call.arguments.as_object().unwrap().is_empty()
{
function_call_part
.insert("args".to_string(), tool_call.arguments.clone());
if let Some(args) = &tool_call.arguments {
if !args.is_empty() {
function_call_part
.insert("args".to_string(), args.clone().into());
}
}
parts.push(json!({
"functionCall": function_call_part
}));
@@ -269,7 +272,10 @@ pub fn response_to_message(response: Value) -> Result<Message> {
if let Some(params) = parameters {
content.push(MessageContent::tool_request(
id,
Ok(ToolCall::new(&name, params.clone())),
Ok(CallToolRequestParam {
name: name.into(),
arguments: Some(object(params.clone())),
}),
));
}
}
@@ -341,6 +347,7 @@ pub fn create_request(
mod tests {
use super::*;
use crate::conversation::message::Message;
use rmcp::model::CallToolRequestParam;
use rmcp::{model::Content, object};
use serde_json::json;
@@ -348,7 +355,7 @@ mod tests {
Message::new(role, 0, vec![MessageContent::text(text.to_string())])
}
fn set_up_tool_request_message(id: &str, tool_call: ToolCall) -> Message {
fn set_up_tool_request_message(id: &str, tool_call: CallToolRequestParam) -> Message {
Message::new(
Role::User,
0,
@@ -356,14 +363,14 @@ mod tests {
)
}
fn set_up_tool_confirmation_message(id: &str, tool_call: ToolCall) -> Message {
fn set_up_tool_confirmation_message(id: &str, tool_call: CallToolRequestParam) -> Message {
Message::new(
Role::User,
0,
vec![MessageContent::tool_confirmation_request(
id.to_string(),
tool_call.name.clone(),
tool_call.arguments.clone(),
tool_call.name.to_string().clone(),
tool_call.arguments.unwrap_or_default().clone(),
Some("goose would like to call the above tool. Allow? (y/n):".to_string()),
)],
)
@@ -415,10 +422,19 @@ mod tests {
"param1": "value1"
});
let messages = vec![
set_up_tool_request_message("id", ToolCall::new("tool_name", arguments.clone())),
set_up_tool_request_message(
"id",
CallToolRequestParam {
name: "tool_name".into(),
arguments: Some(object(arguments.clone())),
},
),
set_up_tool_confirmation_message(
"id2",
ToolCall::new("tool_name_2", arguments.clone()),
CallToolRequestParam {
name: "tool_name_2".into(),
arguments: Some(object(arguments.clone())),
},
),
];
let payload = format_messages(&messages);
@@ -780,7 +796,14 @@ mod tests {
assert_eq!(message.content.len(), 1);
if let Ok(tool_call) = &message.content[0].as_tool_request().unwrap().tool_call {
assert_eq!(tool_call.name, "valid_name");
assert_eq!(tool_call.arguments["param"], "value");
assert_eq!(
tool_call
.arguments
.as_ref()
.and_then(|args| args.get("param"))
.and_then(|v| v.as_str()),
Some("value")
);
} else {
panic!("Expected valid tool request");
}
+147 -10
View File
@@ -8,9 +8,9 @@ use crate::providers::utils::{
use anyhow::{anyhow, Error};
use async_stream::try_stream;
use futures::Stream;
use mcp_core::ToolCall;
use rmcp::model::{
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
object, AnnotateAble, CallToolRequestParam, Content, ErrorCode, ErrorData, RawContent,
ResourceContents, Role, Tool,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -104,6 +104,13 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
MessageContent::ToolRequest(request) => match &request.tool_call {
Ok(tool_call) => {
let sanitized_name = sanitize_function_name(&tool_call.name);
let arguments_str = match &tool_call.arguments {
Some(args) => {
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
}
None => "{}".to_string(),
};
let tool_calls = converted
.as_object_mut()
.unwrap()
@@ -115,7 +122,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
"type": "function",
"function": {
"name": sanitized_name,
"arguments": tool_call.arguments.to_string(),
"arguments": arguments_str,
}
}));
}
@@ -209,6 +216,13 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
MessageContent::FrontendToolRequest(request) => match &request.tool_call {
Ok(tool_call) => {
let sanitized_name = sanitize_function_name(&tool_call.name);
let arguments_str = match &tool_call.arguments {
Some(args) => {
serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string())
}
None => "{}".to_string(),
};
let tool_calls = converted
.as_object_mut()
.unwrap()
@@ -220,7 +234,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
"type": "function",
"function": {
"name": sanitized_name,
"arguments": tool_call.arguments.to_string(),
"arguments": arguments_str,
}
}));
}
@@ -316,7 +330,10 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
Ok(params) => {
content.push(MessageContent::tool_request(
id,
Ok(ToolCall::new(&function_name, params)),
Ok(CallToolRequestParam {
name: function_name.into(),
arguments: Some(object(params)),
}),
));
}
Err(e) => {
@@ -515,7 +532,7 @@ where
Ok(params) => {
MessageContent::tool_request(
id.clone(),
Ok(ToolCall::new(function_name.clone(), params)),
Ok(CallToolRequestParam { name: function_name.clone().into(), arguments: Some(object(params)) }),
)
},
Err(e) => {
@@ -821,7 +838,10 @@ mod tests {
Message::user().with_text("How are you?"),
Message::assistant().with_tool_request(
"tool1",
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
Ok(CallToolRequestParam {
name: "example".into(),
arguments: Some(object!({"param1": "value1"})),
}),
),
];
@@ -855,7 +875,10 @@ mod tests {
fn test_format_messages_multiple_content() -> anyhow::Result<()> {
let mut messages = vec![Message::assistant().with_tool_request(
"tool1",
Ok(ToolCall::new("example", json!({"param1": "value1"}))),
Ok(CallToolRequestParam {
name: "example".into(),
arguments: Some(object!({"param1": "value1"})),
}),
)];
// Get the ID from the tool request to use in the response
@@ -1000,7 +1023,7 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
let tool_call = request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "example_fn");
assert_eq!(tool_call.arguments, json!({"param": "value"}));
assert_eq!(tool_call.arguments, Some(object!({"param": "value"})));
} else {
panic!("Expected ToolRequest content");
}
@@ -1071,7 +1094,7 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
let tool_call = request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "example_fn");
assert_eq!(tool_call.arguments, json!({}));
assert_eq!(tool_call.arguments, Some(object!({})));
} else {
panic!("Expected ToolRequest content");
}
@@ -1079,6 +1102,120 @@ mod tests {
Ok(())
}
#[test]
fn test_format_messages_tool_request_with_none_arguments() -> anyhow::Result<()> {
// Test that tool calls with None arguments are formatted as "{}" string
let message = Message::assistant().with_tool_request(
"tool1",
Ok(CallToolRequestParam {
name: "test_tool".into(),
arguments: None, // This is the key case the fix addresses
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
assert_eq!(spec.len(), 1);
assert_eq!(spec[0]["role"], "assistant");
assert!(spec[0]["tool_calls"].is_array());
let tool_call = &spec[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "test_tool");
// This should be the string "{}", not null
assert_eq!(tool_call["function"]["arguments"], "{}");
Ok(())
}
#[test]
fn test_format_messages_tool_request_with_some_arguments() -> anyhow::Result<()> {
// Test that tool calls with Some arguments are properly JSON-serialized
let message = Message::assistant().with_tool_request(
"tool1",
Ok(CallToolRequestParam {
name: "test_tool".into(),
arguments: Some(object!({"param": "value", "number": 42})),
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
assert_eq!(spec.len(), 1);
assert_eq!(spec[0]["role"], "assistant");
assert!(spec[0]["tool_calls"].is_array());
let tool_call = &spec[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "test_tool");
// This should be a JSON string representation
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
let parsed_args: Value = serde_json::from_str(args_str)?;
assert_eq!(parsed_args["param"], "value");
assert_eq!(parsed_args["number"], 42);
Ok(())
}
#[test]
fn test_format_messages_frontend_tool_request_with_none_arguments() -> anyhow::Result<()> {
// Test that FrontendToolRequest with None arguments are formatted as "{}" string
let message = Message::assistant().with_frontend_tool_request(
"frontend_tool1",
Ok(CallToolRequestParam {
name: "frontend_test_tool".into(),
arguments: None, // This is the key case the fix addresses
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
assert_eq!(spec.len(), 1);
assert_eq!(spec[0]["role"], "assistant");
assert!(spec[0]["tool_calls"].is_array());
let tool_call = &spec[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "frontend_tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "frontend_test_tool");
// This should be the string "{}", not null
assert_eq!(tool_call["function"]["arguments"], "{}");
Ok(())
}
#[test]
fn test_format_messages_frontend_tool_request_with_some_arguments() -> anyhow::Result<()> {
// Test that FrontendToolRequest with Some arguments are properly JSON-serialized
let message = Message::assistant().with_frontend_tool_request(
"frontend_tool1",
Ok(CallToolRequestParam {
name: "frontend_test_tool".into(),
arguments: Some(object!({"action": "click", "element": "button"})),
}),
);
let spec = format_messages(&[message], &ImageFormat::OpenAi);
assert_eq!(spec.len(), 1);
assert_eq!(spec[0]["role"], "assistant");
assert!(spec[0]["tool_calls"].is_array());
let tool_call = &spec[0]["tool_calls"][0];
assert_eq!(tool_call["id"], "frontend_tool1");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "frontend_test_tool");
// This should be a JSON string representation
let args_str = tool_call["function"]["arguments"].as_str().unwrap();
let parsed_args: Value = serde_json::from_str(args_str)?;
assert_eq!(parsed_args["action"], "click");
assert_eq!(parsed_args["element"], "button");
Ok(())
}
#[test]
fn test_create_request_gpt_4o() -> anyhow::Result<()> {
// Test default medium reasoning effort for O3 model
+26 -14
View File
@@ -3,8 +3,8 @@ use crate::model::ModelConfig;
use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use anyhow::{anyhow, Result};
use mcp_core::tool::ToolCall;
use rmcp::model::{Role, Tool};
use rmcp::model::{object, CallToolRequestParam, Role, Tool};
use rmcp::object;
use serde_json::{json, Value};
use std::collections::HashSet;
@@ -181,16 +181,22 @@ pub fn parse_streaming_response(sse_data: &str) -> Result<Message> {
}
// Add tool use if complete
if let (Some(id), Some(name)) = (&tool_use_id, &tool_name) {
if let Some((id, name)) = tool_use_id.zip(tool_name) {
if !tool_input.is_empty() {
let input_value = serde_json::from_str::<Value>(&tool_input)
.unwrap_or_else(|_| Value::String(tool_input.clone()));
let tool_call = ToolCall::new(name, input_value);
message = message.with_tool_request(id, Ok(tool_call));
} else if tool_name.is_some() {
let tool_call = CallToolRequestParam {
name: name.into(),
arguments: Some(object(input_value)),
};
message = message.with_tool_request(&id, Ok(tool_call));
} else {
// Tool with no input - use empty object
let tool_call = ToolCall::new(name, Value::Object(serde_json::Map::new()));
message = message.with_tool_request(id, Ok(tool_call));
let tool_call = CallToolRequestParam {
name: name.into(),
arguments: Some(object!({})),
};
message = message.with_tool_request(&id, Ok(tool_call));
}
}
@@ -238,14 +244,18 @@ pub fn response_to_message(response: &Value) -> Result<Message> {
let name = content
.get("name")
.and_then(|n| n.as_str())
.ok_or_else(|| anyhow!("Missing tool_use name"))?;
.ok_or_else(|| anyhow!("Missing tool_use name"))?
.to_string();
let input = content
.get("input")
.ok_or_else(|| anyhow!("Missing tool input"))?
.clone();
let tool_call = ToolCall::new(name, input);
let tool_call = CallToolRequestParam {
name: name.into(),
arguments: Some(object(input)),
};
message = message.with_tool_request(id, Ok(tool_call));
}
Some("thinking") => {
@@ -425,7 +435,7 @@ mod tests {
if let MessageContent::ToolRequest(tool_request) = &message.content[0] {
let tool_call = tool_request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "calculator");
assert_eq!(tool_call.arguments, json!({"expression": "2 + 2"}));
assert_eq!(tool_call.arguments, Some(object!({"expression": "2 + 2"})));
} else {
panic!("Expected ToolRequest content");
}
@@ -536,7 +546,7 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025
if let MessageContent::ToolRequest(tool_request) = &message.content[1] {
let tool_call = tool_request.tool_call.as_ref().unwrap();
assert_eq!(tool_call.name, "get_stock_price");
assert_eq!(tool_call.arguments, json!({"symbol": "NVDA"}));
assert_eq!(tool_call.arguments, Some(object!({"symbol": "NVDA"})));
assert_eq!(tool_request.id, "tooluse_FB_nOElDTAOKa-YnVWI5Uw");
} else {
panic!("Expected ToolRequest content second");
@@ -679,10 +689,12 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025
#[test]
fn test_message_formatting_skips_tool_requests() {
use crate::conversation::message::Message;
use mcp_core::tool::ToolCall;
// Create a conversation with text, tool requests, and tool responses
let tool_call = ToolCall::new("calculator", json!({"expression": "2 + 2"}));
let tool_call = CallToolRequestParam {
name: "calculator".into(),
arguments: Some(object!({"expression": "2 + 2"})),
};
let messages = vec![
Message::user().with_text("Calculate 2 + 2"),
+10 -7
View File
@@ -38,9 +38,8 @@ use crate::conversation::Conversation;
use crate::model::ModelConfig;
use crate::providers::formats::openai::create_request;
use anyhow::Result;
use mcp_core::tool::ToolCall;
use reqwest::Client;
use rmcp::model::{RawContent, Tool};
use rmcp::model::{object, CallToolRequestParam, RawContent, Tool};
use serde_json::{json, Value};
use std::ops::Deref;
use std::time::Duration;
@@ -60,7 +59,7 @@ pub trait ToolInterpreter {
&self,
content: &str,
tools: &[Tool],
) -> Result<Vec<ToolCall>, ProviderError>;
) -> Result<Vec<CallToolRequestParam>, ProviderError>;
}
/// Ollama-specific implementation of the ToolInterpreter trait
@@ -198,7 +197,9 @@ impl OllamaInterpreter {
Ok(response_json)
}
fn process_interpreter_response(response: &Value) -> Result<Vec<ToolCall>, ProviderError> {
fn process_interpreter_response(
response: &Value,
) -> Result<Vec<CallToolRequestParam>, ProviderError> {
let mut tool_calls = Vec::new();
tracing::info!(
"Tool interpreter response is {}",
@@ -219,12 +220,14 @@ impl OllamaInterpreter {
&& item.get("name").is_some()
&& item.get("arguments").is_some()
{
// Create ToolCall directly from the JSON data
let name = item["name"].as_str().unwrap_or_default().to_string();
let arguments = item["arguments"].clone();
// Add the tool call to our result vector
tool_calls.push(ToolCall::new(name, arguments));
tool_calls.push(CallToolRequestParam {
name: name.into(),
arguments: Some(object(arguments)),
});
}
}
}
@@ -242,7 +245,7 @@ impl ToolInterpreter for OllamaInterpreter {
&self,
last_assistant_msg: &str,
tools: &[Tool],
) -> Result<Vec<ToolCall>, ProviderError> {
) -> Result<Vec<CallToolRequestParam>, ProviderError> {
if tools.is_empty() {
return Ok(vec![]);
}
+15 -6
View File
@@ -11,9 +11,9 @@ use super::retry::ProviderRetry;
use super::utils::map_http_error_to_provider_error;
use crate::conversation::message::{Message, MessageContent};
use crate::impl_provider_default;
use crate::mcp_utils::ToolResult;
use crate::model::ModelConfig;
use mcp_core::{ToolCall, ToolResult};
use rmcp::model::{Role, Tool};
use rmcp::model::{object, CallToolRequestParam, Role, Tool};
// ---------- Capability Flags ----------
#[derive(Debug)]
@@ -339,12 +339,19 @@ impl Provider for VeniceProvider {
.iter()
.filter_map(|tr| {
if let ToolResult::Ok(tool_call) = &tr.tool_call {
// Safely convert arguments to a JSON string
let args_str = tool_call
.arguments
.as_ref() // borrow the Option contents
.map(|map| serde_json::to_string(map).unwrap_or_default())
.unwrap_or_default();
// Log tool call details for debugging
tracing::debug!(
"Tool call conversion: id={}, name={}, args_len={}",
tr.id,
tool_call.name,
tool_call.arguments.to_string().len()
args_str.len()
);
// Convert to Venice format
@@ -353,7 +360,7 @@ impl Provider for VeniceProvider {
"type": "function",
"function": {
"name": tool_call.name,
"arguments": tool_call.arguments.to_string()
"arguments": args_str
}
}))
} else {
@@ -453,8 +460,10 @@ impl Provider for VeniceProvider {
function["arguments"].clone()
};
// Create a ToolCall using the function name and arguments
let tool_call = ToolCall { name, arguments };
let tool_call = CallToolRequestParam {
name: name.into(),
arguments: Some(object(arguments)),
};
// Create a ToolRequest MessageContent
let tool_request = MessageContent::tool_request(id, ToolResult::Ok(tool_call));