Simple to use AI library for Rust with LLM streaming, embeddings, tool calling,
OAuth helpers, and a lightweight agent loop, inspired by
pi.
cargo add ai
cargo add tokio --features macros,rt-multi-thread
cargo add futuresSee crates/ai/README.md for the full API reference.
Most applications should start with stream_simple for streaming responses and
complete_simple for one-shot responses. They take SimpleStreamOptions and
map common settings like reasoning, cache retention, API keys, retries,
cancellation, and provider options onto the selected provider. Use stream or
complete when you need the lower-level StreamOptions shape or direct
provider-option forwarding.
Provider handles are available for OpenAI, Anthropic, GitHub Copilot, and
OpenRouter image generation. Use providers::openai::builder() for
OpenAI-compatible endpoints such as llama.cpp, MLX, Ollama, vLLM, and Azure
Foundry.
See examples/simple-coding-agent for a tiny interactive coding-agent example with one bash tool.
use ai::{complete_simple, providers::openai, Context, Message, Result};
#[tokio::main]
async fn main() -> Result<()> {
let openai = openai::from_env()?;
let model = openai.model("gpt-5.5").build()?;
let context = Context::builder()
.message(Message::user_text("Write a haiku about Rust."))
.build();
let message = complete_simple(model, context, None).await?;
println!("{message:?}");
Ok(())
}use futures::StreamExt;
use ai::{providers::openai, stream_simple, AssistantMessageEvent, Context, Message, Result};
#[tokio::main]
async fn main() -> Result<()> {
let openai = openai::from_env()?;
let model = openai.model("gpt-5.5").build()?;
let context = Context::builder()
.message(Message::user_text("Write a haiku about Rust."))
.build();
let mut events = stream_simple(model, context, None)?;
while let Some(event) = events.next().await {
if let AssistantMessageEvent::TextDelta { delta, .. } = event? {
print!("{delta}");
}
}
Ok(())
}Use embed for one string and embed_many for multiple strings.
use ai::{embed, embed_many, providers::openai, Result};
#[tokio::main]
async fn main() -> Result<()> {
let openai = openai::from_env()?;
let model = openai
.embedding_model("text-embedding-3-small")
.build_embedding()?;
let one = embed(model.clone(), "hello", None).await?;
let batch = embed_many(model, ["first", "second"], None).await?;
println!("single: {:?}, batch: {}", one.embedding, batch.embeddings.len());
Ok(())
}use ai::providers::openai;
let openai_responses_from_env = openai::from_env()?;
let openai_responses_with_key = openai::builder()
.api_key(Some("sk-..."))
.responses()
.build()?;use ai::providers::openai;
let openai_chat_with_key = openai::builder()
.api_key(Some("sk-..."))
.chat_completions()
.build()?;
let ollama_chat = openai::builder()
.base_url("http://localhost:11434/v1")
.chat_completions()
.build()?;use ai::providers::anthropic;
let anthropic_from_env = anthropic::from_env()?;
let anthropic_with_key = anthropic::builder()
.api_key("sk-ant-...")
.build()?;use ai::{generate_images, providers::openai, ImagesContext};
let openai = openai::from_env()?;
let model = openai
.image_model("gpt-image-2")
.build_image()?;
let context = ImagesContext::builder()
.text("Generate a small watercolor robot reading a book.")
.build();
let images = generate_images(model, context, None).await?;For llama.cpp, MLX, Ollama, or another OpenAI-compatible image endpoint, use the OpenAI provider with the compatible server's base URL. For example, with Ollama:
use ai::{generate_images, providers::openai, ImagesContext};
let ollama = openai::builder()
.provider_id("ollama")
.base_url("http://localhost:11434/v1")
.images()
.build()?;
let model = ollama.model("x/z-image-turbo").build_image()?;
let context = ImagesContext::builder().text("Generate a robot.").build();
let images = generate_images(model, context, None).await?;OpenRouter image models are also available through providers::openrouter.
Use Agent when you want conversation state, awaited event subscribers,
abort, and steering/follow-up queues.
use ai::{providers::anthropic, Agent, AgentEvent, AgentOptions, Result};
#[tokio::main]
async fn main() -> Result<()> {
let anthropic = anthropic::from_env()?;
let model = anthropic.model("claude-sonnet-4-5").build()?;
let agent = Agent::new(AgentOptions::new(model));
agent
.set_system_prompt("You are a concise coding assistant.")
.await;
let subscription = agent.subscribe(async |event, cancellation_token| {
if cancellation_token.is_cancelled() {
return Ok(());
}
if let AgentEvent::MessageUpdate {
assistant_message_event: ai::AssistantMessageEvent::TextDelta { delta, .. },
..
} = event
{
print!("{delta}");
}
Ok(())
});
agent
.prompt_text("Explain ownership in one paragraph.", Vec::new())
.await?;
subscription.unsubscribe();
Ok(())
}Keep the subscription handle alive while the listener remains registered. Dropping the handle also unsubscribes.
use futures::StreamExt;
use ai::{
agent_loop, providers::anthropic, AgentContext, AgentEvent, AgentLoopConfig,
AssistantMessageEvent, Message, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let anthropic = anthropic::from_env()?;
let model = anthropic.model("claude-sonnet-4-5").build()?;
let context = AgentContext::builder()
.system_prompt("You are a concise coding assistant.")
.build();
let mut events = agent_loop(
vec![Message::user_text("Explain ownership in one paragraph.")],
context,
AgentLoopConfig::new(model),
None,
None,
);
while let Some(event) = events.next().await {
if let AgentEvent::MessageUpdate {
assistant_message_event: AssistantMessageEvent::TextDelta { delta, .. },
..
} = event
{
print!("{delta}");
}
}
Ok(())
}mise run fmt
mise run check
mise run clippy
mise run test-ai
mise run test
mise run ci
mise run allMIT