aico/llm/
api_models.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Serialize, Debug)]
4pub struct ChatCompletionRequest {
5    pub model: String,
6    pub messages: Vec<Message>,
7    pub stream: bool,
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub stream_options: Option<StreamOptions>,
10    // Allow pass-through of arbitrary "extra" fields like provider flags
11    #[serde(flatten)]
12    pub extra_body: Option<serde_json::Value>,
13}
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
16pub struct Message {
17    pub role: String,
18    pub content: String,
19}
20
21#[derive(Serialize, Debug)]
22pub struct StreamOptions {
23    pub include_usage: bool,
24}
25
26// --- Response Chunks ---
27
28#[derive(Deserialize, Debug)]
29pub struct ChatCompletionChunk {
30    pub choices: Vec<ChunkChoice>,
31    pub usage: Option<ApiUsage>,
32}
33
34#[derive(Deserialize, Debug)]
35pub struct ChunkChoice {
36    pub delta: ChunkDelta,
37}
38
39#[derive(Deserialize, Debug)]
40pub struct ChunkDelta {
41    pub content: Option<String>,
42    #[serde(alias = "reasoning", alias = "thought", alias = "reasoning_content")]
43    pub reasoning_content: Option<String>,
44    #[serde(default)]
45    pub reasoning_details: Option<Vec<ReasoningDetail>>,
46}
47
48#[derive(Deserialize, Debug, Clone)]
49#[serde(tag = "type")]
50pub enum ReasoningDetail {
51    #[serde(rename = "reasoning.text")]
52    Text { text: String },
53    #[serde(rename = "reasoning.summary")]
54    Summary { summary: String },
55    #[serde(other)]
56    Unknown,
57}
58
59#[derive(Deserialize, Debug, Clone)]
60pub struct ApiUsage {
61    pub prompt_tokens: u32,
62    pub completion_tokens: u32,
63    pub total_tokens: u32,
64    #[serde(default)]
65    pub prompt_tokens_details: Option<PromptTokensDetails>,
66    #[serde(default)]
67    pub completion_tokens_details: Option<CompletionTokensDetails>,
68    #[serde(default)]
69    pub cached_tokens: Option<u32>,
70    #[serde(default)]
71    pub reasoning_tokens: Option<u32>,
72    #[serde(default)]
73    pub cost: Option<f64>,
74}
75
76#[derive(Deserialize, Debug, Clone)]
77pub struct PromptTokensDetails {
78    pub cached_tokens: Option<u32>,
79}
80
81#[derive(Deserialize, Debug, Clone)]
82pub struct CompletionTokensDetails {
83    pub reasoning_tokens: Option<u32>,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_deserialize_openai_nested_usage() {
92        let json = r#"{
93            "prompt_tokens": 100,
94            "completion_tokens": 50,
95            "total_tokens": 150,
96            "prompt_tokens_details": { "cached_tokens": 40 },
97            "completion_tokens_details": { "reasoning_tokens": 20 }
98        }"#;
99        let usage: ApiUsage = serde_json::from_str(json).unwrap();
100        assert_eq!(usage.prompt_tokens, 100);
101        assert_eq!(usage.prompt_tokens_details.unwrap().cached_tokens, Some(40));
102        assert_eq!(
103            usage.completion_tokens_details.unwrap().reasoning_tokens,
104            Some(20)
105        );
106    }
107
108    #[test]
109    fn test_deserialize_usage_with_cost() {
110        let json = r#"{
111            "prompt_tokens": 100,
112            "completion_tokens": 50,
113            "total_tokens": 150,
114            "cost": 0.00123
115        }"#;
116        let usage: ApiUsage = serde_json::from_str(json).unwrap();
117        assert_eq!(usage.prompt_tokens, 100);
118        assert_eq!(usage.cost, Some(0.00123));
119    }
120
121    #[test]
122    fn test_deserialize_reasoning_details() {
123        let json = r#"{
124            "content": null,
125            "reasoning_details": [
126                { "type": "reasoning.text", "text": "planning..." },
127                { "type": "reasoning.summary", "summary": "done planning" },
128                { "type": "unknown_type" }
129            ]
130        }"#;
131        let delta: ChunkDelta = serde_json::from_str(json).unwrap();
132        let details = delta.reasoning_details.unwrap();
133        assert_eq!(details.len(), 3);
134        match &details[0] {
135            ReasoningDetail::Text { text } => assert_eq!(text, "planning..."),
136            _ => panic!("Expected Text"),
137        }
138        match &details[1] {
139            ReasoningDetail::Summary { summary } => assert_eq!(summary, "done planning"),
140            _ => panic!("Expected Summary"),
141        }
142        match &details[2] {
143            ReasoningDetail::Unknown => (),
144            _ => panic!("Expected Unknown"),
145        }
146    }
147}