use super::{
openai_get, openai_get_with_query, openai_post, ApiResponseOrError, Credentials,
RequestPagination, Usage,
};
use crate::openai_request_stream;
use derive_builder::Builder;
use futures_util::StreamExt;
use reqwest::Method;
use reqwest_eventsource::{CannotCloneRequestError, Event, EventSource};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use tokio::sync::mpsc::{channel, Receiver, Sender};
pub type ChatCompletion = ChatCompletionGeneric<ChatCompletionChoice>;
pub type ChatCompletionDelta = ChatCompletionGeneric<ChatCompletionChoiceDelta>;
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ChatCompletionGeneric<C> {
#[serde(default)]
pub id: String,
#[serde(default)]
pub object: String,
#[serde(default)]
pub created: u64,
#[serde(default)]
pub model: String,
#[serde(default = "default_empty_vec")]
pub choices: Vec<C>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ChatCompletionChoice {
pub index: u64,
pub finish_reason: String,
pub message: ChatCompletionMessage,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ChatCompletionChoiceDelta {
pub index: u64,
pub finish_reason: Option<String>,
pub delta: ChatCompletionMessageDelta,
}
fn is_none_or_empty_vec<T>(opt: &Option<Vec<T>>) -> bool {
opt.as_ref().map(|v| v.is_empty()).unwrap_or(true)
}
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq, Default)]
pub struct ChatCompletionMessage {
pub role: ChatCompletionMessageRole,
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<ChatCompletionFunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "is_none_or_empty_vec")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ChatCompletionMessageDelta {
pub role: Option<ChatCompletionMessageRole>,
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<ChatCompletionFunctionCallDelta>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "is_none_or_empty_vec")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
pub r#type: String,
pub function: ToolCallFunction,
}
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
pub struct ChatCompletionFunctionDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Value>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ChatCompletionFunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct ChatCompletionFunctionCallDelta {
pub name: Option<String>,
pub arguments: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatCompletionMessageRole {
System,
User,
Assistant,
Function,
Tool,
Developer,
}
#[derive(Serialize, Builder, Debug, Clone)]
#[builder(derive(Clone, Debug, PartialEq))]
#[builder(pattern = "owned")]
#[builder(name = "ChatCompletionBuilder")]
#[builder(setter(strip_option, into))]
pub struct ChatCompletionRequest {
model: String,
messages: Vec<ChatCompletionMessage>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
n: Option<u8>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
stop: Vec<String>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
seed: Option<u64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_completion_tokens: Option<u64>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
presence_penalty: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
frequency_penalty: Option<f32>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
logit_bias: Option<HashMap<String, f32>>,
#[builder(default)]
#[serde(skip_serializing_if = "String::is_empty")]
user: String,
#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
functions: Vec<ChatCompletionFunctionDefinition>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
function_call: Option<Value>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
response_format: Option<ChatCompletionResponseFormat>,
#[serde(skip_serializing)]
#[builder(default)]
credentials: Option<Credentials>,
#[builder(default)]
#[serde(skip_serializing_if = "Option::is_none")]
venice_parameters: Option<VeniceParameters>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
pub store: Option<bool>,
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq)]
pub struct VeniceParameters {
pub include_venice_system_prompt: bool,
}
#[derive(Serialize, Debug, Clone, Eq, PartialEq)]
pub struct ChatCompletionResponseFormat {
#[serde(rename = "type")]
typ: String,
}
impl ChatCompletionResponseFormat {
pub fn json_object() -> Self {
ChatCompletionResponseFormat {
typ: "json_object".to_string(),
}
}
pub fn text() -> Self {
ChatCompletionResponseFormat {
typ: "text".to_string(),
}
}
}
impl<C> ChatCompletionGeneric<C> {
pub fn builder(
model: &str,
messages: impl Into<Vec<ChatCompletionMessage>>,
) -> ChatCompletionBuilder {
ChatCompletionBuilder::create_empty()
.model(model)
.messages(messages)
}
}
#[derive(Serialize, Builder, Debug, Clone, Default)]
#[builder(derive(Clone, Debug, PartialEq))]
#[builder(pattern = "owned")]
#[builder(name = "ChatCompletionMessagesRequestBuilder")]
#[builder(setter(strip_option, into))]
pub struct ChatCompletionMessagesRequest {
#[serde(skip_serializing)]
pub completion_id: String,
#[builder(default)]
#[serde(skip_serializing)]
pub credentials: Option<Credentials>,
#[builder(default)]
#[serde(flatten)]
pub pagination: RequestPagination,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ChatCompletionMessages {
pub data: Vec<ChatCompletionMessage>,
pub object: String,
pub first_id: Option<String>,
pub last_id: Option<String>,
pub has_more: bool,
}
impl ChatCompletion {
pub async fn create(request: ChatCompletionRequest) -> ApiResponseOrError<Self> {
let credentials_opt = request.credentials.clone();
openai_post("chat/completions", &request, credentials_opt).await
}
pub async fn get(id: &str, credentials: Credentials) -> ApiResponseOrError<Self> {
let route = format!("chat/completions/{}", id);
openai_get(route.as_str(), Some(credentials)).await
}
}
impl ChatCompletionDelta {
pub async fn create(
request: ChatCompletionRequest,
) -> Result<Receiver<Self>, CannotCloneRequestError> {
let credentials_opt = request.credentials.clone();
let stream = openai_request_stream(
Method::POST,
"chat/completions",
|r| r.json(&request),
credentials_opt,
)
.await?;
let (tx, rx) = channel::<Self>(32);
tokio::spawn(forward_deserialized_chat_response_stream(stream, tx));
Ok(rx)
}
pub fn merge(
&mut self,
other: ChatCompletionDelta,
) -> Result<(), ChatCompletionDeltaMergeError> {
if other.id.ne(&self.id) {
return Err(ChatCompletionDeltaMergeError::DifferentCompletionIds);
}
for other_choice in other.choices.iter() {
for choice in self.choices.iter_mut() {
if choice.index != other_choice.index {
continue;
}
choice.merge(other_choice)?;
}
}
Ok(())
}
}
impl ChatCompletionChoiceDelta {
pub fn merge(
&mut self,
other: &ChatCompletionChoiceDelta,
) -> Result<(), ChatCompletionDeltaMergeError> {
if self.index != other.index {
return Err(ChatCompletionDeltaMergeError::DifferentCompletionChoiceIndices);
}
if self.delta.role.is_none() {
if let Some(other_role) = other.delta.role {
self.delta.role = Some(other_role);
}
}
if self.delta.name.is_none() {
if let Some(other_name) = &other.delta.name {
self.delta.name = Some(other_name.clone());
}
}
match self.delta.content.as_mut() {
Some(content) => {
match &other.delta.content {
Some(other_content) => {
content.push_str(other_content)
}
None => {}
}
}
None => {
match &other.delta.content {
Some(other_content) => {
self.delta.content = Some(other_content.clone());
}
None => {}
}
}
};
match self.delta.function_call.as_mut() {
Some(function_call) => {
match &other.delta.function_call {
Some(other_function_call) => {
match (&mut function_call.arguments, &other_function_call.arguments) {
(Some(function_call), Some(other_function_call)) => {
function_call.push_str(&other_function_call);
}
(None, Some(other_function_call)) => {
function_call.arguments = Some(other_function_call.clone());
}
_ => {}
}
}
None => {}
}
}
None => {
match &other.delta.function_call {
Some(other_function_call) => {
self.delta.function_call = Some(other_function_call.clone());
}
None => {}
}
}
};
Ok(())
}
}
impl From<ChatCompletionDelta> for ChatCompletion {
fn from(delta: ChatCompletionDelta) -> Self {
ChatCompletion {
id: delta.id,
object: delta.object,
created: delta.created,
model: delta.model,
usage: delta.usage,
choices: delta
.choices
.iter()
.map(|choice| ChatCompletionChoice {
index: choice.index,
finish_reason: clone_default_unwrapped_option_string(&choice.finish_reason),
message: ChatCompletionMessage {
role: choice
.delta
.role
.unwrap_or_else(|| ChatCompletionMessageRole::System),
content: choice.delta.content.clone(),
name: choice.delta.name.clone(),
function_call: choice.delta.function_call.clone().map(|f| f.into()),
tool_call_id: None,
tool_calls: Some(Vec::new()),
},
})
.collect(),
}
}
}
impl From<ChatCompletionFunctionCallDelta> for ChatCompletionFunctionCall {
fn from(delta: ChatCompletionFunctionCallDelta) -> Self {
ChatCompletionFunctionCall {
name: delta.name.unwrap_or("".to_string()),
arguments: delta.arguments.unwrap_or_default(),
}
}
}
impl ChatCompletionMessages {
pub fn builder(completion_id: String) -> ChatCompletionMessagesRequestBuilder {
ChatCompletionMessagesRequestBuilder::create_empty()
.completion_id(completion_id.to_string())
}
pub async fn fetch(
request: ChatCompletionMessagesRequest,
) -> ApiResponseOrError<ChatCompletionMessages> {
let route = format!("chat/completions/{}/messages", request.completion_id);
let credentials = request.credentials.clone();
openai_get_with_query(route.as_str(), &request, credentials).await
}
}
#[derive(Debug)]
pub enum ChatCompletionDeltaMergeError {
DifferentCompletionIds,
DifferentCompletionChoiceIndices,
FunctionCallArgumentTypeMismatch,
}
impl std::fmt::Display for ChatCompletionDeltaMergeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChatCompletionDeltaMergeError::DifferentCompletionIds => {
f.write_str("Different completion IDs")
}
ChatCompletionDeltaMergeError::DifferentCompletionChoiceIndices => {
f.write_str("Different completion choice indices")
}
ChatCompletionDeltaMergeError::FunctionCallArgumentTypeMismatch => {
f.write_str("Function call argument type mismatch")
}
}
}
}
impl std::error::Error for ChatCompletionDeltaMergeError {}
async fn forward_deserialized_chat_response_stream(
mut stream: EventSource,
tx: Sender<ChatCompletionDelta>,
) -> anyhow::Result<()> {
while let Some(event) = stream.next().await {
let event = event?;
match event {
Event::Message(event) => {
let completion = serde_json::from_str::<ChatCompletionDelta>(&event.data)?;
tx.send(completion).await?;
}
_ => {}
}
}
Ok(())
}
impl ChatCompletionBuilder {
pub async fn create(self) -> ApiResponseOrError<ChatCompletion> {
ChatCompletion::create(self.build().unwrap()).await
}
pub async fn create_stream(
mut self,
) -> Result<Receiver<ChatCompletionDelta>, CannotCloneRequestError> {
self.stream = Some(Some(true));
ChatCompletionDelta::create(self.build().unwrap()).await
}
}
impl ChatCompletionMessagesRequestBuilder {
pub async fn fetch(self) -> ApiResponseOrError<ChatCompletionMessages> {
ChatCompletionMessages::fetch(self.build().unwrap()).await
}
}
fn clone_default_unwrapped_option_string(string: &Option<String>) -> String {
match string {
Some(value) => value.clone(),
None => "".to_string(),
}
}
impl Default for ChatCompletionMessageRole {
fn default() -> Self {
Self::User
}
}
fn default_empty_vec<C>() -> Vec<C> {
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
use dotenvy::dotenv;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn chat() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_completion = ChatCompletion::builder(
"gpt-3.5-turbo",
[ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Hello!".to_string()),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
}],
)
.temperature(0.0)
.response_format(ChatCompletionResponseFormat::text())
.credentials(credentials)
.create()
.await
.unwrap();
assert_eq!(
chat_completion
.choices
.first()
.unwrap()
.message
.content
.as_ref()
.unwrap(),
"Hello! How can I assist you today?"
);
}
#[tokio::test]
async fn chat_seed() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_completion = ChatCompletion::builder(
"gpt-3.5-turbo",
[ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some(
"What type of seed does Mr. England sow in the song? Reply with 1 word."
.to_string(),
),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
}],
)
.temperature(0.0)
.seed(1337u64)
.credentials(credentials)
.create()
.await
.unwrap();
assert_eq!(
chat_completion
.choices
.first()
.unwrap()
.message
.content
.as_ref()
.unwrap(),
"Love"
);
}
#[tokio::test]
async fn chat_stream() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_stream = ChatCompletion::builder(
"gpt-3.5-turbo",
[ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Hello!".to_string()),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
}],
)
.temperature(0.0)
.credentials(credentials)
.create_stream()
.await
.unwrap();
let chat_completion = stream_to_completion(chat_stream).await;
assert_eq!(
chat_completion
.choices
.first()
.unwrap()
.message
.content
.as_ref()
.unwrap(),
"Hello! How can I assist you today?"
);
}
#[tokio::test]
async fn chat_function() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_stream = ChatCompletion::builder(
"gpt-4o",
[
ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("What is the weather in Boston?".to_string()),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
}
]
).functions([ChatCompletionFunctionDefinition {
description: Some("Get the current weather in a given location.".to_string()),
name: "get_current_weather".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state to get the weather for. (eg: San Francisco, CA)"
}
},
"required": ["location"]
})),
}])
.temperature(0.2)
.credentials(credentials)
.create_stream()
.await
.unwrap();
let chat_completion = stream_to_completion(chat_stream).await;
assert_eq!(
chat_completion
.choices
.first()
.unwrap()
.message
.function_call
.as_ref()
.unwrap()
.name,
"get_current_weather".to_string(),
);
assert_eq!(
serde_json::from_str::<Value>(
&chat_completion
.choices
.first()
.unwrap()
.message
.function_call
.as_ref()
.unwrap()
.arguments
)
.unwrap(),
serde_json::json!({
"location": "Boston, MA"
}),
);
}
#[tokio::test]
async fn chat_response_format_json() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_completion = ChatCompletion::builder(
"gpt-3.5-turbo",
[ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Write an example JSON for a JWT header using RS256".to_string()),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
}],
)
.temperature(0.0)
.seed(1337u64)
.response_format(ChatCompletionResponseFormat::json_object())
.credentials(credentials)
.create()
.await
.unwrap();
let response_string = chat_completion
.choices
.first()
.unwrap()
.message
.content
.as_ref()
.unwrap();
#[derive(Deserialize, Eq, PartialEq, Debug)]
struct Response {
alg: String,
typ: String,
}
let response = serde_json::from_str::<Response>(response_string).unwrap();
assert_eq!(
response,
Response {
alg: "RS256".to_owned(),
typ: "JWT".to_owned()
}
);
}
#[test]
fn builder_clone_and_eq() {
let builder_a = ChatCompletion::builder("gpt-4", [])
.temperature(0.0)
.seed(65u64);
let builder_b = builder_a.clone();
let builder_c = builder_b.clone().temperature(1.0);
let builder_d = ChatCompletionBuilder::default();
assert_eq!(builder_a, builder_b);
assert_ne!(builder_a, builder_c);
assert_ne!(builder_b, builder_c);
assert_ne!(builder_a, builder_d);
assert_ne!(builder_c, builder_d);
}
async fn stream_to_completion(
mut chat_stream: Receiver<ChatCompletionDelta>,
) -> ChatCompletion {
let mut merged: Option<ChatCompletionDelta> = None;
while let Some(delta) = chat_stream.recv().await {
match merged.as_mut() {
Some(c) => {
c.merge(delta).unwrap();
}
None => merged = Some(delta),
};
}
merged.unwrap().into()
}
#[tokio::test]
async fn chat_tool_response_completion() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_completion = ChatCompletion::builder(
"gpt-4o-mini",
[
ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some(
"What's 0.9102847*28456? \
reply in plain text, \
round the number to to 2 decimals \
and reply with the result number only, \
with no full stop at the end"
.to_string(),
),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(Vec::new()),
},
ChatCompletionMessage {
role: ChatCompletionMessageRole::Assistant,
content: Some("Let me calculate that for you.".to_string()),
name: None,
function_call: None,
tool_call_id: None,
tool_calls: Some(vec![ToolCall {
id: "the_tool_call".to_string(),
r#type: "function".to_string(),
function: ToolCallFunction {
name: "mul".to_string(),
arguments: "not_required_to_be_valid_here".to_string(),
},
}]),
},
ChatCompletionMessage {
role: ChatCompletionMessageRole::Tool,
content: Some("the result is 25903.061423199997".to_string()),
name: None,
function_call: None,
tool_call_id: Some("the_tool_call".to_owned()),
tool_calls: Some(Vec::new()),
},
],
)
.temperature(0.0)
.seed(1337u64)
.credentials(credentials)
.create()
.await
.unwrap();
assert_eq!(
chat_completion
.choices
.first()
.unwrap()
.message
.content
.as_ref()
.unwrap(),
"25903.06"
);
}
#[tokio::test]
async fn get_completion() {
dotenv().ok();
let credentials = Credentials::from_env();
let chat_completion = ChatCompletion::builder(
"gpt-3.5-turbo",
[ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Hello!".to_string()),
..Default::default()
}],
)
.credentials(credentials.clone())
.store(true)
.create()
.await
.unwrap();
sleep(Duration::from_secs(7)).await;
let retrieved_completion = ChatCompletion::get(&chat_completion.id, credentials.clone())
.await
.unwrap();
assert_eq!(retrieved_completion, chat_completion);
}
#[tokio::test]
async fn get_completion_non_existent() {
dotenv().ok();
let credentials = Credentials::from_env();
match ChatCompletion::get("non_existent_id", credentials.clone()).await {
Ok(_) => panic!("Expected error"),
Err(e) => assert_eq!(e.code, Some("not_found".to_string())),
}
}
#[tokio::test]
async fn get_completion_messages() {
dotenv().ok();
let credentials = Credentials::from_env();
let user_message = ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Tell me a short joke".to_string()),
..Default::default()
};
let chat_completion = ChatCompletion::builder("gpt-3.5-turbo", [user_message.clone()])
.credentials(credentials.clone())
.store(true)
.create()
.await
.unwrap();
sleep(Duration::from_secs(7)).await;
let retrieved_messages = ChatCompletionMessages::builder(chat_completion.id)
.credentials(credentials.clone())
.fetch()
.await
.unwrap();
assert_eq!(retrieved_messages.data, vec![user_message]);
assert_eq!(retrieved_messages.has_more, false);
}
#[tokio::test]
async fn get_completion_messages_with_pagination() {
dotenv().ok();
let credentials = Credentials::from_env();
let user_message = ChatCompletionMessage {
role: ChatCompletionMessageRole::User,
content: Some("Tell me a short joke".to_string()),
..Default::default()
};
let chat_completion = ChatCompletion::builder("gpt-3.5-turbo", [user_message.clone()])
.credentials(credentials.clone())
.store(true)
.create()
.await
.unwrap();
dbg!(&chat_completion);
sleep(Duration::from_secs(7)).await;
let retrieved_messages1 = ChatCompletionMessages::builder(chat_completion.id.clone())
.credentials(credentials.clone())
.pagination(RequestPagination {
limit: Some(1),
..Default::default()
})
.fetch()
.await
.unwrap();
assert_eq!(retrieved_messages1.data, vec![user_message]);
assert_eq!(retrieved_messages1.has_more, false);
assert!(retrieved_messages1.first_id.is_some());
assert!(retrieved_messages1.last_id.is_some());
let retrieved_messages2 = ChatCompletionMessages::builder(chat_completion.id.clone())
.credentials(credentials.clone())
.pagination(RequestPagination {
limit: Some(1),
after: Some(retrieved_messages1.first_id.unwrap()),
..Default::default()
})
.fetch()
.await
.unwrap();
assert_eq!(retrieved_messages2.data, vec![]);
assert_eq!(retrieved_messages2.has_more, false);
assert!(retrieved_messages2.first_id.is_none());
assert!(retrieved_messages2.last_id.is_none());
}
}