Aquaregia
Note: Aquaregia is in rapid iteration before v0.2.0, and the API may have breaking changes. use with caution.
Aquaregia is a provider-agnostic Rust toolkit for building AI applications and tool-using agents.
It provides a unified API across OpenAI, Anthropic, Google, and OpenAI-compatible services, with first-class support for reasoning-aware output, streaming events, multi-step tool execution, and vision/image inputs.
Read the API docs, browse examples, or switch to 中文文档.
Installation
You need Rust and a Tokio async runtime in your project.
Default features enable openai and anthropic. Optional features:
| Feature | Description |
|---|---|
openai |
OpenAI adapter (default) |
anthropic |
Anthropic adapter (default) |
telemetry |
tracing spans for generate, stream, agent steps and tool calls |
Unified Provider Architecture
One LlmClient binds to one provider configuration.
Each call passes a GenerateTextRequest that carries both the model and the messages.
| Provider | Register API | Model argument |
|---|---|---|
| OpenAI | LlmClient::openai(api_key) (+ optional .base_url(...)) |
"gpt-4o" |
| Anthropic | LlmClient::anthropic(api_key) (+ optional .base_url(...), .api_version(...)) |
"claude-sonnet-4-5" |
LlmClient::google(api_key) (+ optional .base_url(...)) |
"gemini-2.0-flash" |
|
| OpenAI-compatible | LlmClient::openai_compatible(base_url).api_key(...) |
"deepseek-chat" |
Usage
Generating Text
use ;
async
Streaming Text
use ;
use StreamExt;
async
StreamEvent covers all variants:
ReasoningStarted, ReasoningDelta, ReasoningDone, TextDelta, ToolCallReady, Usage, and Done.
Reasoning
Reasoning is exposed in both non-streaming and streaming APIs.
let out = client
.generate
.await?;
println!;
println!;
println!;
for part in &out.reasoning_parts
Unified output fields:
GenerateTextResponse.reasoning_text: flattened reasoning text (convenience field).GenerateTextResponse.reasoning_parts: structured reasoning blocks with optional provider metadata.Usage.input_tokens: total input tokens reported by provider.Usage.input_no_cache_tokens: non-cached input tokens (best effort).Usage.input_cache_read_tokens/Usage.input_cache_write_tokens: cache read/write split when available.Usage.output_tokens: total output tokens.Usage.output_text_tokens: output text token split when available.Usage.reasoning_tokens: provider-reported reasoning tokens when available.Usage.raw_usage: raw provider usage payload for debugging/future extension.Message.parts: assistant messages can includeContentPart::Reasoning(...)for transcript replay.
Provider mapping:
| Provider | Reasoning Content | Usage Mapping |
|---|---|---|
| OpenAI / OpenAI-compatible | reasoning_content (or reasoning) in sync + stream |
parses prompt_tokens_details.cached_tokens + completion_tokens_details.reasoning_tokens |
| Anthropic | thinking / redacted_thinking, stream thinking_delta + signature_delta |
parses cache_read_input_tokens / cache_creation_input_tokens; reasoning token split unavailable |
parts with thought: true, optional thoughtSignature metadata |
parses cachedContentTokenCount + thoughtsTokenCount |
Multimodal Vision
Send images to vision-capable models alongside text using ImagePart / MediaData.
All three formats (URL, base64, raw bytes) are supported across Anthropic, OpenAI, Google, and OpenAI-compatible providers.
use ;
async
Three convenience constructors cover common cases:
| Constructor | Description |
|---|---|
Message::user_image_url(url) |
Single image from a URL |
Message::user_image_bytes(bytes, mime) |
Single image from raw bytes |
Message::user_text_and_image_url(text, url) |
Text + image URL in one message |
For full control, build the parts directly:
use ;
// Base64-encoded image
let msg = new?;
// Raw bytes (e.g. read from a file)
let bytes = read?;
let msg = user_image_bytes;
Provider image format mapping:
| Provider | URL | Base64 / Bytes |
|---|---|---|
| Anthropic | source.type: url |
source.type: base64 |
| OpenAI / Compatible | image_url with remote URL |
image_url with data:<mime>;base64,… |
fileData.fileUri |
inlineData.data |
Error Handling
match client .generate(GenerateTextRequest::from_user_prompt("deepseek-chat", "hello")) .await { Ok(out) => println!("{}", out.output_text), Err(err) => match err.code { ErrorCode::RateLimited => eprintln!("rate limited; retry later"), ErrorCode::AuthFailed => eprintln!("check API key"), ErrorCode::Cancelled => eprintln!("request was cancelled"), _ => eprintln!("request failed: {}", err), }, }
### Agent + Tool Loop
```rust
use aquaregia::{Agent, LlmClient, tool};
use serde_json::{Value, json};
#[tool(description = "Get weather by city")]
async fn get_weather(city: String) -> Result<Value, String> {
Ok(json!({ "city": city, "temp_c": 23, "condition": "sunny" }))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = LlmClient::openai_compatible("https://api.deepseek.com")
.api_key(std::env::var("DEEPSEEK_API_KEY"))
.build()?;
let agent = Agent::builder(client, "deepseek-chat")
.instructions("You can call tools before answering.")
.tools([get_weather])
.max_steps(4)
.build()?;
let out = agent.run("What is the weather in Shanghai?").await?;
println!("{}", out.output_text);
Ok(())
}
Dynamic Planning (prepare_call / prepare_step)
use ;
let agent = builder
.max_steps
.prepare_call
.prepare_step
.build?;
Cancellation
Every request and agent run can be cancelled via a CancellationToken.
use ;
use ;
// Cancel a single generate call
let token = new;
let token_clone = token.clone;
spawn;
let req = builder
.user_prompt
.cancellation_token
.build?;
match client.generate.await
Agents expose dedicated helpers:
let token = new;
token.cancel; // or cancel later from another task
// Returns Err with ErrorCode::Cancelled
agent.run_cancellable.await?;
// Pass your own message list
agent.run_messages_cancellable.await?;
Cancellation is checked:
- Before every HTTP send (via
tokio::select!— zero overhead when not cancelled) - After every SSE chunk in streaming responses
- At the top of every agent step in the tool loop
Telemetry
Enable the telemetry feature to get tracing spans automatically:
= { = "*", = ["telemetry"] }
Spans emitted:
| Span | Fields |
|---|---|
aquaregia::generate |
model, provider |
aquaregia::stream |
model |
agent_step |
step |
tool_call |
tool.name |
Wire your own subscriber (e.g. tracing-subscriber, tracing-opentelemetry) — Aquaregia does not configure one for you.
init; // or any other subscriber
let out = client.generate.await?; // emits a span
OpenAI-Compatible Advanced Settings
use LlmClient;
let client = openai_compatible
.api_key
.header
.query_param
.chat_completions_path
.think_tag_parsing
.think_tag_case_insensitive
.build?;
Examples
| Example | Command | Focus |
|---|---|---|
| Basic generation | cargo run --example basic_generate |
one-shot generate |
| Basic stream | cargo run --example basic_stream |
stream + StreamEvent handling |
| Minimal agent | cargo run --example agent_minimal |
Agent::builder + one tool |
| Tool loop guardrails | cargo run --example tools_max_steps |
multi-step tools + max_steps |
| Dynamic hooks | cargo run --example prepare_hooks |
prepare_call / prepare_step |
| Compatible custom path/query/header | cargo run --example openai_compatible_custom |
custom headers / query params / path |
| Mini terminal code agent | cargo run --example mini_claude_code |
Agent::builder + #[tool] + local tools |
| Multimodal image | cargo run --example multimodal_image |
Message::user_text_and_image_url + vision |
Development
Contributing
Contributions are welcome. For behavior changes, include integration tests (happy path + error mapping + tool/stream flows where relevant).