1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::cache::CacheControl;
7use crate::id::{ModelName, ProviderId};
8use crate::tool_types::{ToolCall, ToolChoice, ToolSpec};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ChatRequest {
13 pub model: ModelName,
15 pub messages: Vec<Message>,
17 pub tools: Vec<ToolSpec>,
19 pub tool_choice: ToolChoice,
21 pub response_format: Option<ResponseFormat>,
23 pub temperature: Option<f32>,
25 pub top_p: Option<f32>,
27 pub max_output_tokens: Option<u32>,
29 pub stop: Vec<String>,
31 pub metadata: Value,
33}
34
35impl ChatRequest {
36 #[must_use]
38 pub fn new(model: ModelName) -> Self {
39 Self {
40 model,
41 messages: Vec::new(),
42 tools: Vec::new(),
43 tool_choice: ToolChoice::default(),
44 response_format: None,
45 temperature: None,
46 top_p: None,
47 max_output_tokens: None,
48 stop: Vec::new(),
49 metadata: Value::Null,
50 }
51 }
52
53 #[must_use]
55 pub fn with_message(mut self, message: Message) -> Self {
56 self.messages.push(message);
57 self
58 }
59
60 #[must_use]
62 pub fn with_user_text(self, text: impl Into<String>) -> Self {
63 self.with_message(Message::user_text(text))
64 }
65
66 #[must_use]
68 pub fn with_tool(mut self, tool: ToolSpec) -> Self {
69 self.tools.push(tool);
70 self
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case", tag = "role")]
77#[non_exhaustive]
78pub enum Message {
79 System {
81 content: Vec<ContentPart>,
83 },
84 User {
86 content: Vec<ContentPart>,
88 },
89 Assistant {
91 content: Vec<ContentPart>,
93 tool_calls: Vec<ToolCall>,
95 },
96 Tool {
98 tool_call_id: String,
100 name: String,
102 content: Vec<ContentPart>,
104 },
105}
106
107impl Message {
108 #[must_use]
110 pub fn system_text(text: impl Into<String>) -> Self {
111 Self::System {
112 content: vec![ContentPart::text(text)],
113 }
114 }
115
116 #[must_use]
118 pub fn user_text(text: impl Into<String>) -> Self {
119 Self::User {
120 content: vec![ContentPart::text(text)],
121 }
122 }
123
124 #[must_use]
126 pub fn assistant_text(text: impl Into<String>) -> Self {
127 Self::Assistant {
128 content: vec![ContentPart::text(text)],
129 tool_calls: Vec::new(),
130 }
131 }
132
133 #[must_use]
135 pub fn tool_text(
136 tool_call_id: impl Into<String>,
137 name: impl Into<String>,
138 text: impl Into<String>,
139 ) -> Self {
140 Self::Tool {
141 tool_call_id: tool_call_id.into(),
142 name: name.into(),
143 content: vec![ContentPart::text(text)],
144 }
145 }
146
147 #[must_use]
149 pub fn tool_calls(&self) -> &[ToolCall] {
150 match self {
151 Self::Assistant { tool_calls, .. } => tool_calls.as_slice(),
152 _ => &[],
153 }
154 }
155
156 #[must_use]
164 pub fn mark_cache_breakpoint(mut self) -> Self {
165 let ctrl = CacheControl::ephemeral();
166 match &mut self {
167 Self::System { content } | Self::User { content } | Self::Tool { content, .. } => {
168 if let Some(last) = content.last_mut() {
169 last.set_cache_control(ctrl);
170 }
171 }
172 Self::Assistant { content, .. } => {
173 if let Some(last) = content.last_mut() {
174 last.set_cache_control(ctrl);
175 }
176 }
177 }
178 self
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case", tag = "type")]
185#[non_exhaustive]
186pub enum ContentPart {
187 Text {
189 text: String,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 cache_control: Option<CacheControl>,
194 },
195 Json {
197 value: Value,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 cache_control: Option<CacheControl>,
202 },
203 ImageUrl {
205 url: String,
207 mime_type: Option<String>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 cache_control: Option<CacheControl>,
212 },
213}
214
215impl ContentPart {
216 #[must_use]
218 pub fn text(text: impl Into<String>) -> Self {
219 Self::Text {
220 text: text.into(),
221 cache_control: None,
222 }
223 }
224
225 #[must_use]
227 pub fn json(value: Value) -> Self {
228 Self::Json {
229 value,
230 cache_control: None,
231 }
232 }
233
234 #[must_use]
236 pub fn image_url(url: impl Into<String>, mime_type: Option<String>) -> Self {
237 Self::ImageUrl {
238 url: url.into(),
239 mime_type,
240 cache_control: None,
241 }
242 }
243
244 #[must_use]
246 pub fn with_cache_control(mut self, ctrl: CacheControl) -> Self {
247 self.set_cache_control(ctrl);
248 self
249 }
250
251 pub fn set_cache_control(&mut self, ctrl: CacheControl) {
253 match self {
254 Self::Text { cache_control, .. }
255 | Self::Json { cache_control, .. }
256 | Self::ImageUrl { cache_control, .. } => *cache_control = Some(ctrl),
257 }
258 }
259
260 #[must_use]
262 pub fn cache_control(&self) -> Option<CacheControl> {
263 match self {
264 Self::Text { cache_control, .. }
265 | Self::Json { cache_control, .. }
266 | Self::ImageUrl { cache_control, .. } => *cache_control,
267 }
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case", tag = "type")]
274#[non_exhaustive]
275pub enum ResponseFormat {
276 Text,
278 JsonObject,
280 JsonSchema {
282 name: String,
284 schema: Value,
286 strict: bool,
288 },
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293pub struct ChatResponse {
294 pub provider: ProviderId,
296 pub model: ModelName,
298 pub message: Message,
300 pub finish_reason: FinishReason,
302 pub usage: Option<TokenUsage>,
304 pub raw: Option<Value>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311#[non_exhaustive]
312pub enum FinishReason {
313 Stop,
315 ToolCalls,
317 Length,
319 ContentFilter,
321 Error,
323 Unknown(String),
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329pub struct TokenUsage {
330 pub input_tokens: u64,
332 pub output_tokens: u64,
334 pub total_tokens: u64,
336 pub cache_creation_input_tokens: Option<u64>,
342 pub cache_read_input_tokens: Option<u64>,
348 pub cached_input_tokens: Option<u64>,
353}
354
355impl TokenUsage {
356 #[must_use]
361 pub const fn new(input_tokens: u64, output_tokens: u64) -> Self {
362 Self {
363 input_tokens,
364 output_tokens,
365 total_tokens: input_tokens + output_tokens,
366 cache_creation_input_tokens: None,
367 cache_read_input_tokens: None,
368 cached_input_tokens: None,
369 }
370 }
371
372 #[must_use]
374 pub const fn with_cache_stats(
375 mut self,
376 cache_creation_input_tokens: Option<u64>,
377 cache_read_input_tokens: Option<u64>,
378 cached_input_tokens: Option<u64>,
379 ) -> Self {
380 self.cache_creation_input_tokens = cache_creation_input_tokens;
381 self.cache_read_input_tokens = cache_read_input_tokens;
382 self.cached_input_tokens = cached_input_tokens;
383 self
384 }
385
386 #[must_use]
392 pub fn cache_tokens(&self) -> u64 {
393 self.cache_creation_input_tokens.unwrap_or(0)
394 + self.cache_read_input_tokens.unwrap_or(0)
395 + self.cached_input_tokens.unwrap_or(0)
396 }
397
398 #[must_use]
405 pub fn merge(self, other: Self) -> Self {
406 Self {
407 input_tokens: self.input_tokens + other.input_tokens,
408 output_tokens: self.output_tokens + other.output_tokens,
409 total_tokens: self.input_tokens
410 + other.input_tokens
411 + self.output_tokens
412 + other.output_tokens,
413 cache_creation_input_tokens: add_opt(
414 self.cache_creation_input_tokens,
415 other.cache_creation_input_tokens,
416 ),
417 cache_read_input_tokens: add_opt(
418 self.cache_read_input_tokens,
419 other.cache_read_input_tokens,
420 ),
421 cached_input_tokens: add_opt(self.cached_input_tokens, other.cached_input_tokens),
422 }
423 }
424}
425
426const fn add_opt(a: Option<u64>, b: Option<u64>) -> Option<u64> {
427 match (a, b) {
428 (None, None) => None,
429 (Some(x), None) | (None, Some(x)) => Some(x),
430 (Some(x), Some(y)) => Some(x + y),
431 }
432}
433
434#[cfg(test)]
435#[allow(clippy::unwrap_used)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn token_usage_new_has_no_cache_stats() {
441 let usage = TokenUsage::new(10, 5);
442 assert_eq!(usage.input_tokens, 10);
443 assert_eq!(usage.output_tokens, 5);
444 assert_eq!(usage.total_tokens, 15);
445 assert_eq!(usage.cache_creation_input_tokens, None);
446 assert_eq!(usage.cache_read_input_tokens, None);
447 assert_eq!(usage.cached_input_tokens, None);
448 }
449
450 #[test]
451 fn token_usage_with_cache_stats_populates_fields() {
452 let usage = TokenUsage::new(100, 50).with_cache_stats(Some(80), Some(20), None);
453 assert_eq!(usage.cache_creation_input_tokens, Some(80));
454 assert_eq!(usage.cache_read_input_tokens, Some(20));
455 assert_eq!(usage.cached_input_tokens, None);
456 }
457
458 #[test]
459 fn token_usage_cache_tokens_sums_all_three_fields() {
460 let usage = TokenUsage::new(100, 50).with_cache_stats(Some(10), Some(20), Some(30));
461 assert_eq!(usage.cache_tokens(), 60);
462 }
463
464 #[test]
465 fn token_usage_cache_tokens_zero_when_all_none() {
466 let usage = TokenUsage::new(100, 50);
467 assert_eq!(usage.cache_tokens(), 0);
468 }
469
470 #[test]
471 fn token_usage_merge_sums_numeric_fields() {
472 let a = TokenUsage::new(100, 50);
473 let b = TokenUsage::new(200, 30);
474 let merged = a.merge(b);
475 assert_eq!(merged.input_tokens, 300);
476 assert_eq!(merged.output_tokens, 80);
477 assert_eq!(merged.total_tokens, 380);
478 }
479
480 #[test]
481 fn token_usage_merge_both_none_yields_none() {
482 let a = TokenUsage::new(100, 50);
483 let b = TokenUsage::new(200, 30);
484 let merged = a.merge(b);
485 assert_eq!(merged.cache_creation_input_tokens, None);
486 assert_eq!(merged.cache_read_input_tokens, None);
487 assert_eq!(merged.cached_input_tokens, None);
488 }
489
490 #[test]
491 fn token_usage_merge_one_some_passes_through() {
492 let a = TokenUsage::new(100, 50).with_cache_stats(Some(40), Some(20), Some(10));
493 let b = TokenUsage::new(200, 30);
494 let merged = a.merge(b);
495 assert_eq!(merged.cache_creation_input_tokens, Some(40));
496 assert_eq!(merged.cache_read_input_tokens, Some(20));
497 assert_eq!(merged.cached_input_tokens, Some(10));
498 }
499
500 #[test]
501 fn token_usage_merge_both_some_sums() {
502 let a = TokenUsage::new(100, 50).with_cache_stats(Some(40), Some(20), Some(10));
503 let b = TokenUsage::new(200, 30).with_cache_stats(Some(60), Some(80), Some(90));
504 let merged = a.merge(b);
505 assert_eq!(merged.cache_creation_input_tokens, Some(100));
506 assert_eq!(merged.cache_read_input_tokens, Some(100));
507 assert_eq!(merged.cached_input_tokens, Some(100));
508 }
509}