Vibe Engines
YouTube

Lesson 8.4 · 10 min

Ship Chatterbox

The program works on your machine, from inside the project folder. Shipping means a single file you can run from anywhere, and a few lines of text so someone else knows how. Then you are done: Chatterbox v1.0 is the part that talks to a model, finished and installed.

Adds to Chatterboxcargo build --release; the finished project.

Debug build, release build

Until now you used cargo run. That makes a debug build: it compiles fast and runs slowly, with extra checks switched on, and the file lives in target/debug/. When you are ready to hand the program to someone, you ask for the release build, which takes longer to compile and runs faster.

Build the optimised binary (a real run; a first release build also compiles reqwest and its dependencies, so it takes longer)
$ cargo build --release
   Compiling chatterbox v0.1.0
    Finished `release` profile [optimized] target(s) in 2.42s
$ ls -lh target/release/chatterbox
-rwxr-xr-x  1 you  staff   6.3M  target/release/chatterbox

That one file, target/release/chatterbox, is the whole program. You can copy it to another machine of the same kind and run it there with nothing else installed: no Rust, no cargo, no packages. The size is a few megabytes because the HTTP client and the JSON code are inside it. (The 2.42 s in that session is what a rebuild took on the machine it was recorded on; a first release build also compiles the dependencies and takes much longer.) The v0.1.0 in the output is the version field in Cargo.toml, which is separate from the "v1.0" the program prints in its own banner: one names the package, the other is just a string in main.

Install it

Running ./target/release/chatterbox works, but only from this folder. cargo install --path . builds the release binary and puts it in cargo's bin folder, which rustup already added to your PATH. After that, chatterbox is a command like any other.

Install and run from anywhere (a real run in a scratch install directory; the echo replier answers because no key is set)
$ cargo install --path .
  Installing chatterbox v0.1.0
   Compiling chatterbox v0.1.0
    Finished `release` profile [optimized] target(s) in 2m 11s
  Installing ~/.cargo/bin/chatterbox
   Installed package `chatterbox v0.1.0` (executable `chatterbox`)
$ cd ~ && echo hi | chatterbox
Chatterbox v1.0 (type /quit to leave)
bot> you said: hi

Write it down

A program nobody knows how to use is half shipped. A README is a plain text file named README.md in the project folder. It can be short. Answer: what is it, how do I run it, and what does it need?

# Chatterbox

A small command-line chat program written in Rust.

## Run

    cargo install --path .
    chatterbox

Without a key it echoes what you type.

## Talk to a model

    export CHATTERBOX_API_KEY=your-key
    export CHATTERBOX_MODEL=your-model-name
    export CHATTERBOX_URL=https://your-provider/v1/chat/completions
    chatterbox

Type /quit to leave.

What you built

Here is the part of the program that talks to a model, in one file, v1.0. The commands and features from earlier chapters (/help, history, stats, /long, the HashMap, closures, modules and files) are in your project folder as you built them; this listing leaves them out so the model path is easy to see. Most of it you have met: struct and enum from chapter 5, Vec from chapter 6, Result and ? from chapter 7, and the trait, the JSON and the call from this chapter.

Three small things are new. "user".into() converts a &str into the String the field needs, the same job as .to_string(). io::stdin().lock().lines() locks standard input and gives you its lines one at a time (it needs BufRead in scope). io::stdout().flush() pushes any buffered output to the screen right now rather than later, and needs Write in scope.

src/main.rs: Chatterbox v1.0
use serde::{Deserialize, Serialize};
use std::env;
use std::io::{self, BufRead, Write};
 
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Message {
role: String,
content: String,
}
 
#[derive(Serialize)]
struct Request {
model: String,
messages: Vec<Message>,
}
 
#[derive(Deserialize)]
struct Response {
choices: Vec<Choice>,
}
 
#[derive(Deserialize)]
struct Choice {
message: Message,
}
 
#[derive(Debug)]
enum ChatError {
Http(reqwest::Error),
Empty,
}
 
impl From<reqwest::Error> for ChatError {
fn from(e: reqwest::Error) -> Self {
ChatError::Http(e)
}
}
 
impl std::fmt::Display for ChatError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ChatError::Http(e) => write!(f, "the model call failed: {e}"),
ChatError::Empty => write!(f, "nothing to reply to"),
}
}
}
 
trait Replier {
fn reply(&self, history: &[Message]) -> Result<String, ChatError>;
}
 
struct EchoReplier;
 
impl Replier for EchoReplier {
fn reply(&self, history: &[Message]) -> Result<String, ChatError> {
let last = history.last().ok_or(ChatError::Empty)?;
Ok(format!("you said: {}", last.content))
}
}
 
struct ModelReplier {
url: String,
key: String,
model: String,
}
 
impl Replier for ModelReplier {
fn reply(&self, history: &[Message]) -> Result<String, ChatError> {
let body = Request { model: self.model.clone(), messages: history.to_vec() };
let resp: Response = reqwest::blocking::Client::new()
.post(&self.url)
.bearer_auth(&self.key)
.json(&body)
.send()?
.error_for_status()?
.json()?;
let first = resp.choices.into_iter().next().ok_or(ChatError::Empty)?;
Ok(first.message.content)
}
}
 
fn pick_replier() -> Box<dyn Replier> {
match env::var("CHATTERBOX_API_KEY") {
Ok(key) => Box::new(ModelReplier {
url: env::var("CHATTERBOX_URL")
.unwrap_or("https://api.openai.com/v1/chat/completions".to_string()),
key,
model: env::var("CHATTERBOX_MODEL").unwrap_or("gpt-4o-mini".to_string()),
}),
Err(_) => Box::new(EchoReplier),
}
}
 
fn main() {
let replier = pick_replier();
let mut history: Vec<Message> = Vec::new();
println!("Chatterbox v1.0 (type /quit to leave)");
for line in io::stdin().lock().lines() {
let text = line.unwrap();
if text == "/quit" {
break;
}
history.push(Message { role: "user".into(), content: text });
match replier.reply(&history) {
Ok(answer) => {
println!("bot> {answer}");
history.push(Message { role: "assistant".into(), content: answer });
}
Err(e) => println!("bot> (error: {e})"),
}
io::stdout().flush().unwrap();
}
}

Four parts, in order. The data (Message, Request, Response) is what travels. The error is what can go wrong. The repliers are the two ways to answer, hidden behind one trait. And main is a loop that reads a line, keeps the history, asks the replier and prints. Swap the replier and nothing else moves. That is the point of everything since chapter 4: small pieces with clear owners and clear promises.

Where to go next

You now have a program that does a real thing, which is a better place to learn from than any exercise. Here are good next steps, roughly in order of how much they will teach you.

  • Make it yours. Add a /clear command that empties the history. Add a system message at the start. Print how many messages the history holds.
  • Read the Rust Book. The official book is free. You have now met most of its first half in a different order, so chapters on ownership and traits will read like a second pass.
  • Streaming and async. Real chat interfaces show words as they arrive. That needs async Rust and the tokio crate, which is the natural next topic after blocking calls.
  • Write tests. Every exercise ran against #[test] functions. Add a few to Chatterbox for EchoReplier and for parsing a reply.

You started with "what is a program". You finish with a program you wrote, built, installed and shipped.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What is the difference between <code>cargo run</code> and <code>cargo build --release</code>?Reveal
cargo run makes a debug build that compiles quickly and runs slowly. --release takes longer to compile and produces a faster binary in target/release/.
What does <code>cargo install --path .</code> do?Reveal
It builds the project in release mode and copies the binary into cargo's bin folder, so you can run it by name from any folder.
Which part of the final program would change if you wanted a different model provider?Reveal
Only the environment variables that feed ModelReplier (URL, key and model name). The loop and the trait stay the same.

That is lesson 8.4. Tick it off so the course remembers where you are.

Questions people ask

How do I build a Rust program for release?

Run cargo build --release in the project folder. The optimised binary is written to target/release/.

How do I install my own Rust program?

Run cargo install --path . in the project folder. The program is built and copied into the cargo bin folder, which is normally already on your PATH.

Can I give the binary to someone else?

Yes, as long as they use the same kind of machine (same operating system and processor). The file needs no Rust installation to run.

Learn Rust by building a chat app · Lesson 8.4 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026