1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use super::request::{FinishReason, ToolCall};
6
7#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
8pub struct ErrResponse {
9 pub error: Err,
10}
11
12#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
13pub struct Err {
14 pub message: String,
15 pub r#type: String,
16 pub param: String,
17 pub code: String,
18}
19
20#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
21pub struct Response {
22 pub id: String,
24 pub choices: Vec<Choice>,
26 pub created: u64,
28 pub model: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
33 pub service_tier: Option<ServiceTier>,
34 pub system_fingerprint: Option<String>,
38 pub object: String,
40 pub usage: Usage,
42}
43
44#[derive(Debug, Clone)]
45pub struct ResponseBuilder {
46 inner: Response,
47}
48
49impl Default for ResponseBuilder {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl ResponseBuilder {
56 pub fn new() -> Self {
57 Self {
58 inner: Response {
59 object: "chat.completion".to_string(),
60 ..Default::default()
61 },
62 }
63 }
64
65 pub fn build(self) -> Response {
66 self.inner
67 }
68
69 pub fn push_assistant_message(mut self, message: impl Into<String>) -> Self {
70 self.inner.choices.push(Choice {
71 index: self.inner.choices.len(),
72 message: Message {
73 role: Role::Assistant,
74 content: Some(message.into()),
75 ..Default::default()
76 },
77 finish_reason: None,
78 logprobs: None,
79 });
80 self
81 }
82
83 pub fn id(mut self, id: impl Into<String>) -> Self {
84 self.inner.id = id.into();
85 self
86 }
87
88 pub fn model(mut self, model: impl Into<String>) -> Self {
89 self.inner.model = model.into();
90 self
91 }
92}
93
94#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
96#[serde(rename_all = "lowercase")]
97pub enum ServiceTier {
98 Auto,
99 Default,
100 Flex,
101 Priority,
102}
103
104#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
106pub struct Usage {
107 pub completion_tokens: u32,
109 pub prompt_tokens: u32,
111 pub total_tokens: u32,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub completion_tokens_details: Option<CompletionTokensDetails>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub prompt_tokens_details: Option<PromptTokensDetails>,
119}
120
121#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
123pub struct CompletionTokensDetails {
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub accepted_prediction_tokens: Option<u32>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub audio_tokens: Option<u32>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub reasoning_tokens: Option<u32>,
133 #[serde(skip_serializing_if = "Option::is_none")]
137 pub rejected_prediction_tokens: Option<u32>,
138}
139
140#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
142pub struct PromptTokensDetails {
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub audio_tokens: Option<u32>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub cached_tokens: Option<u32>,
149}
150
151#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
152pub struct Message {
153 pub content: Option<String>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
159 pub reasoning: Option<String>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub tool_calls: Option<Vec<ToolCall>>,
164
165 pub role: Role,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub refusal: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub annotations: Option<Vec<Annotation>>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub audio: Option<ChatCompletionAudio>,
179}
180
181#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq)]
182#[serde(rename_all = "lowercase")]
183pub enum Role {
184 System,
185 #[default]
186 User,
187 Assistant,
188 Tool,
189}
190
191impl fmt::Display for Role {
192 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193 match *self {
194 Role::System => write!(f, "system"),
195 Role::User => write!(f, "user"),
196 Role::Assistant => write!(f, "assistant"),
197 Role::Tool => write!(f, "tool"),
198 }
199 }
200}
201
202impl Role {
203 pub fn as_str(&self) -> &'static str {
204 match *self {
205 Role::System => "system",
206 Role::User => "user",
207 Role::Assistant => "assistant",
208 Role::Tool => "tool",
209 }
210 }
211}
212
213#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq)]
214pub struct Choice {
215 pub index: usize,
216 pub message: Message,
217 pub finish_reason: Option<FinishReason>,
222 pub logprobs: Option<Logprobs>,
224}
225
226#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
227pub struct Logprobs {
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub content: Option<Vec<LogprobContent>>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub refusal: Option<Vec<LogprobContent>>,
234}
235
236#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
237pub struct LogprobContent {
238 pub token: String,
240 pub logprob: f64,
243 #[serde(skip_serializing_if = "Option::is_none")]
248 pub bytes: Option<Vec<u8>>,
249 #[serde(skip_serializing_if = "Option::is_none")]
252 pub top_logprobs: Option<Vec<TopLogprobs>>,
253}
254
255#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
256pub struct TopLogprobs {
257 pub token: String,
259 pub logprob: f64,
262 #[serde(skip_serializing_if = "Option::is_none")]
267 pub bytes: Option<Vec<u8>>,
268}
269
270#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
272pub struct Annotation {
273 #[serde(rename = "type")]
275 pub annotation_type: String,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub url_citation: Option<AnnotationURLCitation>,
279}
280
281#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
283pub struct AnnotationURLCitation {
284 pub end_index: usize,
286 pub start_index: usize,
288 pub title: String,
290 pub url: String,
292}
293
294#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
296pub struct ChatCompletionAudio {
297 pub id: String,
299 pub data: String,
301 pub expires_at: u64,
303 pub transcript: String,
305}
306
307impl Response {
308 pub fn first_assistant_message(&self) -> Option<&Message> {
309 self.choices
310 .iter()
311 .find(|choice| choice.message.role == Role::Assistant)
312 .map(|choice| &choice.message)
313 }
314
315 pub fn first_assistant_message_text(&self) -> Option<String> {
316 self.first_assistant_message()
317 .and_then(|message| message.content.to_owned())
318 }
319
320 pub fn first_assistant_message_reasoning_text(&self) -> Option<String> {
321 self.first_assistant_message()
322 .and_then(|message| message.reasoning.to_owned())
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::completions::request::{ToolCallFunction, ToolCallFunctionObj};
329
330 use super::*;
331
332 #[test]
333 fn serde() {
334 let tests = vec![
335 (
336 "default",
337 r#"{
338 "id": "chatcmpl-123",
339 "object": "chat.completion",
340 "created": 1677652288,
341 "model": "gpt-3.5-turbo-0613",
342 "system_fingerprint": "fp_44709d6fcb",
343 "choices": [{
344 "index": 0,
345 "message": {
346 "role": "assistant",
347 "content": "\n\nHello there, how may I assist you today?"
348 },
349 "logprobs": null,
350 "finish_reason": "stop"
351 }],
352 "usage": {
353 "prompt_tokens": 9,
354 "completion_tokens": 12,
355 "total_tokens": 21
356 }
357 }"#,
358 Response {
359 id: "chatcmpl-123".to_string(),
360 object: "chat.completion".to_string(),
361 created: 1677652288,
362 model: "gpt-3.5-turbo-0613".to_string(),
363 system_fingerprint: Some("fp_44709d6fcb".to_string()),
364 choices: vec![Choice {
365 index: 0,
366 message: Message {
367 role: Role::Assistant,
368 content: Some(
369 "\n\nHello there, how may I assist you today?".to_string(),
370 ),
371 reasoning: None,
372 tool_calls: None,
373 refusal: None,
374 annotations: None,
375 audio: None,
376 },
377 logprobs: None,
378 finish_reason: Some(FinishReason::Stop),
379 }],
380 usage: Usage {
381 prompt_tokens: 9,
382 completion_tokens: 12,
383 total_tokens: 21,
384 ..Default::default()
385 },
386 service_tier: None,
387 },
388 ),
389 (
390 "function",
391 r#"{
392 "id": "chatcmpl-abc123",
393 "object": "chat.completion",
394 "created": 1699896916,
395 "model": "gpt-3.5-turbo-0613",
396 "system_fingerprint": "fp_6b68a8204b",
397 "choices": [
398 {
399 "index": 0,
400 "message": {
401 "role": "assistant",
402 "content": null,
403 "tool_calls": [
404 {
405 "id": "call_abc123",
406 "type": "function",
407 "function": {
408 "name": "get_current_weather",
409 "arguments": "{\n\"location\": \"Boston, MA\"\n}"
410 }
411 }
412 ]
413 },
414 "logprobs": null,
415 "finish_reason": "tool_calls"
416 }
417 ],
418 "usage": {
419 "prompt_tokens": 82,
420 "completion_tokens": 17,
421 "total_tokens": 99
422 }
423 }"#,
424 Response {
425 id: "chatcmpl-abc123".to_string(),
426 object: "chat.completion".to_string(),
427 created: 1699896916,
428 model: "gpt-3.5-turbo-0613".to_string(),
429 system_fingerprint: Some("fp_6b68a8204b".to_string()),
430 choices: vec![Choice {
431 index: 0,
432 message: Message {
433 role: Role::Assistant,
434 content: None,
435 tool_calls: Some(vec![ToolCall::Function(ToolCallFunction {
436 id: "call_abc123".to_string(),
437 function: ToolCallFunctionObj {
438 name: "get_current_weather".to_string(),
439 arguments: "{\n\"location\": \"Boston, MA\"\n}".to_string(),
440 },
441 })]),
442 refusal: None,
443 reasoning: None,
444 annotations: None,
445 audio: None,
446 },
447 logprobs: None,
448 finish_reason: Some(FinishReason::ToolCalls),
449 }],
450 usage: Usage {
451 prompt_tokens: 82,
452 completion_tokens: 17,
453 total_tokens: 99,
454 ..Default::default()
455 },
456 service_tier: None,
457 },
458 ),
459 (
460 "logprobs",
461 r#"{
462 "id": "chatcmpl-123",
463 "object": "chat.completion",
464 "created": 1702685778,
465 "model": "gpt-3.5-turbo-0613",
466 "choices": [
467 {
468 "index": 0,
469 "message": {
470 "role": "assistant",
471 "content": "Hello! How can I assist you today?"
472 },
473 "logprobs": {
474 "content": [
475 {
476 "token": "Hello",
477 "logprob": -0.31725305,
478 "bytes": [72, 101, 108, 108, 111],
479 "top_logprobs": [
480 {
481 "token": "Hello",
482 "logprob": -0.31725305,
483 "bytes": [72, 101, 108, 108, 111]
484 },
485 {
486 "token": "Hi",
487 "logprob": -1.3190403,
488 "bytes": [72, 105]
489 }
490 ]
491 },
492 {
493 "token": "!",
494 "logprob": -0.02380986,
495 "bytes": [33],
496 "top_logprobs": [
497 {
498 "token": "!",
499 "logprob": -0.02380986,
500 "bytes": [33]
501 },
502 {
503 "token": " there",
504 "logprob": -3.787621,
505 "bytes": [32, 116, 104, 101, 114, 101]
506 }
507 ]
508 },
509 {
510 "token": " How",
511 "logprob": -0.000054669687,
512 "bytes": [32, 72, 111, 119],
513 "top_logprobs": [
514 {
515 "token": " How",
516 "logprob": -0.000054669687,
517 "bytes": [32, 72, 111, 119]
518 },
519 {
520 "token": "<|end|>",
521 "logprob": -10.953937,
522 "bytes": null
523 }
524 ]
525 }
526 ]
527 },
528 "finish_reason": "stop"
529 }
530 ],
531 "usage": {
532 "prompt_tokens": 9,
533 "completion_tokens": 9,
534 "total_tokens": 18
535 },
536 "system_fingerprint": "fp_44709d6fcb"
537 }"#,
538 Response {
539 id: "chatcmpl-123".to_string(),
540 object: "chat.completion".to_string(),
541 created: 1702685778,
542 model: "gpt-3.5-turbo-0613".to_string(),
543 system_fingerprint: Some("fp_44709d6fcb".to_string()),
544 choices: vec![Choice {
545 index: 0,
546 message: Message {
547 role: Role::Assistant,
548 content: Some("Hello! How can I assist you today?".to_string()),
549 reasoning: None,
550 tool_calls: None,
551 refusal: None,
552 annotations: None,
553 audio: None,
554 },
555 logprobs: Some(Logprobs {
556 content: Some(vec![
557 LogprobContent {
558 token: "Hello".to_string(),
559 logprob: -0.31725305,
560 bytes: Some(vec![72, 101, 108, 108, 111]),
561 top_logprobs: Some(vec![
562 TopLogprobs {
563 token: "Hello".to_string(),
564 logprob: -0.31725305,
565 bytes: Some(vec![72, 101, 108, 108, 111]),
566 },
567 TopLogprobs {
568 token: "Hi".to_string(),
569 logprob: -1.3190403,
570 bytes: Some(vec![72, 105]),
571 },
572 ]),
573 },
574 LogprobContent {
575 token: "!".to_string(),
576 logprob: -0.02380986,
577 bytes: Some(vec![33]),
578 top_logprobs: Some(vec![
579 TopLogprobs {
580 token: "!".to_string(),
581 logprob: -0.02380986,
582 bytes: Some(vec![33]),
583 },
584 TopLogprobs {
585 token: " there".to_string(),
586 logprob: -3.787621,
587 bytes: Some(vec![32, 116, 104, 101, 114, 101]),
588 },
589 ]),
590 },
591 LogprobContent {
592 token: " How".to_string(),
593 logprob: -0.000054669687,
594 bytes: Some(vec![32, 72, 111, 119]),
595 top_logprobs: Some(vec![
596 TopLogprobs {
597 token: " How".to_string(),
598 logprob: -0.000054669687,
599 bytes: Some(vec![32, 72, 111, 119]),
600 },
601 TopLogprobs {
602 token: "<|end|>".to_string(),
603 logprob: -10.953937,
604 bytes: None,
605 },
606 ]),
607 },
608 ]),
609 refusal: None,
610 }),
611 finish_reason: Some(FinishReason::Stop),
612 }],
613 usage: Usage {
614 prompt_tokens: 9,
615 completion_tokens: 9,
616 total_tokens: 18,
617 completion_tokens_details: None,
618 prompt_tokens_details: None,
619 },
620 service_tier: None,
621 },
622 ),
623 (
624 "refusal",
625 r#"{
626 "id": "chatcmpl-123456",
627 "object": "chat.completion",
628 "created": 1728933352,
629 "model": "gpt-4o-2024-08-06",
630 "choices": [
631 {
632 "index": 0,
633 "message": {
634 "role": "assistant",
635 "content": "Hi there! How can I assist you today?"
636 },
637 "logprobs": null,
638 "finish_reason": "stop"
639 }
640 ],
641 "usage": {
642 "prompt_tokens": 19,
643 "completion_tokens": 10,
644 "total_tokens": 29,
645 "prompt_tokens_details": {
646 "cached_tokens": 0
647 },
648 "completion_tokens_details": {
649 "reasoning_tokens": 0,
650 "accepted_prediction_tokens": 0,
651 "rejected_prediction_tokens": 0
652 }
653 },
654 "system_fingerprint": "fp_6b68a8204b"
655 }"#,
656 Response {
657 id: "chatcmpl-123456".to_string(),
658 object: "chat.completion".to_string(),
659 created: 1728933352,
660 model: "gpt-4o-2024-08-06".to_string(),
661 system_fingerprint: Some("fp_6b68a8204b".to_string()),
662 choices: vec![Choice {
663 index: 0,
664 message: Message {
665 role: Role::Assistant,
666 content: Some("Hi there! How can I assist you today?".to_string()),
667 reasoning: None,
668 tool_calls: None,
669 refusal: None,
670 annotations: None,
671 audio: None,
672 },
673 logprobs: None,
674 finish_reason: Some(FinishReason::Stop),
675 }],
676 usage: Usage {
677 prompt_tokens: 19,
678 completion_tokens: 10,
679 total_tokens: 29,
680 prompt_tokens_details: Some(PromptTokensDetails {
681 audio_tokens: None,
682 cached_tokens: Some(0),
683 }),
684 completion_tokens_details: Some(CompletionTokensDetails {
685 reasoning_tokens: Some(0),
686 accepted_prediction_tokens: Some(0),
687 rejected_prediction_tokens: Some(0),
688 audio_tokens: None,
689 }),
690 },
691 service_tier: None,
692 },
693 ),
694 ];
695 for (name, json, expected) in tests {
696 let actual: Response = serde_json::from_str(json).unwrap();
698 assert_eq!(actual, expected, "deserialize test failed: {}", name);
699 let serialized = serde_json::to_string(&expected).unwrap();
701 let actual: Response = serde_json::from_str(&serialized).unwrap();
702 assert_eq!(actual, expected, "serialize test failed: {}", name);
703 }
704 }
705}