Skip to main content

openclaw_node/providers/
types.rs

1//! JavaScript-friendly types for provider bindings.
2
3use napi_derive::napi;
4use serde::{Deserialize, Serialize};
5
6use openclaw_providers::{
7    CompletionRequest, CompletionResponse, ContentBlock, Message, MessageContent, Role, StopReason,
8    Tool as ProviderTool,
9};
10
11/// A message in a conversation.
12#[napi(object)]
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct JsMessage {
15    /// Role: "user", "assistant", "system", or "tool"
16    pub role: String,
17    /// Message content (text)
18    pub content: String,
19    /// Tool use ID (for tool results)
20    pub tool_use_id: Option<String>,
21    /// Tool name (for tool results)
22    pub tool_name: Option<String>,
23}
24
25/// A completion request.
26#[napi(object)]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct JsCompletionRequest {
29    /// Model to use (e.g., "claude-3-5-sonnet-20241022")
30    pub model: String,
31    /// Conversation messages
32    pub messages: Vec<JsMessage>,
33    /// System prompt (optional)
34    pub system: Option<String>,
35    /// Maximum tokens to generate
36    pub max_tokens: u32,
37    /// Temperature (0.0-1.0)
38    pub temperature: Option<f64>,
39    /// Stop sequences
40    pub stop: Option<Vec<String>>,
41    /// Tools available to the model
42    pub tools: Option<Vec<JsTool>>,
43}
44
45/// A tool definition.
46#[napi(object)]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct JsTool {
49    /// Tool name
50    pub name: String,
51    /// Tool description
52    pub description: String,
53    /// JSON schema for input parameters
54    pub input_schema: serde_json::Value,
55}
56
57/// A completion response.
58#[napi(object)]
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct JsCompletionResponse {
61    /// Response ID
62    pub id: String,
63    /// Model used
64    pub model: String,
65    /// Text content (joined from all text blocks)
66    pub content: String,
67    /// Stop reason: "`end_turn`", "`max_tokens`", "`stop_sequence`", "`tool_use`"
68    pub stop_reason: Option<String>,
69    /// Tool calls made by the model
70    pub tool_calls: Option<Vec<JsToolCall>>,
71    /// Token usage
72    pub usage: JsTokenUsage,
73}
74
75/// A tool call from the model.
76#[napi(object)]
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct JsToolCall {
79    /// Tool use ID (for providing results back)
80    pub id: String,
81    /// Tool name
82    pub name: String,
83    /// Tool input parameters
84    pub input: serde_json::Value,
85}
86
87/// Token usage information.
88#[napi(object)]
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct JsTokenUsage {
91    /// Input tokens consumed
92    pub input_tokens: u32,
93    /// Output tokens generated
94    pub output_tokens: u32,
95    /// Tokens read from cache (if applicable)
96    pub cache_read_tokens: Option<u32>,
97    /// Tokens written to cache (if applicable)
98    pub cache_write_tokens: Option<u32>,
99}
100
101/// A streaming chunk.
102#[napi(object)]
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct JsStreamChunk {
105    /// Chunk type: "`message_start`", "`content_block_start`", "`content_block_delta`",
106    /// "`content_block_stop`", "`message_delta`", "`message_stop`"
107    pub chunk_type: String,
108    /// Text delta (for `content_block_delta`)
109    pub delta: Option<String>,
110    /// Block index
111    pub index: Option<u32>,
112    /// Stop reason (for `message_delta` with stop)
113    pub stop_reason: Option<String>,
114}
115
116// ---- Conversion functions ----
117
118/// Convert `JsMessage` to internal Message.
119#[must_use]
120pub fn convert_js_message(msg: &JsMessage) -> Message {
121    let role = match msg.role.as_str() {
122        "user" => Role::User,
123        "assistant" => Role::Assistant,
124        "system" => Role::System,
125        "tool" => Role::Tool,
126        _ => Role::User,
127    };
128
129    // Handle tool results
130    if role == Role::Tool {
131        Message {
132            role,
133            content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
134                tool_use_id: msg.tool_use_id.clone().unwrap_or_default(),
135                content: msg.content.clone(),
136                is_error: None,
137            }]),
138        }
139    } else {
140        Message {
141            role,
142            content: MessageContent::Text(msg.content.clone()),
143        }
144    }
145}
146
147/// Convert `JsTool` to internal Tool.
148#[must_use]
149pub fn convert_js_tool(tool: &JsTool) -> ProviderTool {
150    ProviderTool {
151        name: tool.name.clone(),
152        description: tool.description.clone(),
153        input_schema: tool.input_schema.clone(),
154    }
155}
156
157/// Convert `JsCompletionRequest` to internal `CompletionRequest`.
158pub fn convert_request(req: JsCompletionRequest) -> CompletionRequest {
159    CompletionRequest {
160        model: req.model,
161        messages: req.messages.iter().map(convert_js_message).collect(),
162        system: req.system,
163        max_tokens: req.max_tokens,
164        temperature: req.temperature.map_or(1.0, |t| t as f32),
165        stop: req.stop,
166        tools: req
167            .tools
168            .map(|tools| tools.iter().map(convert_js_tool).collect()),
169    }
170}
171
172/// Convert internal `CompletionResponse` to `JsCompletionResponse`.
173#[must_use]
174pub fn convert_response(resp: CompletionResponse) -> JsCompletionResponse {
175    // Extract text content
176    let content = resp
177        .content
178        .iter()
179        .filter_map(|block| {
180            if let ContentBlock::Text { text } = block {
181                Some(text.as_str())
182            } else {
183                None
184            }
185        })
186        .collect::<Vec<_>>()
187        .join("");
188
189    // Extract tool calls
190    let tool_calls: Vec<JsToolCall> = resp
191        .content
192        .iter()
193        .filter_map(|block| {
194            if let ContentBlock::ToolUse { id, name, input } = block {
195                Some(JsToolCall {
196                    id: id.clone(),
197                    name: name.clone(),
198                    input: input.clone(),
199                })
200            } else {
201                None
202            }
203        })
204        .collect();
205
206    let stop_reason = resp.stop_reason.map(|sr| match sr {
207        StopReason::EndTurn => "end_turn".to_string(),
208        StopReason::MaxTokens => "max_tokens".to_string(),
209        StopReason::StopSequence => "stop_sequence".to_string(),
210        StopReason::ToolUse => "tool_use".to_string(),
211    });
212
213    JsCompletionResponse {
214        id: resp.id,
215        model: resp.model,
216        content,
217        stop_reason,
218        tool_calls: if tool_calls.is_empty() {
219            None
220        } else {
221            Some(tool_calls)
222        },
223        usage: JsTokenUsage {
224            input_tokens: resp.usage.input_tokens as u32,
225            output_tokens: resp.usage.output_tokens as u32,
226            cache_read_tokens: resp.usage.cache_read_tokens.map(|v| v as u32),
227            cache_write_tokens: resp.usage.cache_write_tokens.map(|v| v as u32),
228        },
229    }
230}