use napi_derive::napi;
use serde::{Deserialize, Serialize};
use openclaw_providers::{
CompletionRequest, CompletionResponse, ContentBlock, Message, MessageContent, Role, StopReason,
Tool as ProviderTool,
};
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsMessage {
pub role: String,
pub content: String,
pub tool_use_id: Option<String>,
pub tool_name: Option<String>,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsCompletionRequest {
pub model: String,
pub messages: Vec<JsMessage>,
pub system: Option<String>,
pub max_tokens: u32,
pub temperature: Option<f64>,
pub stop: Option<Vec<String>>,
pub tools: Option<Vec<JsTool>>,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsTool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsCompletionResponse {
pub id: String,
pub model: String,
pub content: String,
pub stop_reason: Option<String>,
pub tool_calls: Option<Vec<JsToolCall>>,
pub usage: JsTokenUsage,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsToolCall {
pub id: String,
pub name: String,
pub input: serde_json::Value,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsTokenUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub cache_read_tokens: Option<u32>,
pub cache_write_tokens: Option<u32>,
}
#[napi(object)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsStreamChunk {
pub chunk_type: String,
pub delta: Option<String>,
pub index: Option<u32>,
pub stop_reason: Option<String>,
}
#[must_use]
pub fn convert_js_message(msg: &JsMessage) -> Message {
let role = match msg.role.as_str() {
"user" => Role::User,
"assistant" => Role::Assistant,
"system" => Role::System,
"tool" => Role::Tool,
_ => Role::User,
};
if role == Role::Tool {
Message {
role,
content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
tool_use_id: msg.tool_use_id.clone().unwrap_or_default(),
content: msg.content.clone(),
is_error: None,
}]),
}
} else {
Message {
role,
content: MessageContent::Text(msg.content.clone()),
}
}
}
#[must_use]
pub fn convert_js_tool(tool: &JsTool) -> ProviderTool {
ProviderTool {
name: tool.name.clone(),
description: tool.description.clone(),
input_schema: tool.input_schema.clone(),
}
}
pub fn convert_request(req: JsCompletionRequest) -> CompletionRequest {
CompletionRequest {
model: req.model,
messages: req.messages.iter().map(convert_js_message).collect(),
system: req.system,
max_tokens: req.max_tokens,
temperature: req.temperature.map_or(1.0, |t| t as f32),
stop: req.stop,
tools: req
.tools
.map(|tools| tools.iter().map(convert_js_tool).collect()),
}
}
#[must_use]
pub fn convert_response(resp: CompletionResponse) -> JsCompletionResponse {
let content = resp
.content
.iter()
.filter_map(|block| {
if let ContentBlock::Text { text } = block {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
let tool_calls: Vec<JsToolCall> = resp
.content
.iter()
.filter_map(|block| {
if let ContentBlock::ToolUse { id, name, input } = block {
Some(JsToolCall {
id: id.clone(),
name: name.clone(),
input: input.clone(),
})
} else {
None
}
})
.collect();
let stop_reason = resp.stop_reason.map(|sr| match sr {
StopReason::EndTurn => "end_turn".to_string(),
StopReason::MaxTokens => "max_tokens".to_string(),
StopReason::StopSequence => "stop_sequence".to_string(),
StopReason::ToolUse => "tool_use".to_string(),
});
JsCompletionResponse {
id: resp.id,
model: resp.model,
content,
stop_reason,
tool_calls: if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
},
usage: JsTokenUsage {
input_tokens: resp.usage.input_tokens as u32,
output_tokens: resp.usage.output_tokens as u32,
cache_read_tokens: resp.usage.cache_read_tokens.map(|v| v as u32),
cache_write_tokens: resp.usage.cache_write_tokens.map(|v| v as u32),
},
}
}