openai-sdk-rs

Unofficial, minimal, async OpenAI API client for Rust.
⚠️ Disclaimer: This is an unofficial implementation and is not affiliated with OpenAI. Use at your own discretion.
✨ Features
What it covers:
- Chat Completions:
POST /v1/chat/completions
- Embeddings:
POST /v1/embeddings
- Streaming for Chat Completions (SSE)
- Responses API (+ streaming) - Full support with tool calling
- Images generation
- Files list + upload (multipart)
- Advanced configurations - Temperature, max tokens, reasoning, etc.
Not a full mirror of the API yet — intentionally small and focused.
🚀 Quick Start
📚 Examples
Check out the examples/
directory for comprehensive usage examples:
cargo run --example chat
- Basic chat completion
cargo run --example chat_stream
- Streaming chat
cargo run --example responses
- Basic responses API
cargo run --example responses_stream
- Streaming responses
cargo run --example responses_tool_call
- Tool calling with functions
cargo run --example responses_advanced
- Advanced parameters and configurations
cargo run --example images
- Image generation
Install
Add to your Cargo.toml
:
[dependencies]
openai-sdk-rs = { path = "." }
Or once published:
openai-sdk-rs = "0.1"
Configure
Copy .env.example
to .env
and fill your values, or export env vars directly.
export OPENAI_API_KEY=sk-...
export OPENAI_ORG_ID=org_...
export OPENAI_PROJECT_ID=proj_...
To use a proxy-compatible base URL:
export OPENAI_BASE_URL=https://api.openai.com/v1
📦 Installation
Add this to your Cargo.toml
:
[dependencies]
openai-sdk-rs = "0.1.0"
tokio = { version = "1.0", features = ["full"] }
Or install via cargo:
cargo add openai-sdk-rs
🚀 Quick Start
You'll need an OpenAI API key. Set it as an environment variable:
export OPENAI_API_KEY="your-api-key-here"
🔧 Usage Examples
Chat:
use openai_sdk_rs::{OpenAI, types::chat::{ChatMessage, ChatCompletionRequest}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let req = ChatCompletionRequest {
model: "gpt-4o-mini".to_string(),
messages: vec![
ChatMessage::system("You are a helpful assistant."),
ChatMessage::user("Write a haiku about Rust."),
],
..Default::default()
};
let resp = client.chat_completion(req).await?;
println!("{}", resp.first_choice_text().unwrap_or("<no text>"));
Ok(())
}
Embeddings:
use openai_sdk_rs::{OpenAI, types::embeddings::{EmbeddingsRequest, EmbeddingInput}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let req = EmbeddingsRequest { model: "text-embedding-3-small".into(), input: EmbeddingInput::from("hello world"), user: None };
let resp = client.embeddings(req).await?;
println!("{} vectors", resp.data.len());
Ok(())
}
Streaming chat:
use openai_sdk_rs::{OpenAI, types::chat::{ChatMessage, ChatCompletionRequest}};
use futures_util::TryStreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let req = ChatCompletionRequest { model: "gpt-4o-mini".into(), messages: vec![ChatMessage::user("Stream a short line.")], ..Default::default() };
let mut stream = client.chat_completion_stream(req).await?;
while let Some(chunk) = stream.try_next().await? {
if let Some(text) = chunk.choices.get(0).and_then(|c| c.delta.content.as_deref()) {
print!("{}", text);
}
}
println!();
Ok(())
}
Responses API:
use openai_sdk_rs::{OpenAI, types::responses::ResponsesRequest};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let resp = client.responses(ResponsesRequest::text("gpt-4o-mini", "One sentence about Rust.")).await?;
println!("{}", resp.output_text().unwrap_or("<no text>".into()));
Ok(())
}
Streaming Responses:
use openai_sdk_rs::{OpenAI, types::responses::{ResponsesRequest, StreamOptions}};
use futures_util::TryStreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let mut req = ResponsesRequest::text("gpt-4o-mini", "Stream a short fact about whales.");
req.stream_options = Some(StreamOptions { include_usage: Some(true) });
let mut stream = client.responses_stream(req).await?;
while let Some(event) = stream.try_next().await? {
if let Some(text) = event.output_text {
print!("{}", text);
}
}
println!();
Ok(())
}
Aggregated streaming helpers:
use openai_sdk_rs::{OpenAI, types::chat::{ChatMessage, ChatCompletionRequest}, types::responses::ResponsesRequest};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let chat_text = client.chat_completion_stream_text(ChatCompletionRequest {
model: "gpt-4o-mini".into(),
messages: vec![ChatMessage::user("Say something short.")],
..Default::default()
}).await?;
println!("chat: {}", chat_text);
let resp_text = client.responses_stream_text(ResponsesRequest::text("gpt-4o-mini", "Stream one line.")).await?;
println!("responses: {}", resp_text);
Ok(())
}
SSE helper utilities:
use openai_sdk_rs::sse::{extract_data_lines_from_bytes, extract_json_values_from_bytes, extract_data_lines_from_str, try_extract_json_values_from_str};
let raw = b"data: {\"a\":1}\n\n:data comment\n\ndata: [DONE]\n";
let lines = extract_data_lines_from_bytes(raw);
assert_eq!(lines, vec!["{\"a\":1}".to_string()]);
let jsons = extract_json_values_from_bytes(raw);
assert_eq!(jsons[0]["a"], 1);
let text = "data: {\"a\":1}\n\n";
let lines_str = extract_data_lines_from_str(text);
assert_eq!(lines_str, vec!["{\"a\":1}"]);
let jsons_str = try_extract_json_values_from_str(text).unwrap();
assert_eq!(jsons_str[0]["a"], 1);
Goals
- Keep a clean, small surface area
- Prioritize reliability and clear errors
- Include light retries with exponential backoff for 429/5xx/timeouts
- Be easy to extend for more endpoints later
Images:
use openai_sdk_rs::{OpenAI, types::images::{ImageGenerationRequest, ImageResponseFormat}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let req = ImageGenerationRequest {
model: "dall-e-3".into(),
prompt: "A tiny Rust crab".into(),
n: Some(1),
size: Some("1024x1024".into()),
response_format: Some(ImageResponseFormat::B64Json)
};
let resp = client.images_generate(req).await?;
println!("variants: {}", resp.data.len());
Ok(())
}
Tool calling (Responses)
Supply tools via ResponsesRequest.tools
with JSON Schema:
use openai_sdk_rs::OpenAI;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let files = client.files_list().await?;
println!("{} files", files.data.len());
if let Some(f) = files.data.first() {
let bytes = client.files_download(&f.id).await?;
println!("downloaded {} bytes from {}", bytes.len(), f.filename);
let del = client.files_delete(&f.id).await?;
println!("deleted {}: {}", del.id, del.deleted);
}
Ok(())
}
use openai_sdk_rs::types::responses::{ResponsesRequest, ToolSpec};
use serde_json::json;
let req = ResponsesRequest {
model: "gpt-4o-mini".into(),
input: Some(json!("What's the weather in SF?")),
tools: Some(vec![ToolSpec {
type_: "function".to_string(),
name: "get_weather".to_string(),
description: Some("Get weather by city".to_string()),
parameters: Some(json!({
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
})),
}]),
..Default::default()
};
Builder options
timeout(Duration)
set request timeout
max_retries(u32)
and retry_base_delay(Duration)
configure retries
proxy(url)
set an HTTP(S) proxy for all requests
Custom reqwest Client
If you need full control over HTTP behavior (proxies, pools, TLS, UA), inject your own reqwest::Client
:
use std::time::Duration;
use openai_sdk_rs::{OpenAI, types::chat::{ChatMessage, ChatCompletionRequest}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.user_agent("my-app/0.1")
.build()?;
let api_key = std::env::var("OPENAI_API_KEY")?;
let oai = OpenAI::with_http_client(http, api_key)?;
let resp = oai.chat_completion(ChatCompletionRequest {
model: "gpt-4o-mini".into(),
messages: vec![ChatMessage::user("Hello from custom client!")],
..Default::default()
}).await?;
println!("{}", resp.first_choice_text().unwrap_or("<no text>"));
Ok(())
}
Or via the builder:
let http = reqwest::Client::builder().build()?;
let oai = OpenAI::builder()
.http_client(http)
.api_key(std::env::var("OPENAI_API_KEY")?)
.build()?;
Note: when injecting a client, builder options like timeout
, proxy
, and user_agent
are not applied; configure them on your reqwest::Client
.
License
MIT or Apache-2.0, at your option.