Skip to content

Commit a8e54f9

Browse files
authored
Merge pull request #2 from opensass/completions
feat: impl deez completions
2 parents ac9f487 + 1aa89fd commit a8e54f9

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-0
lines changed

completions

Whitespace-only changes.

src/completions.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! Reference: https://docs.x.ai/api/endpoints#chat-completions
2+
3+
use crate::error::XaiError;
4+
use crate::traits::ChatCompletionsFetcher;
5+
use crate::traits::ClientConfig;
6+
use serde::{Deserialize, Serialize};
7+
8+
#[derive(Debug, Clone, Serialize, Deserialize)]
9+
pub struct ChatCompletionRequest {
10+
pub model: String,
11+
pub messages: Vec<Message>,
12+
pub temperature: Option<f32>,
13+
pub max_tokens: Option<u32>,
14+
pub frequency_penalty: Option<f32>,
15+
pub presence_penalty: Option<f32>,
16+
pub n: Option<u32>,
17+
pub stop: Option<Vec<String>>,
18+
pub stream: Option<bool>,
19+
pub logprobs: Option<bool>,
20+
pub top_p: Option<f32>,
21+
pub top_logprobs: Option<u32>,
22+
pub seed: Option<u32>,
23+
pub user: Option<String>,
24+
}
25+
26+
#[derive(Debug, Clone, Serialize, Deserialize)]
27+
pub struct Message {
28+
pub role: String,
29+
pub content: String,
30+
}
31+
32+
#[derive(Debug, Clone, Serialize, Deserialize)]
33+
pub struct ChatCompletionResponse {
34+
pub id: String,
35+
pub object: String,
36+
pub created: u64,
37+
pub model: String,
38+
pub choices: Vec<Choice>,
39+
pub usage: Option<Usage>,
40+
pub system_fingerprint: Option<String>,
41+
}
42+
43+
#[derive(Debug, Clone, Serialize, Deserialize)]
44+
pub struct Choice {
45+
pub index: u32,
46+
pub message: Message,
47+
pub finish_reason: String,
48+
}
49+
50+
#[derive(Debug, Clone, Serialize, Deserialize)]
51+
pub struct Usage {
52+
pub prompt_tokens: u32,
53+
pub completion_tokens: u32,
54+
pub total_tokens: u32,
55+
}
56+
57+
#[derive(Debug, Clone)]
58+
pub struct ChatCompletionsRequestBuilder<T: ClientConfig + Clone + Send + Sync> {
59+
client: T,
60+
request: ChatCompletionRequest,
61+
}
62+
63+
impl<T> ChatCompletionsRequestBuilder<T>
64+
where
65+
T: ClientConfig + Clone + Send + Sync,
66+
{
67+
pub fn new(client: T, model: String, messages: Vec<Message>) -> Self {
68+
Self {
69+
client,
70+
request: ChatCompletionRequest {
71+
model,
72+
messages,
73+
temperature: None,
74+
max_tokens: None,
75+
frequency_penalty: None,
76+
presence_penalty: None,
77+
n: None,
78+
stop: None,
79+
stream: None,
80+
logprobs: None,
81+
top_p: None,
82+
top_logprobs: None,
83+
seed: None,
84+
user: None,
85+
},
86+
}
87+
}
88+
89+
pub fn temperature(mut self, temperature: f32) -> Self {
90+
self.request.temperature = Some(temperature);
91+
self
92+
}
93+
94+
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
95+
self.request.max_tokens = Some(max_tokens);
96+
self
97+
}
98+
99+
pub fn n(mut self, n: u32) -> Self {
100+
self.request.n = Some(n);
101+
self
102+
}
103+
104+
pub fn stop(mut self, stop: Vec<String>) -> Self {
105+
self.request.stop = Some(stop);
106+
self
107+
}
108+
109+
pub fn build(self) -> Result<ChatCompletionRequest, XaiError> {
110+
Ok(self.request)
111+
}
112+
}
113+
114+
impl<T> ChatCompletionsFetcher for ChatCompletionsRequestBuilder<T>
115+
where
116+
T: ClientConfig + Clone + Send + Sync,
117+
{
118+
async fn create_chat_completion(
119+
&self,
120+
request: ChatCompletionRequest,
121+
) -> Result<ChatCompletionResponse, XaiError> {
122+
let response = self
123+
.client
124+
.request(reqwest::Method::POST, "chat/completions")?
125+
.json(&request)
126+
.send()
127+
.await?;
128+
129+
if response.status().is_success() {
130+
let chat_completion = response.json::<ChatCompletionResponse>().await?;
131+
Ok(chat_completion)
132+
} else {
133+
Err(XaiError::Http(
134+
response.error_for_status().unwrap_err().to_string(),
135+
))
136+
}
137+
}
138+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod api_key;
22
pub mod client;
3+
pub mod completions;
34
pub mod error;
45
pub mod traits;
56

src/traits.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#![allow(async_fn_in_trait)]
22

33
use crate::api_key::ApiKeyInfo;
4+
use crate::completions::ChatCompletionRequest;
5+
use crate::completions::ChatCompletionResponse;
46
use crate::error::XaiError;
57
use reqwest::{Method, RequestBuilder};
68

@@ -13,3 +15,10 @@ pub trait ClientConfig {
1315
pub trait ApiKeyFetcher {
1416
async fn fetch_api_key_info(&self) -> Result<ApiKeyInfo, XaiError>;
1517
}
18+
19+
pub trait ChatCompletionsFetcher {
20+
async fn create_chat_completion(
21+
&self,
22+
request: ChatCompletionRequest,
23+
) -> Result<ChatCompletionResponse, XaiError>;
24+
}

tests/completions.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use mockito::{Matcher, Server};
2+
use reqwest::Method;
3+
use serde_json::json;
4+
use x_ai::client::XaiClient;
5+
use x_ai::traits::ClientConfig;
6+
7+
#[tokio::test]
8+
async fn test_chat_completions() {
9+
let mut server = Server::new_async().await;
10+
11+
let chat_completion_mock = server
12+
.mock("POST", "/v1/chat/completions")
13+
.match_header("Content-Type", "application/json")
14+
.match_body(Matcher::JsonString(r#"
15+
{
16+
"messages": [
17+
{
18+
"role": "system",
19+
"content": "You are Grok, a chatbot inspired by the Hitchhikers Guide to the Galaxy."
20+
},
21+
{
22+
"role": "user",
23+
"content": "What is the answer to life and universe?"
24+
}
25+
],
26+
"model": "grok-beta",
27+
"stream": false,
28+
"temperature": 0
29+
}
30+
"#.to_string()))
31+
.with_status(200)
32+
.with_header("Content-Type", "application/json")
33+
.with_body(r#"
34+
{
35+
"id": "304e12ef-81f4-4e93-a41c-f5f57f6a2b56",
36+
"object": "chat.completion",
37+
"created": 1728511727,
38+
"model": "grok-beta",
39+
"choices": [
40+
{
41+
"index": 0,
42+
"message": {
43+
"role": "assistant",
44+
"content": "The answer to the ultimate question of life, the universe, and everything is **42**, according to Douglas Adams science fiction series \"The Hitchhiker's Guide to the Galaxy.\" This number is often humorously referenced in discussions about the meaning of life. However, in the context of the story, the actual question to which 42 is the answer remains unknown, symbolizing the ongoing search for understanding the purpose or meaning of existence."
45+
},
46+
"finish_reason": "stop"
47+
}
48+
],
49+
"usage": {
50+
"prompt_tokens": 24,
51+
"completion_tokens": 91,
52+
"total_tokens": 115
53+
},
54+
"system_fingerprint": "fp_3813298403"
55+
}
56+
"#)
57+
.create_async()
58+
.await;
59+
60+
let client = XaiClient::builder()
61+
.base_url(&format!("{}/", server.url()))
62+
.build()
63+
.expect("Failed to build XaiClient");
64+
65+
client.set_api_key("test-api-key".to_string());
66+
67+
let body = json!({
68+
"messages": [
69+
{
70+
"role": "system",
71+
"content": "You are Grok, a chatbot inspired by the Hitchhikers Guide to the Galaxy."
72+
},
73+
{
74+
"role": "user",
75+
"content": "What is the answer to life and universe?"
76+
}
77+
],
78+
"model": "grok-beta",
79+
"stream": false,
80+
"temperature": 0
81+
});
82+
83+
let result = client
84+
.request(Method::POST, "/v1/chat/completions")
85+
.expect("body")
86+
.json(&body)
87+
.send()
88+
.await;
89+
90+
assert!(result.is_ok());
91+
let response = result.unwrap();
92+
assert_eq!(response.status(), 200);
93+
94+
let response_text = response.text().await.unwrap();
95+
assert_eq!(
96+
response_text,
97+
r#"
98+
{
99+
"id": "304e12ef-81f4-4e93-a41c-f5f57f6a2b56",
100+
"object": "chat.completion",
101+
"created": 1728511727,
102+
"model": "grok-beta",
103+
"choices": [
104+
{
105+
"index": 0,
106+
"message": {
107+
"role": "assistant",
108+
"content": "The answer to the ultimate question of life, the universe, and everything is **42**, according to Douglas Adams science fiction series \"The Hitchhiker's Guide to the Galaxy.\" This number is often humorously referenced in discussions about the meaning of life. However, in the context of the story, the actual question to which 42 is the answer remains unknown, symbolizing the ongoing search for understanding the purpose or meaning of existence."
109+
},
110+
"finish_reason": "stop"
111+
}
112+
],
113+
"usage": {
114+
"prompt_tokens": 24,
115+
"completion_tokens": 91,
116+
"total_tokens": 115
117+
},
118+
"system_fingerprint": "fp_3813298403"
119+
}
120+
"#
121+
);
122+
123+
chat_completion_mock.assert_async().await;
124+
}

0 commit comments

Comments
 (0)