Find a file
Bryan Helmkamp e7d64136b3 Rewrite changelog entries with improved style and structure
Each major feature gets its own H2 heading with narrative depth
and code examples instead of dense bullet lists under category
headers. Minor improvements and fixes go at the bottom as a flat
list. Style inspired by Qlty, Linear, Vercel, and Resend changelogs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 03:13:42 -05:00
.cargo Disable empty doc-tests and add terse test output alias 2026-02-23 10:57:26 -05:00
.github Add GitHub Actions CI workflows for Rust and TypeScript 2026-03-04 00:35:04 -05:00
apps/arc-web Rename run-files-changed.tsx to run-compare.tsx to match route 2026-03-05 00:57:52 -05:00
assets/brand move brand 2026-02-28 10:54:20 -05:00
crates Simplify hook implementation: deduplicate LLM setup, fix async I/O, cache HTTP clients 2026-03-05 02:49:06 -05:00
demo Add multi-provider ensemble demo and generate PNG/SVG for all demo workflows 2026-03-04 09:47:54 -05:00
docker Remove unused per-service Dockerfiles 2026-03-05 00:34:03 -05:00
docs Rewrite changelog entries with improved style and structure 2026-03-05 03:13:42 -05:00
openapi Rename GET /runs/{id}/files to GET /runs/{id}/compare 2026-03-05 00:53:45 -05:00
packages/arc-api-client Rename GET /runs/{id}/files to GET /runs/{id}/compare 2026-03-05 00:53:45 -05:00
pipelines spec-dod-multimodel.png 2026-03-04 17:38:48 -05:00
scripts Rename arc-attractor crate to arc-workflows 2026-03-01 03:05:43 -05:00
test Rename kilroy → attractor in test directory and compat tests 2026-03-01 18:10:22 -05:00
.env.example Move GitHub App ID and Client ID from env vars to TOML config 2026-03-03 01:05:48 -05:00
.gitignore Add generated Axios API client package (@qltysh/arc-api-client) 2026-03-01 22:03:38 -05:00
bun.lock Move auth and API config from env vars to TOML (~/.arc/arc.toml) 2026-03-02 22:49:04 -05:00
Cargo.lock Add HTTP hook executor with env var interpolation 2026-03-05 01:59:38 -05:00
Cargo.toml Render Markdown output in terminal using termimad 2026-03-04 10:25:36 -05:00
CLAUDE.md Extract AuthLayout component and polish auth UI 2026-03-02 20:19:15 -05:00
docker-compose.demo.yaml Add Docker Compose demo setup for local development 2026-03-05 00:32:48 -05:00
Dockerfile Add Docker Compose demo setup for local development 2026-03-05 00:32:48 -05:00
entrypoint.ts Add Docker Compose demo setup for local development 2026-03-05 00:32:48 -05:00
package.json SQLite-backed sessions with GitHub email and app manifest fix 2026-03-02 21:05:59 -05:00
README.md Enable all clippy lints and fix all warnings 2026-02-19 15:41:43 -04:00

unified-llm

A unified Rust client library for multiple LLM providers (OpenAI, Anthropic, Google Gemini). Write provider-agnostic code and switch models by changing a single string identifier.

Architecture

The library is organized into four layers:

Layer 4: High-Level API         generate(), stream(), generate_object()
Layer 3: Core Client            Client, provider routing, middleware hooks
Layer 2: Provider Utilities     Shared helpers (SSE parsing, retry, etc.)
Layer 1: Provider Specification ProviderAdapter trait, shared types

Installation

Add to your Cargo.toml:

[dependencies]
unified-llm = { path = "crates/unified-llm" }
tokio = { version = "1", features = ["full"] }

Usage Examples

Simple Generation

use unified_llm::generate::{generate, GenerateParams};

#[tokio::main]
async fn main() {
    let result = generate(
        GenerateParams::new("claude-opus-4-6")
            .prompt("Explain quantum computing in one paragraph")
    ).await.unwrap();

    println!("{}", result.text);
    println!("Tokens used: {}", result.usage.total_tokens);
}

Generation with System Message

use unified_llm::generate::{generate, GenerateParams};

let result = generate(
    GenerateParams::new("claude-opus-4-6")
        .system("You are a helpful coding assistant.")
        .prompt("Write a Rust function to check if a number is prime")
).await.unwrap();

Generation with Tools

use unified_llm::generate::{generate, GenerateParams};
use unified_llm::tools::Tool;

let weather_tool = Tool::active(
    "get_weather",
    "Get the current weather for a location",
    serde_json::json!({
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "City name, e.g. 'San Francisco, CA'"
            }
        },
        "required": ["location"]
    }),
    |args| async move {
        let location = args["location"].as_str().unwrap_or("unknown");
        Ok(serde_json::json!(format!("72F and sunny in {}", location)))
    },
);

let result = generate(
    GenerateParams::new("claude-opus-4-6")
        .system("You are a helpful assistant with access to weather data.")
        .prompt("What is the weather in San Francisco?")
        .tools(vec![weather_tool])
        .max_tool_rounds(5)
).await.unwrap();

println!("{}", result.text);
println!("Steps taken: {}", result.steps.len());
println!("Total tokens: {}", result.total_usage.total_tokens);

Streaming

use unified_llm::generate::{stream_generate, GenerateParams};
use unified_llm::types::StreamEventType;
use futures::StreamExt;

let mut stream = stream_generate(
    GenerateParams::new("claude-opus-4-6")
        .prompt("Write a haiku about coding")
).await.unwrap();

while let Some(event) = stream.next().await {
    let event = event.unwrap();
    if event.r#type == StreamEventType::TextDelta {
        print!("{}", event.delta.unwrap_or_default());
    }
}

Structured Output

use unified_llm::generate::{generate_object, GenerateParams};

let schema = serde_json::json!({
    "type": "object",
    "properties": {
        "name": { "type": "string" },
        "age": { "type": "integer" }
    },
    "required": ["name", "age"]
});

let result = generate_object(
    GenerateParams::new("gpt-5.2")
        .prompt("Extract: 'Alice is 30 years old'"),
    schema,
).await.unwrap();

let output = result.output.unwrap();
assert_eq!(output["name"], "Alice");
assert_eq!(output["age"], 30);

Client Configuration

use unified_llm::client::Client;
use std::sync::Arc;

// From environment variables (reads OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.)
let client = Client::from_env();

// Or configure explicitly
let mut client = Client::new(
    std::collections::HashMap::new(),
    None,
    vec![],
);
// Register adapters...

// Use with generate
let result = generate(
    GenerateParams::new("claude-opus-4-6")
        .prompt("Hello")
        .client(Arc::new(client))
).await.unwrap();

Model Catalog

use unified_llm::catalog::{get_model_info, list_models, get_latest_model};

// Look up a model
let info = get_model_info("claude-opus-4-6").unwrap();
println!("{} ({})", info.display_name, info.provider);
println!("Context window: {} tokens", info.context_window);

// Look up by alias
let info = get_model_info("opus").unwrap();
assert_eq!(info.id, "claude-opus-4-6");

// List all models for a provider
let anthropic_models = list_models(Some("anthropic"));
for model in &anthropic_models {
    println!("  {} - {}", model.id, model.display_name);
}

// Get the latest model for a provider
let best = get_latest_model("openai", Some("reasoning")).unwrap();
println!("Best OpenAI reasoning model: {}", best.id);

Retry Logic

use unified_llm::retry::retry;
use unified_llm::types::RetryPolicy;

let policy = RetryPolicy {
    max_retries: 3,
    base_delay: 1.0,
    max_delay: 60.0,
    backoff_multiplier: 2.0,
    jitter: true,
};

let response = retry(&policy, || {
    let c = client.clone();
    let r = request.clone();
    async move { c.complete(&r).await }
}).await.unwrap();

Error Handling

use unified_llm::error::SdkError;

match result {
    Ok(response) => println!("{}", response.text()),
    Err(SdkError::RateLimit { retry_after, .. }) => {
        println!("Rate limited. Retry after {:?}s", retry_after);
    }
    Err(SdkError::Authentication { message, .. }) => {
        println!("Auth error: {}", message);
    }
    Err(e) if e.retryable() => {
        println!("Transient error, can retry: {}", e);
    }
    Err(e) => {
        println!("Fatal error: {}", e);
    }
}

Modules

Module Description
types Core data types: Message, Request, Response, Usage, StreamEvent, etc.
error Error hierarchy with retryability classification
client Client with provider routing and middleware
provider ProviderAdapter trait
middleware Middleware trait for cross-cutting concerns
tools Tool definitions and parallel execution
retry Retry with exponential backoff and jitter
generate High-level API: generate(), stream(), generate_object()
catalog Model catalog with lookup functions

Supported Providers

Provider API Environment Variable
OpenAI Responses API (/v1/responses) OPENAI_API_KEY
Anthropic Messages API (/v1/messages) ANTHROPIC_API_KEY
Gemini Gemini API (/v1beta/...) GEMINI_API_KEY

License

MIT