use super::types::{ChatMessage, FunctionCall, FunctionDefinition, OaiEvent, ToolCall, ToolDefinition};
use crate::runtime::types::{LlmEvent, SessionEvent, StreamEvent};
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ToolNameMap {
original_to_oai: HashMap<String, String>,
oai_to_original: HashMap<String, String>,
}
impl ToolNameMap {
pub fn to_oai<'a>(&'a self, name: &'a str) -> &'a str {
self.original_to_oai.get(name).map(String::as_str).unwrap_or(name)
}
pub fn to_original<'a>(&'a self, name: &'a str) -> &'a str {
self.oai_to_original.get(name).map(String::as_str).unwrap_or(name)
}
fn insert(&mut self, original: &str, oai: &str) {
if original != oai {
self.original_to_oai.insert(original.to_string(), oai.to_string());
self.oai_to_original.insert(oai.to_string(), original.to_string());
}
}
}
fn sanitize_oai_tool_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for ch in name.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
"tool".to_string()
} else {
if out.len() > 128 {
out.truncate(128);
}
out
}
}
pub fn tools_to_oai(schema: &[Value]) -> (Vec<ToolDefinition>, ToolNameMap) {
let mut name_map = ToolNameMap::default();
let mut used_names: HashMap<String, String> = HashMap::new();
let tools = schema
.iter()
.filter_map(|t| {
let name = t.get("name")?.as_str()?.to_string();
if name.is_empty() || name == "respond" || name == "send_channel" || name == "watcher_exit" {
return None;
}
let mut oai_name = sanitize_oai_tool_name(&name);
if let Some(existing) = used_names.get(&oai_name) {
if existing != &name {
let max_base = 128_usize.saturating_sub(4);
let base = if oai_name.len() > max_base {
oai_name[..max_base].to_string()
} else {
oai_name.clone()
};
let mut suffix = 2;
while used_names.contains_key(&oai_name) {
oai_name = format!("{base}_{suffix}");
suffix += 1;
}
}
}
used_names.insert(oai_name.clone(), name.clone());
name_map.insert(&name, &oai_name);
let description = t
.get("description")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
let parameters = t
.get("input_schema")
.cloned()
.unwrap_or_else(|| json!({"type": "object", "properties": {}}));
Some(ToolDefinition {
kind: "function".to_string(),
function: FunctionDefinition {
name: oai_name,
description,
parameters,
},
})
})
.collect();
(tools, name_map)
}
pub fn messages_to_oai(
anthropic_messages: &[Value],
system_prompt: &Option<String>,
name_map: &ToolNameMap,
) -> Vec<ChatMessage> {
let mut out: Vec<ChatMessage> = Vec::new();
if let Some(sp) = system_prompt.as_ref() {
if !sp.is_empty() {
out.push(ChatMessage::system(sp.clone()));
}
}
let mut tool_name_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for msg in anthropic_messages {
if msg.get("role").and_then(|r| r.as_str()) == Some("assistant") {
if let Some(Value::Array(blocks)) = msg.get("content") {
for block in blocks {
if block.get("type").and_then(|t| t.as_str()) == Some("tool_use") {
if let (Some(id), Some(name)) = (
block.get("id").and_then(|v| v.as_str()),
block.get("name").and_then(|v| v.as_str()),
) {
tool_name_map.insert(id.to_string(), name_map.to_oai(name).to_string());
}
}
}
}
}
}
for msg in anthropic_messages {
let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("user");
let content = msg.get("content");
match role {
"user" => {
match content {
Some(Value::String(s)) => out.push(ChatMessage::user(s.clone())),
Some(Value::Array(blocks)) => {
let mut text_buf = String::new();
for block in blocks {
let btype = block.get("type").and_then(|t| t.as_str()).unwrap_or("");
match btype {
"text" => {
if let Some(t) = block.get("text").and_then(|t| t.as_str()) {
text_buf.push_str(t);
}
}
"tool_result" => {
if !text_buf.is_empty() {
out.push(ChatMessage::user(std::mem::take(&mut text_buf)));
}
let tool_id = block
.get("tool_use_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let result_content = match block.get("content") {
Some(Value::String(s)) => s.clone(),
Some(Value::Array(arr)) => arr
.iter()
.filter_map(|b| {
b.get("text").and_then(|t| t.as_str()).map(String::from)
})
.collect::<Vec<_>>()
.join(""),
Some(other) => other.to_string(),
None => String::new(),
};
let tool_name = tool_name_map.get(&tool_id).cloned().unwrap_or_default();
out.push(ChatMessage::tool_result(tool_id, tool_name, result_content));
}
_ => {}
}
}
if !text_buf.is_empty() {
out.push(ChatMessage::user(text_buf));
}
}
_ => {}
}
}
"assistant" => {
let mut text_buf = String::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
if let Some(Value::Array(blocks)) = content {
for block in blocks {
let btype = block.get("type").and_then(|t| t.as_str()).unwrap_or("");
match btype {
"text" => {
if let Some(t) = block.get("text").and_then(|t| t.as_str()) {
text_buf.push_str(t);
}
}
"tool_use" => {
let id = block
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = block
.get("name")
.and_then(|v| v.as_str())
.map(|n| name_map.to_oai(n).to_string())
.unwrap_or_default();
let arguments = block
.get("input")
.map(|v| v.to_string())
.unwrap_or_else(|| "{}".to_string());
tool_calls.push(ToolCall {
id,
kind: "function".to_string(),
function: FunctionCall { name, arguments },
});
}
"thinking" => {
}
_ => {}
}
}
} else if let Some(Value::String(s)) = content {
text_buf.push_str(s);
}
let has_text = !text_buf.is_empty();
let has_tools = !tool_calls.is_empty();
match (has_text, has_tools) {
(true, false) => out.push(ChatMessage::assistant(text_buf)),
(false, true) => out.push(ChatMessage::assistant_tool_calls(tool_calls)),
(true, true) => out.push(ChatMessage {
role: "assistant".into(),
content: Some(text_buf),
tool_calls: Some(tool_calls),
tool_call_id: None,
name: None,
}),
(false, false) => {}
}
}
_ => {}
}
}
let mut fixed = Vec::with_capacity(out.len());
for msg in out {
if msg.role == "user" && fixed.last().map(|m: &ChatMessage| m.role == "tool").unwrap_or(false) {
fixed.push(ChatMessage::assistant(" ".to_string()));
}
fixed.push(msg);
}
fixed
}
pub fn oai_event_to_llm(event: &OaiEvent) -> Option<StreamEvent> {
match event {
OaiEvent::TextDelta(t) => Some(StreamEvent::Llm(LlmEvent::Text(t.clone()))),
OaiEvent::ToolCallStart { name, id, .. } => {
Some(StreamEvent::Llm(LlmEvent::ToolUseStart {
tool_name: name.clone(),
tool_id: id.clone(),
}))
}
OaiEvent::ToolCallArgumentsDelta { delta, id, .. } => {
Some(StreamEvent::Llm(LlmEvent::ToolUseDelta {
tool_id: id.clone(),
delta: delta.clone(),
}))
}
OaiEvent::Usage { prompt_tokens, completion_tokens, cached_tokens } => {
Some(StreamEvent::Session(SessionEvent::Usage {
input_tokens: *prompt_tokens as u64,
output_tokens: *completion_tokens as u64,
cache_read_input_tokens: *cached_tokens as u64,
cache_creation_input_tokens: 0,
model: None,
}))
}
OaiEvent::Warning(s) => {
tracing::warn!("openai stream warning: {}", s);
None
}
OaiEvent::RoleStart(_) | OaiEvent::Done | OaiEvent::ToolCallsComplete { .. } => None,
}
}
pub fn tool_calls_to_content_blocks(calls: &[ToolCall], name_map: &ToolNameMap) -> Vec<Value> {
calls
.iter()
.map(|c| {
let input: Value = serde_json::from_str(&c.function.arguments).unwrap_or_else(|_| json!({}));
json!({
"type": "tool_use",
"id": c.id,
"name": name_map.to_original(&c.function.name),
"input": input,
})
})
.collect()
}