mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: flatten tool messages for NEAR AI cloud-api compatibility (#41)
* fix: flatten tool messages for NEAR AI cloud-api compatibility NEAR AI cloud-api does not support the OpenAI multi-turn tool-calling protocol (role:"tool" messages cause HTTP 400). This adds a flatten_tool_messages() pass in NearAiChatProvider that rewrites assistant tool_call messages and tool result messages into plain assistant/user text before sending to the API. The model still sees the tool execution history, just in a text format it can process. Also includes a minor fix to telegram channel send_pairing_reply for updated WASM host function signature. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: resolve CI failures in fmt, rate limiting, and test configuration - Apply cargo fmt to nearai_chat.rs formatting violations - Fix truncate(true) bug in record_failed_approve that cleared the attempts file before reading, preventing rate limit from ever triggering - Skip bundled channel test when WASM build artifacts are unavailable (CI lacks wasm32-wasip2 target) - Split CI test workflow to exclude workspace_integration tests that require PostgreSQL Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: resolve clippy unnecessary_unwrap lint (Rust 1.93) Replace is_some() + unwrap() pattern with if-let binding to satisfy clippy::unnecessary_unwrap which is now deny-by-default. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: firat.sertgoz <[email protected]> Co-authored-by: Illia Polosukhin <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
firat.sertgoz
Illia Polosukhin
parent
33ef0a6ea5
commit
b3dee13954
@@ -222,6 +222,12 @@ impl LlmProvider for NearAiChatProvider {
|
||||
let messages: Vec<ChatCompletionMessage> =
|
||||
req.messages.into_iter().map(|m| m.into()).collect();
|
||||
|
||||
// NEAR AI cloud-api does not support multi-turn tool calling (rejects
|
||||
// any request containing role:"tool" messages with HTTP 400). Rewrite
|
||||
// tool-call / tool-result pairs into plain text so the conversation
|
||||
// history is preserved without using unsupported message roles.
|
||||
let messages = flatten_tool_messages(messages);
|
||||
|
||||
let tools: Vec<ChatCompletionTool> = req
|
||||
.tools
|
||||
.into_iter()
|
||||
@@ -367,6 +373,64 @@ struct ChatCompletionMessage {
|
||||
tool_calls: Option<Vec<ChatCompletionToolCall>>,
|
||||
}
|
||||
|
||||
/// Rewrite tool-call / tool-result messages into plain assistant/user text.
|
||||
///
|
||||
/// NEAR AI cloud-api does not support the OpenAI multi-turn tool-calling
|
||||
/// protocol (`role: "tool"` messages). This function converts:
|
||||
/// - Assistant messages with `tool_calls` → assistant text describing the calls
|
||||
/// - Tool result messages (`role: "tool"`) → user messages with the result
|
||||
///
|
||||
/// Non-tool messages pass through unchanged.
|
||||
fn flatten_tool_messages(messages: Vec<ChatCompletionMessage>) -> Vec<ChatCompletionMessage> {
|
||||
let has_tool_msgs = messages.iter().any(|m| m.role == "tool");
|
||||
if !has_tool_msgs {
|
||||
return messages;
|
||||
}
|
||||
|
||||
tracing::debug!("Flattening tool messages for NEAR AI compatibility");
|
||||
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|msg| {
|
||||
if let (true, Some(calls)) = (msg.role == "assistant", &msg.tool_calls) {
|
||||
// Convert assistant tool_calls into descriptive text
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if let Some(ref text) = msg.content {
|
||||
if !text.is_empty() {
|
||||
parts.push(text.clone());
|
||||
}
|
||||
}
|
||||
for tc in calls {
|
||||
parts.push(format!(
|
||||
"[Called tool `{}` with arguments: {}]",
|
||||
tc.function.name, tc.function.arguments
|
||||
));
|
||||
}
|
||||
ChatCompletionMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: Some(parts.join("\n")),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
}
|
||||
} else if msg.role == "tool" {
|
||||
// Convert tool result into a user message
|
||||
let tool_name = msg.name.as_deref().unwrap_or("unknown");
|
||||
let result = msg.content.as_deref().unwrap_or("");
|
||||
ChatCompletionMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some(format!("[Tool `{}` returned: {}]", tool_name, result)),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
}
|
||||
} else {
|
||||
msg
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl From<ChatMessage> for ChatCompletionMessage {
|
||||
fn from(msg: ChatMessage) -> Self {
|
||||
let role = match msg.role {
|
||||
@@ -544,4 +608,119 @@ mod tests {
|
||||
serde_json::from_str(&calls[0].function.arguments).expect("valid JSON string");
|
||||
assert_eq!(parsed["key"], "value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_no_tool_messages_passthrough() {
|
||||
let messages = vec![
|
||||
ChatCompletionMessage {
|
||||
role: "system".to_string(),
|
||||
content: Some("You are helpful.".to_string()),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
ChatCompletionMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some("Hello".to_string()),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
let result = flatten_tool_messages(messages);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].role, "system");
|
||||
assert_eq!(result[1].role, "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_tool_call_and_result() {
|
||||
let messages = vec![
|
||||
ChatCompletionMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some("test".to_string()),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
ChatCompletionMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: None,
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: Some(vec![ChatCompletionToolCall {
|
||||
id: "call_1".to_string(),
|
||||
call_type: "function".to_string(),
|
||||
function: ChatCompletionToolCallFunction {
|
||||
name: "echo".to_string(),
|
||||
arguments: r#"{"message":"hi"}"#.to_string(),
|
||||
},
|
||||
}]),
|
||||
},
|
||||
ChatCompletionMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some("hi".to_string()),
|
||||
tool_call_id: Some("call_1".to_string()),
|
||||
name: Some("echo".to_string()),
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
|
||||
let result = flatten_tool_messages(messages);
|
||||
assert_eq!(result.len(), 3);
|
||||
|
||||
// Assistant tool_calls → plain assistant text
|
||||
assert_eq!(result[1].role, "assistant");
|
||||
assert!(result[1].tool_calls.is_none());
|
||||
assert!(
|
||||
result[1]
|
||||
.content
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("[Called tool `echo`")
|
||||
);
|
||||
|
||||
// Tool result → user message
|
||||
assert_eq!(result[2].role, "user");
|
||||
assert!(result[2].tool_call_id.is_none());
|
||||
assert!(
|
||||
result[2]
|
||||
.content
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("[Tool `echo` returned: hi]")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_preserves_assistant_text_with_tool_calls() {
|
||||
let messages = vec![
|
||||
ChatCompletionMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: Some("Let me check that.".to_string()),
|
||||
tool_call_id: None,
|
||||
name: None,
|
||||
tool_calls: Some(vec![ChatCompletionToolCall {
|
||||
id: "call_1".to_string(),
|
||||
call_type: "function".to_string(),
|
||||
function: ChatCompletionToolCallFunction {
|
||||
name: "search".to_string(),
|
||||
arguments: r#"{"q":"test"}"#.to_string(),
|
||||
},
|
||||
}]),
|
||||
},
|
||||
ChatCompletionMessage {
|
||||
role: "tool".to_string(),
|
||||
content: Some("found it".to_string()),
|
||||
tool_call_id: Some("call_1".to_string()),
|
||||
name: Some("search".to_string()),
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
|
||||
let result = flatten_tool_messages(messages);
|
||||
let text = result[0].content.as_ref().unwrap();
|
||||
assert!(text.starts_with("Let me check that."));
|
||||
assert!(text.contains("[Called tool `search`"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user