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 #[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#[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}