Skip to content

Commit ae1970d

Browse files
authored
Merge pull request #186 from LeadcodeDev/main
feat: implement `v1/chat/completions` endpoint as stream consumption
2 parents 6a62547 + b27fc91 commit ae1970d

File tree

12 files changed

+694
-278
lines changed

12 files changed

+694
-278
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ default-tls = ["reqwest/default-tls", "tokio-tungstenite/native-tls"]
1717
[dependencies.reqwest]
1818
version = "0.12"
1919
default-features = false
20-
features = ["charset", "http2", "json", "multipart", "socks"]
20+
features = ["charset", "http2", "json", "multipart", "socks", "stream"]
2121

2222
[dependencies.tokio]
2323
version = "1"

examples/chat_completion.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest};
2+
use openai_api_rs::v1::chat_completion::chat_completion::ChatCompletionRequest;
3+
use openai_api_rs::v1::chat_completion::{self};
34
use openai_api_rs::v1::common::GPT4_O_MINI;
45
use std::env;
56

examples/chat_completion_stream.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use futures_util::StreamExt;
2+
use openai_api_rs::v1::api::OpenAIClient;
3+
use openai_api_rs::v1::chat_completion::chat_completion_stream::{
4+
ChatCompletionStreamRequest, ChatCompletionStreamResponse,
5+
};
6+
use openai_api_rs::v1::chat_completion::{self};
7+
use openai_api_rs::v1::common::GPT4_O_MINI;
8+
use std::env;
9+
10+
#[tokio::main]
11+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
12+
let api_key = env::var("OPENAI_API_KEY").unwrap().to_string();
13+
let mut client = OpenAIClient::builder().with_api_key(api_key).build()?;
14+
15+
let req = ChatCompletionStreamRequest::new(
16+
GPT4_O_MINI.to_string(),
17+
vec![chat_completion::ChatCompletionMessage {
18+
role: chat_completion::MessageRole::user,
19+
content: chat_completion::Content::Text(String::from("What is bitcoin?")),
20+
name: None,
21+
tool_calls: None,
22+
tool_call_id: None,
23+
}],
24+
);
25+
26+
let mut result = client.chat_completion_stream(req).await?;
27+
while let Some(response) = result.next().await {
28+
match response.clone() {
29+
ChatCompletionStreamResponse::ToolCall(toolcalls) => {
30+
println!("Tool Call: {:?}", toolcalls);
31+
}
32+
ChatCompletionStreamResponse::Content(content) => {
33+
println!("Content: {:?}", content);
34+
}
35+
ChatCompletionStreamResponse::Done => {
36+
println!("Done");
37+
}
38+
}
39+
}
40+
41+
Ok(())
42+
}
43+
44+
// OPENAI_API_KEY=xxxx cargo run --package openai-api-rs --example chat_completion_stream

examples/function_call.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest};
2+
use openai_api_rs::v1::chat_completion::{
3+
chat_completion::ChatCompletionRequest, ChatCompletionMessage,
4+
};
5+
use openai_api_rs::v1::chat_completion::{
6+
Content, FinishReason, MessageRole, Tool, ToolChoiceType, ToolType,
7+
};
38
use openai_api_rs::v1::common::GPT4_O;
49
use openai_api_rs::v1::types;
510
use serde::{Deserialize, Serialize};
@@ -32,16 +37,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
3237

3338
let req = ChatCompletionRequest::new(
3439
GPT4_O.to_string(),
35-
vec![chat_completion::ChatCompletionMessage {
36-
role: chat_completion::MessageRole::user,
37-
content: chat_completion::Content::Text(String::from("What is the price of Ethereum?")),
40+
vec![ChatCompletionMessage {
41+
role: MessageRole::user,
42+
content: Content::Text(String::from("What is the price of Ethereum?")),
3843
name: None,
3944
tool_calls: None,
4045
tool_call_id: None,
4146
}],
4247
)
43-
.tools(vec![chat_completion::Tool {
44-
r#type: chat_completion::ToolType::Function,
48+
.tools(vec![Tool {
49+
r#type: ToolType::Function,
4550
function: types::Function {
4651
name: String::from("get_coin_price"),
4752
description: Some(String::from("Get the price of a cryptocurrency")),
@@ -52,7 +57,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5257
},
5358
},
5459
}])
55-
.tool_choice(chat_completion::ToolChoiceType::Auto);
60+
.tool_choice(ToolChoiceType::Auto);
5661

5762
// debug request json
5863
// let serialized = serde_json::to_string(&req).unwrap();
@@ -65,14 +70,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
6570
println!("No finish_reason");
6671
println!("{:?}", result.choices[0].message.content);
6772
}
68-
Some(chat_completion::FinishReason::stop) => {
73+
Some(FinishReason::stop) => {
6974
println!("Stop");
7075
println!("{:?}", result.choices[0].message.content);
7176
}
72-
Some(chat_completion::FinishReason::length) => {
77+
Some(FinishReason::length) => {
7378
println!("Length");
7479
}
75-
Some(chat_completion::FinishReason::tool_calls) => {
80+
Some(FinishReason::tool_calls) => {
7681
println!("ToolCalls");
7782
#[derive(Deserialize, Serialize)]
7883
struct Currency {
@@ -90,10 +95,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9095
}
9196
}
9297
}
93-
Some(chat_completion::FinishReason::content_filter) => {
98+
Some(FinishReason::content_filter) => {
9499
println!("ContentFilter");
95100
}
96-
Some(chat_completion::FinishReason::null) => {
101+
Some(FinishReason::null) => {
97102
println!("Null");
98103
}
99104
}

examples/function_call_role.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest};
2+
use openai_api_rs::v1::chat_completion::chat_completion::ChatCompletionRequest;
3+
use openai_api_rs::v1::chat_completion::{self};
34
use openai_api_rs::v1::common::GPT4_O;
45
use openai_api_rs::v1::types;
56
use serde::{Deserialize, Serialize};

examples/openrouter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest};
2+
use openai_api_rs::v1::chat_completion::chat_completion::ChatCompletionRequest;
3+
use openai_api_rs::v1::chat_completion::{self};
34
use openai_api_rs::v1::common::GPT4_O_MINI;
45
use std::env;
56

examples/openrouter_reasoning.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{
3-
self, ChatCompletionRequest, Reasoning, ReasoningEffort, ReasoningMode,
4-
};
2+
use openai_api_rs::v1::chat_completion::chat_completion::ChatCompletionRequest;
3+
use openai_api_rs::v1::chat_completion::{self, Reasoning, ReasoningEffort, ReasoningMode};
54
use std::env;
65

76
#[tokio::main]

examples/vision.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use openai_api_rs::v1::api::OpenAIClient;
2-
use openai_api_rs::v1::chat_completion::{self, ChatCompletionRequest};
2+
use openai_api_rs::v1::chat_completion::chat_completion::ChatCompletionRequest;
3+
use openai_api_rs::v1::chat_completion::{self};
34
use openai_api_rs::v1::common::GPT4_O;
45
use std::env;
56

src/v1/api.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use crate::v1::audio::{
77
AudioTranslationRequest, AudioTranslationResponse,
88
};
99
use crate::v1::batch::{BatchResponse, CreateBatchRequest, ListBatchResponse};
10-
use crate::v1::chat_completion::{ChatCompletionRequest, ChatCompletionResponse};
10+
use crate::v1::chat_completion::chat_completion::{ChatCompletionRequest, ChatCompletionResponse};
11+
use crate::v1::chat_completion::chat_completion_stream::{
12+
ChatCompletionStream, ChatCompletionStreamRequest, ChatCompletionStreamResponse,
13+
};
1114
use crate::v1::common;
1215
use crate::v1::completion::{CompletionRequest, CompletionResponse};
1316
use crate::v1::edit::{EditRequest, EditResponse};
@@ -39,11 +42,12 @@ use crate::v1::run::{
3942
use crate::v1::thread::{CreateThreadRequest, ModifyThreadRequest, ThreadObject};
4043

4144
use bytes::Bytes;
45+
use futures_util::Stream;
4246
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
4347
use reqwest::multipart::{Form, Part};
4448
use reqwest::{Client, Method, Response};
4549
use serde::Serialize;
46-
use serde_json::Value;
50+
use serde_json::{to_value, Value};
4751
use url::Url;
4852

4953
use std::error::Error;
@@ -334,6 +338,40 @@ impl OpenAIClient {
334338
self.post("chat/completions", &req).await
335339
}
336340

341+
pub async fn chat_completion_stream(
342+
&mut self,
343+
req: ChatCompletionStreamRequest,
344+
) -> Result<impl Stream<Item = ChatCompletionStreamResponse>, APIError> {
345+
let mut payload = to_value(&req).map_err(|err| APIError::CustomError {
346+
message: format!("Failed to serialize request: {}", err),
347+
})?;
348+
349+
if let Some(obj) = payload.as_object_mut() {
350+
obj.insert("stream".into(), Value::Bool(true));
351+
}
352+
353+
let request = self.build_request(Method::POST, "chat/completions").await;
354+
let request = request.json(&payload);
355+
let response = request.send().await?;
356+
357+
if response.status().is_success() {
358+
Ok(ChatCompletionStream {
359+
response: Box::pin(response.bytes_stream()),
360+
buffer: String::new(),
361+
first_chunk: true,
362+
})
363+
} else {
364+
let error_text = response
365+
.text()
366+
.await
367+
.unwrap_or_else(|_| String::from("Unknown error"));
368+
369+
Err(APIError::CustomError {
370+
message: error_text,
371+
})
372+
}
373+
}
374+
337375
pub async fn audio_transcription(
338376
&mut self,
339377
req: AudioTranscriptionRequest,

0 commit comments

Comments
 (0)