1use serde::{Deserialize, Serialize};
4
5use super::{Message, Role, ToolCall};
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub struct ChatCompletionResponse {
10 #[serde(default)]
13 pub id: Option<String>,
14 #[serde(default)]
16 pub object: Option<String>,
17 #[serde(default)]
19 pub created: Option<u64>,
20 pub model: String,
22 pub choices: Vec<Choice>,
24 #[serde(default)]
26 pub usage: Option<Usage>,
27 #[serde(default)]
29 pub provider: Option<String>,
30 #[serde(default)]
32 pub system_fingerprint: Option<String>,
33}
34
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub struct Choice {
38 pub index: u32,
40 #[serde(default)]
42 pub message: Option<Message>,
43 #[serde(default)]
45 pub delta: Option<Delta>,
46 #[serde(default)]
48 pub finish_reason: Option<String>,
49 #[serde(default)]
51 pub native_finish_reason: Option<String>,
52 #[serde(default)]
54 pub logprobs: Option<LogProbs>,
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
59pub struct Delta {
60 #[serde(default)]
62 pub role: Option<Role>,
63 #[serde(default)]
65 pub content: Option<String>,
66 #[serde(default)]
69 pub tool_calls: Option<Vec<ToolCall>>,
70 #[serde(default)]
72 pub reasoning: Option<String>,
73}
74
75pub type LogProbs = serde_json::Value;
77
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
80pub struct CompletionResponse {
81 #[serde(default)]
83 pub id: Option<String>,
84 #[serde(default)]
86 pub object: Option<String>,
87 #[serde(default)]
89 pub created: Option<u64>,
90 pub model: String,
92 pub choices: Vec<CompletionChoice>,
94 #[serde(default)]
96 pub usage: Option<Usage>,
97 #[serde(default)]
99 pub provider: Option<String>,
100}
101
102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
104pub struct CompletionChoice {
105 pub index: u32,
107 pub text: String,
109 #[serde(default)]
111 pub finish_reason: Option<String>,
112 #[serde(default)]
114 pub native_finish_reason: Option<String>,
115 #[serde(default)]
117 pub logprobs: Option<LogProbs>,
118}
119
120#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
122pub struct Usage {
123 #[serde(default)]
125 pub prompt_tokens: Option<u32>,
126 #[serde(default)]
128 pub completion_tokens: Option<u32>,
129 #[serde(default)]
131 pub total_tokens: Option<u32>,
132 #[serde(default)]
134 pub prompt_tokens_details: Option<TokenDetails>,
135 #[serde(default)]
137 pub completion_tokens_details: Option<TokenDetails>,
138 #[serde(default)]
140 pub cost: Option<f64>,
141}
142
143#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
145pub struct TokenDetails {
146 #[serde(default)]
148 pub cached_tokens: Option<u32>,
149 #[serde(default)]
151 pub reasoning_tokens: Option<u32>,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use pretty_assertions::assert_eq;
158
159 #[test]
160 fn round_trip_chat_response() {
161 let raw = r#"{
162 "id":"gen-1",
163 "object":"chat.completion",
164 "created":1700000000,
165 "model":"anthropic/claude-3-opus",
166 "provider":"Anthropic",
167 "choices":[{
168 "index":0,
169 "message":{"role":"assistant","content":"hi"},
170 "finish_reason":"stop"
171 }],
172 "usage":{"prompt_tokens":3,"completion_tokens":1,"total_tokens":4}
173 }"#;
174 let r: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
175 assert_eq!(r.id.as_deref(), Some("gen-1"));
176 assert_eq!(r.choices.len(), 1);
177 let c = &r.choices[0];
178 assert_eq!(c.message.as_ref().unwrap().content_text(), Some("hi"));
179 assert_eq!(r.usage.as_ref().unwrap().total_tokens, Some(4));
180 }
181
182 #[test]
183 fn round_trip_streaming_chunk() {
184 let raw = r#"{
185 "id":"gen-2",
186 "model":"x/y",
187 "choices":[{
188 "index":0,
189 "delta":{"role":"assistant","content":"Hel"},
190 "finish_reason":null
191 }]
192 }"#;
193 let r: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
194 let d = r.choices[0].delta.as_ref().unwrap();
195 assert_eq!(d.content.as_deref(), Some("Hel"));
196 assert_eq!(d.role, Some(Role::Assistant));
197 }
198
199 #[test]
200 fn annotations_round_trip_url_citations() {
201 let raw = r#"{
202 "id":"gen-a","model":"x/y",
203 "choices":[{
204 "index":0,
205 "message":{
206 "role":"assistant",
207 "content":"see source",
208 "annotations":[{
209 "type":"url_citation",
210 "url_citation":{
211 "url":"https://example.com",
212 "title":"Example",
213 "start_index":0,
214 "end_index":10
215 }
216 }]
217 },
218 "finish_reason":"stop"
219 }]
220 }"#;
221 let r: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
222 let msg = r.choices[0].message.as_ref().unwrap();
223 let anns = msg.annotations.as_ref().unwrap();
224 assert_eq!(anns.len(), 1);
225 match &anns[0] {
226 crate::types::Annotation::UrlCitation { url_citation } => {
227 assert_eq!(url_citation.url, "https://example.com");
228 assert_eq!(url_citation.title.as_deref(), Some("Example"));
229 }
230 other => panic!("unexpected annotation: {other:?}"),
231 }
232 }
233
234 #[test]
235 fn reasoning_fields_round_trip() {
236 let raw = r#"{
237 "id":"gen-r","model":"x/y",
238 "choices":[{
239 "index":0,
240 "message":{"role":"assistant","content":"42","reasoning":"long chain"},
241 "finish_reason":"stop"
242 }],
243 "usage":{
244 "prompt_tokens":3,"completion_tokens":5,"total_tokens":8,
245 "completion_tokens_details":{"reasoning_tokens":17}
246 }
247 }"#;
248 let r: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
249 let msg = r.choices[0].message.as_ref().unwrap();
250 assert_eq!(msg.reasoning.as_deref(), Some("long chain"));
251 let details = r
252 .usage
253 .as_ref()
254 .unwrap()
255 .completion_tokens_details
256 .as_ref()
257 .unwrap();
258 assert_eq!(details.reasoning_tokens, Some(17));
259 }
260
261 #[test]
262 fn round_trip_tool_call_response() {
263 let raw = r#"{
264 "id":"gen-3","model":"x/y",
265 "choices":[{
266 "index":0,
267 "message":{
268 "role":"assistant",
269 "content":"",
270 "tool_calls":[{"id":"c1","type":"function","function":{"name":"f","arguments":"{}"}}]
271 },
272 "finish_reason":"tool_calls"
273 }]
274 }"#;
275 let r: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
276 let calls = r.choices[0]
277 .message
278 .as_ref()
279 .unwrap()
280 .tool_calls
281 .as_ref()
282 .unwrap();
283 assert_eq!(calls.len(), 1);
284 assert_eq!(calls[0].function.name.as_deref(), Some("f"));
285 }
286}