magi_openai/completions/
response.rs

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    /// A unique identifier for the chat completion.
23    pub id: String,
24    /// A list of chat completion choices. Can be more than one if n is greater than 1.
25    pub choices: Vec<Choice>,
26    /// The Unix timestamp (in seconds) of when the chat completion was created.
27    pub created: u64,
28    /// The model used for the chat completion.
29    pub model: String,
30    /// The service tier used for processing the request.
31    /// This field is only included if the service_tier parameter is specified in the request.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub service_tier: Option<ServiceTier>,
34    /// This fingerprint represents the backend configuration that the model runs with.
35    /// Can be used in conjunction with the seed request parameter to understand when backend changes have been
36    /// made that might impact determinism.
37    pub system_fingerprint: Option<String>,
38    /// The object type, which is always chat.completion
39    pub object: String,
40    /// Usage statistics for the completion request.
41    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/// Service tier used for processing the request
95#[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/// Usage statistics for the completion request.
105#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
106pub struct Usage {
107    /// Number of tokens in the generated completion.
108    pub completion_tokens: u32,
109    /// Number of tokens in the prompt.
110    pub prompt_tokens: u32,
111    /// Total number of tokens used in the request (prompt + completion).
112    pub total_tokens: u32,
113    /// Breakdown of tokens used in a completion.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub completion_tokens_details: Option<CompletionTokensDetails>,
116    /// Breakdown of tokens used in the prompt.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub prompt_tokens_details: Option<PromptTokensDetails>,
119}
120
121/// Breakdown of tokens used in a completion.
122#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
123pub struct CompletionTokensDetails {
124    /// When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub accepted_prediction_tokens: Option<u32>,
127    /// Audio input tokens generated by the model.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub audio_tokens: Option<u32>,
130    /// Tokens generated by the model for reasoning.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub reasoning_tokens: Option<u32>,
133    /// When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion.
134    /// However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes
135    /// of billing, output, and context window limits.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub rejected_prediction_tokens: Option<u32>,
138}
139
140/// Breakdown of tokens used in the prompt.
141#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
142pub struct PromptTokensDetails {
143    /// Audio input tokens present in the prompt.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub audio_tokens: Option<u32>,
146    /// Cached tokens present in the prompt.
147    #[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    /// The contents of the message.
154    pub content: Option<String>,
155
156    /// Open router compatible field
157    /// https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub reasoning: Option<String>,
160
161    /// The tool calls generated by the model, such as function calls.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub tool_calls: Option<Vec<ToolCall>>,
164
165    /// The role of the author of this message.
166    pub role: Role,
167
168    /// The refusal message generated by the model.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub refusal: Option<String>,
171
172    /// Annotations for the message, when applicable, as when using the web search tool.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub annotations: Option<Vec<Annotation>>,
175
176    /// If the audio output modality is requested, this object contains data about the audio response from the model.
177    #[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    /// The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence,
218    /// `length` if the maximum number of tokens specified in the request was reached,
219    /// `content_filter` if content was omitted due to a flag from our content filters,
220    /// `tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.
221    pub finish_reason: Option<FinishReason>,
222    /// Log probability information for the choice.
223    pub logprobs: Option<Logprobs>,
224}
225
226#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
227pub struct Logprobs {
228    /// A list of message content tokens with log probability information.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub content: Option<Vec<LogprobContent>>,
231    /// A list of message refusal tokens with log probability information.
232    #[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    /// The token.
239    pub token: String,
240    /// The log probability of this token, if it is within the top 20 most likely tokens.
241    /// Otherwise, the value -9999.0 is used to signify that the token is very unlikely.
242    pub logprob: f64,
243    /// A list of integers representing the UTF-8 bytes representation of the token.
244    /// Useful in instances where characters are represented by multiple tokens and their
245    /// byte representations must be combined to generate the correct text representation.
246    /// Can be null if there is no bytes representation for the token.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub bytes: Option<Vec<u8>>,
249    /// List of the most likely tokens and their log probability, at this token position.
250    /// In rare cases, there may be fewer than the number of requested top_logprobs returned.
251    #[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    /// The token.
258    pub token: String,
259    /// The log probability of this token, if it is within the top 20 most likely tokens.
260    /// Otherwise, the value -9999.0 is used to signify that the token is very unlikely.
261    pub logprob: f64,
262    /// A list of integers representing the UTF-8 bytes representation of the token.
263    /// Useful in instances where characters are represented by multiple tokens and their
264    /// byte representations must be combined to generate the correct text representation.
265    /// Can be null if there is no bytes representation for the token.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub bytes: Option<Vec<u8>>,
268}
269
270/// Annotation for a message, such as a URL citation when using web search.
271#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
272pub struct Annotation {
273    /// The type of the annotation. Currently, only 'url_citation' is supported.
274    #[serde(rename = "type")]
275    pub annotation_type: String,
276    /// A URL citation when using web search.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub url_citation: Option<AnnotationURLCitation>,
279}
280
281/// A URL citation when using web search.
282#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
283pub struct AnnotationURLCitation {
284    /// The index of the last character of the URL citation in the message.
285    pub end_index: usize,
286    /// The index of the first character of the URL citation in the message.
287    pub start_index: usize,
288    /// The title of the web resource.
289    pub title: String,
290    /// The URL of the web resource.
291    pub url: String,
292}
293
294/// Audio response from the model when audio output modality is requested.
295#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
296pub struct ChatCompletionAudio {
297    /// Unique identifier for this audio response.
298    pub id: String,
299    /// Base64 encoded audio bytes generated by the model, in the format specified in the request.
300    pub data: String,
301    /// The Unix timestamp (in seconds) for when this audio response will no longer be accessible on the server for use in multi-turn conversations.
302    pub expires_at: u64,
303    /// Transcript of the audio generated by the model.
304    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            //test deserialize
697            let actual: Response = serde_json::from_str(json).unwrap();
698            assert_eq!(actual, expected, "deserialize test failed: {}", name);
699            //test serialize
700            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}