Rust for Blazing Fast ChatGPT Calls — A Beginner’s Guide

Yong kang Chia
7 min readNov 9, 2023
Blazing Fast

Are you bored of using the clunky Python code snippets in the OpenAI docs? Craving a faster, more reliable solution for integrating with AI? Well, look no further than Rust!

Rust’s performance and reliability blow Python out of the water. Its strict compiler catches errors at compile-time, eliminating pesky bugs. And Rust’s lightweight threads allow concurrent requests without any callback hell.

In this article, we will develop a straightforward Rust application that leverages OpenAI’s GPT-3.5-Turbo by implementing a basic LeetCode problem in Rust. Let’s #RewriteInRust!

Why Rust?

So you might ask, why do we use Rust instead of just sticking with the same old Python for our prototype?

The adoption of Rust extends beyond mere curiosity or desire for novelty; it is rooted in significant advantages that make it an optimal choice over traditional languages like Python. Rust’s programs have a low memory footprint and efficient CPU usage, which is reflected in its “blazing fast” speed.

However, the most compelling reason to use Rust is its focus on safety. It has a unique ownership system that automatically manages memory, reducing the likelihood of common programming errors like null pointer dereferencing and buffer overflows. By enforcing strict borrowing and lifetime rules at compile time, Rust ensures safe concurrency without data races, which can be a significant advantage in multi-threaded programming.

Thus, Rust offers a unique blend of performance, safety, and efficiency, making it a worthy choice for modern-day programming.

This video does a really good job of explaining the benefits of rust. In summary, Rust is environmentally friendly, fast, and most importantly safe.

Prerequisites

Before we get started, you’ll need to have the following:

Setup

We’ll start by creating a new Rust project. Open a terminal and run the following command:

cargo new gpt_leetcode_solver

This command creates a new directory called gpt_leetcode_solver with a basic Rust project. Navigate into this directory:

cd gpt_leetcode_solver

You will need the reqwest and serde crates for this project. Add them to your Cargo.toml file:

[dependencies]
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.14.0", features = ["full", "macros"] }
dotenv = "0.15.0"

Now, let’s write the Rust code.

The Rust Code

In the src directory, replace the content of the main.rs file with the following code:

use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE};
use reqwest::Client;
use std::env;

use serde_derive::{Deserialize, Serialize};

// Insert the rest of the code here

This code sets up the necessary imports for our program. It includes libraries for making HTTP requests, for deserializing and serializing JSON, and for accessing environment variables.

Let’s add the struct definitions:

#[derive(Serialize, Deserialize, Debug)]
struct OAIRequest {
model: String,
messages: Vec<Message>,
}

#[derive(Debug, Deserialize)]
struct OAIResponse {
id: Option<String>,
object: Option<String>,
created: Option<i64>,
model: Option<String>,
system_fingerprint: Option<String>,
choices: Vec<Choice>,
usage: Option<Usage>,
}

#[derive(Debug, Deserialize)]
struct Usage {
prompt_tokens: i32,
completion_tokens: i32,
total_tokens: i32,
}

#[derive(Debug, Deserialize)]
struct Choice {
index: i32,
message: Message,
finish_reason: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String,
content: String,
}

These structs are used to define the structure of the request and response data for the OpenAI API.

Why might we want to model structs in Rust?

Modeling business logic with structs in Rust offers several benefits.

Firstly, structs provide a way to organize related data fields, making the code more readable and maintainable.

Secondly, structs enable data validation by defining field types and constraints, enforced by Rust’s strong typing and type system.

Thirdly, structs facilitate encapsulation and abstraction, allowing the logic to be contained within the struct’s implementation, promoting modularity and separation of concerns. Additionally, structs support composition and relationships between entities or concepts. They simplify serialization and deserialization of data, easing integration with external systems.

Lastly, modeling with structs improves code readability and maintainability, providing a clear structure and language for discussing and reasoning about the business logic.

Now, let’s add the main function openai_query which will send our problem to the OpenAI API:

pub async fn openai_query(text: &str) -> Result<String, Box<dyn std::error::Error>> {
// Insert the rest of the function code here
}

Before we fill out this function, let’s define the constants it will use:

const URI: &str = "https://api.openai.com/v1/chat/completions";
const MODEL: &str = "gpt-3.5-turbo";

Next, within the openai_query function, we obtain the OpenAI API key from the environment:

let oai_token = env::var("OPENAI_API").expect("OPENAI_API must be set");

We then set up the HTTP client and headers:

let client = Client::new();

let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
headers.insert(
AUTHORIZATION,
format!("Bearer {}", oai_token).parse().unwrap(),
);

Then, we set up the system message and the user message, which is the LeetCode problem:

let prompt_message: Message = Message {
role: String::from("system"),
content: String::from("You're a helpful assistant. Solve the following LeetCode problem:\n\n"),
};

let req = OAIRequest {
model: String::from(MODEL),
messages: vec![
prompt_message,
Message {
role: String::from("user"),
content: String::from(text),
},
],
};

Finally, we make the request to the OpenAI API and process the response:

let res = client
.post(URI)
.headers(headers)
.json(&req)
.send()
.await?
.json::<OAIResponse>()
.await?;

// extract out the last index of the choices vector and get the message
let message = res.choices.last().ok_or("No choices returned")?.message.content.clone();

Ok(message)

This sends the request to the OpenAI API and then waits for the response. It then extracts the content of the last message from the choices in the response.

The final version of the openai_query function, including all necessary dependencies and struct definitions, is as follows:

use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE};
use reqwest::Client;
use std::env;
use serde_derive::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct OAIRequest {
model: String,
messages: Vec<Message>,
}

#[derive(Debug, Deserialize)]
struct OAIResponse {
choices: Vec<Choice>,
}

#[derive(Debug, Deserialize)]
struct Choice {
message: Message,
}

#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String,
content: String,
}

const URI: &str = "https://api.openai.com/v1/chat/completions";
const MODEL: &str = "gpt-3.5-turbo";

pub async fn openai_query(text: &str) -> Result<String, Box<dyn std::error::Error>> {
let oai_token = env::var("OPENAI_API").expect("OPENAI_API must be set");

// Setup of HTTP Client
let client = Client::new();

let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
headers.insert(
AUTHORIZATION,
format!("Bearer {}", oai_token).parse().unwrap(),
);

let prompt_message: Message = Message {
role: String::from("system"),
content: String::from("You're a Leetcode pro. Solve the following LeetCode problem:\n\n"),
};

let req = OAIRequest {
model: String::from(MODEL),
messages: vec![
prompt_message,
Message {
role: String::from("user"),
content: String::from(text),
},
],
};

let res = client
.post(URI)
.headers(headers)
.json(&req)
.send()
.await?
.json::<OAIResponse>()
.await?;

let message = res.choices.last().ok_or("No choices returned")?.message.content.clone();

Ok(message)
}

Testing

In Rust, the idiomatic way to test your code is to place the unit tests in the same file as the code they are testing. This convention is recommended by The Rust Programming Language. By placing the tests in the same file, you can easily associate the tests with the code they are testing, making it more readable and maintainable. To indicate that a module contains tests, you can annotate it with cfg(test). This approach allows you to keep the tests close to the code they are testing, making it easier to understand and modify the code and tests together. Additionally, having the tests in the same file can help improve compilation times.

Let’s add a test case to check if our function is working correctly. Add this at the bottom of your main.rs:

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_openai_query() {
let text = String::from("Given an array of integers nums = [2,7,11,15] and an integer target = 9, return indices of the two numbers such that they add up to target.");
let res = openai_query(&text).await.unwrap();
println!("{}", res);
assert_eq!(res, String::from("[0,1]"));
}
}

This test case sends a specific 2-sum problem to OpenAI and checks if the response matches the expected output. We're solving the 2-sum problem where the input is nums = [2,7,11,15] and target = 9, we expect the output to be [0,1].

Calling from Main and Executing the Program

Now that we have our openai_query function implemented and tested, we can use it in our main program.

In your main.rs, add a main function if it doesn’t already exist. This function is the entry point to your program. It should look something like this:

#[tokio::main]
async fn main() {
// Main program logic goes here
}

The #[tokio::main] attribute is a procedural macro that allows you to write asynchronous code in your main function.

Inside the main function, you can call the `openai_query` function. For instance, if you want to solve a specific 2-sum problem, you can do the following:

#[tokio::main]
async fn main() {
let problem = "Given an array of integers nums = [2,7,11,15] and an integer target = 9, return indices of the two numbers such that they add up to target.";
match openai_query(problem).await {
Ok(solution) => println!("Solution: {}", solution),
Err(e) => eprintln!("Error: {}", e),
}
}

This code sends the problem to the OpenAI API and then prints out the solution. If there’s an error, it prints out the error message.

To run your program, open a terminal in your project directory. Since your application requires an environment variable, you should run your program with the `OPENAI_API` environment variable set to your OpenAI API key:

OPENAI_API=your_api_key cargo run

This builds and runs your application. You should see the solution to your problem printed on the console.

Keep in mind that you should replace your_api_key with your actual OpenAI API key. If you’re planning on sharing your code or deploying it to a public server, make sure to keep your API key private. One common approach is to set your API keys as environment variables on your server or development machine.

Check the full code here!

Full Code

Thank you for following, and folks that is how you program ChatGPT calls in Rust from scratch. Stay Rusty!

--

--

Yong kang Chia

Blockchain Developer. Chainlink Ex Spartan Group, Ex Netherminds