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