use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum Message {
System(String),
User(String),
Assistant {
text: Option<String>,
tool_calls: Vec<ToolCall>,
},
ToolResult {
id: String,
content: String,
is_error: bool,
},
}
impl Message {
pub fn system(s: impl Into<String>) -> Message {
Message::System(s.into())
}
pub fn user(s: impl Into<String>) -> Message {
Message::User(s.into())
}
pub fn tool_result(
id: impl Into<String>,
content: impl Into<String>,
is_error: bool,
) -> Message {
Message::ToolResult {
id: id.into(),
content: content.into(),
is_error,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Clone)]
pub struct Request {
pub model: String,
pub messages: Vec<Message>,
pub tools: Vec<ToolDef>,
pub max_tokens: u32,
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
ToolUse,
MaxTokens,
Other,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
}
impl Usage {
pub fn total(&self) -> u64 {
self.input_tokens + self.output_tokens
}
}
#[derive(Debug, Clone)]
pub struct Response {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub stop_reason: StopReason,
pub usage: Usage,
}
impl Response {
pub fn wants_tools(&self) -> bool {
!self.tool_calls.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_totals() {
let u = Usage {
input_tokens: 100,
output_tokens: 25,
};
assert_eq!(u.total(), 125);
}
#[test]
fn tool_call_roundtrips() {
let tc = ToolCall {
id: "call_1".into(),
name: "read_file".into(),
arguments: serde_json::json!({"path": "/etc/hosts"}),
};
let s = serde_json::to_string(&tc).unwrap();
let back: ToolCall = serde_json::from_str(&s).unwrap();
assert_eq!(back, tc);
}
#[test]
fn response_branch() {
let r = Response {
text: None,
tool_calls: vec![ToolCall {
id: "1".into(),
name: "x".into(),
arguments: Value::Null,
}],
stop_reason: StopReason::ToolUse,
usage: Usage::default(),
};
assert!(r.wants_tools());
}
#[test]
fn stop_reason_snake_case() {
assert_eq!(
serde_json::to_string(&StopReason::ToolUse).unwrap(),
"\"tool_use\""
);
assert_eq!(
serde_json::to_string(&StopReason::EndTurn).unwrap(),
"\"end_turn\""
);
}
}