use super::handle_local::coerce_integer_like_tool_args;
pub(crate) fn build_assistant_tool_message(
text: &str,
tool_calls: &[serde_json::Value],
provider: &str,
model: &str,
) -> serde_json::Value {
let is_anthropic = super::super::provider::provider_uses_anthropic_messages(provider, model);
let is_gemini = super::super::provider::provider_uses_gemini_messages(provider, model);
let is_ollama = super::super::provider::provider_uses_ollama_messages(provider, model);
if is_anthropic {
let mut content = Vec::new();
if !text.is_empty() {
content.push(serde_json::json!({"type": "text", "text": text}));
}
for tc in tool_calls {
content.push(serde_json::json!({
"type": "tool_use",
"id": tc["id"],
"name": tc["name"],
"input": tc["arguments"],
}));
}
serde_json::json!({"role": "assistant", "content": content})
} else if is_gemini {
let mut parts = Vec::new();
if !text.is_empty() {
parts.push(serde_json::json!({"text": text}));
}
for tc in tool_calls {
let mut function_call = serde_json::json!({
"name": tc["name"],
"args": tc["arguments"],
});
if let Some(id) = tc.get("id").and_then(|value| value.as_str()) {
if !id.is_empty() {
function_call["id"] = serde_json::json!(id);
}
}
let mut part = serde_json::json!({ "functionCall": function_call });
if let Some(signature) = tc
.get("thought_signature")
.or_else(|| tc.get("thoughtSignature"))
.and_then(|value| value.as_str())
{
if !signature.is_empty() {
part["thoughtSignature"] = serde_json::json!(signature);
}
}
parts.push(part);
}
serde_json::json!({"role": "assistant", "content": parts})
} else if is_ollama {
let calls: Vec<serde_json::Value> = tool_calls
.iter()
.enumerate()
.map(|(idx, tc)| {
serde_json::json!({
"id": tc["id"],
"type": "function",
"function": {
"index": idx,
"name": tc["name"],
"arguments": serde_json::to_string(&tc["arguments"]).unwrap_or_default(),
}
})
})
.collect();
let mut msg = serde_json::json!({
"role": "assistant",
"tool_calls": calls,
});
if !text.is_empty() {
msg["content"] = serde_json::json!(text);
}
msg
} else {
let calls: Vec<serde_json::Value> = tool_calls
.iter()
.map(|tc| {
serde_json::json!({
"id": tc["id"],
"type": "function",
"function": {
"name": tc["name"],
"arguments": serde_json::to_string(&tc["arguments"]).unwrap_or_default(),
}
})
})
.collect();
serde_json::json!({
"role": "assistant",
"content": if text.is_empty() { serde_json::Value::String(String::new()) } else { serde_json::json!(text) },
"tool_calls": calls,
})
}
}
pub(crate) fn build_assistant_response_message(
text: &str,
blocks: &[serde_json::Value],
tool_calls: &[serde_json::Value],
reasoning: Option<&str>,
provider: &str,
model: &str,
) -> serde_json::Value {
let mut message = if !tool_calls.is_empty() {
if super::super::provider::provider_uses_gemini_messages(provider, model)
&& !blocks.is_empty()
{
let content =
crate::llm::content::gemini_parts(&serde_json::Value::Array(blocks.to_vec()));
if content
.iter()
.any(|part| part.get("functionCall").is_some())
{
serde_json::json!({"role": "assistant", "content": content})
} else {
build_assistant_tool_message(text, tool_calls, provider, model)
}
} else {
build_assistant_tool_message(text, tool_calls, provider, model)
}
} else if !blocks.is_empty() {
serde_json::json!({
"role": "assistant",
"content": blocks,
})
} else {
serde_json::json!({
"role": "assistant",
"content": text,
})
};
if let Some(reasoning) = reasoning.filter(|value| !value.is_empty()) {
message["reasoning"] = serde_json::json!(reasoning);
}
message
}
pub(crate) fn normalize_tool_args(name: &str, args: &serde_json::Value) -> serde_json::Value {
let mut obj = match args.as_object() {
Some(o) => o.clone(),
None => return args.clone(),
};
if let Some(annotations) = crate::orchestration::current_tool_annotations(name) {
for (alias, canonical) in &annotations.arg_schema.arg_aliases {
if obj.contains_key(canonical) {
continue;
}
if let Some(value) = obj.remove(alias) {
obj.insert(canonical.clone(), value);
}
}
}
let mut normalized = serde_json::Value::Object(obj);
coerce_integer_like_tool_args(&mut normalized);
normalized
}