1use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15#[derive(Debug, Clone, PartialEq)]
18pub enum Message {
19 System(String),
20 User(String),
21 Assistant {
22 text: Option<String>,
23 tool_calls: Vec<ToolCall>,
24 },
25 ToolResult {
29 id: String,
30 content: String,
31 is_error: bool,
32 },
33}
34
35impl Message {
36 pub fn system(s: impl Into<String>) -> Message {
37 Message::System(s.into())
38 }
39 pub fn user(s: impl Into<String>) -> Message {
40 Message::User(s.into())
41 }
42 pub fn tool_result(
43 id: impl Into<String>,
44 content: impl Into<String>,
45 is_error: bool,
46 ) -> Message {
47 Message::ToolResult {
48 id: id.into(),
49 content: content.into(),
50 is_error,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct ToolCall {
58 pub id: String,
59 pub name: String,
60 pub arguments: Value,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct ToolDef {
67 pub name: String,
68 #[serde(default, skip_serializing_if = "String::is_empty")]
69 pub description: String,
70 pub input_schema: Value,
72}
73
74#[derive(Debug, Clone)]
76pub struct Request {
77 pub model: String,
78 pub messages: Vec<Message>,
79 pub tools: Vec<ToolDef>,
80 pub max_tokens: u32,
81 pub temperature: Option<f32>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum StopReason {
89 EndTurn,
91 ToolUse,
93 MaxTokens,
95 Other,
97}
98
99#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
102pub struct Usage {
103 #[serde(default)]
104 pub input_tokens: u64,
105 #[serde(default)]
106 pub output_tokens: u64,
107}
108
109impl Usage {
110 pub fn total(&self) -> u64 {
111 self.input_tokens + self.output_tokens
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct Response {
118 pub text: Option<String>,
119 pub tool_calls: Vec<ToolCall>,
120 pub stop_reason: StopReason,
121 pub usage: Usage,
122}
123
124impl Response {
125 pub fn wants_tools(&self) -> bool {
128 !self.tool_calls.is_empty()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn usage_totals() {
138 let u = Usage {
139 input_tokens: 100,
140 output_tokens: 25,
141 };
142 assert_eq!(u.total(), 125);
143 }
144
145 #[test]
146 fn tool_call_roundtrips() {
147 let tc = ToolCall {
148 id: "call_1".into(),
149 name: "read_file".into(),
150 arguments: serde_json::json!({"path": "/etc/hosts"}),
151 };
152 let s = serde_json::to_string(&tc).unwrap();
153 let back: ToolCall = serde_json::from_str(&s).unwrap();
154 assert_eq!(back, tc);
155 }
156
157 #[test]
158 fn response_branch() {
159 let r = Response {
160 text: None,
161 tool_calls: vec![ToolCall {
162 id: "1".into(),
163 name: "x".into(),
164 arguments: Value::Null,
165 }],
166 stop_reason: StopReason::ToolUse,
167 usage: Usage::default(),
168 };
169 assert!(r.wants_tools());
170 }
171
172 #[test]
173 fn stop_reason_snake_case() {
174 assert_eq!(
175 serde_json::to_string(&StopReason::ToolUse).unwrap(),
176 "\"tool_use\""
177 );
178 assert_eq!(
179 serde_json::to_string(&StopReason::EndTurn).unwrap(),
180 "\"end_turn\""
181 );
182 }
183}