Find a file
Bryan Helmkamp 300a12a4eb Refine workflow runs list: grid layout, search, CI dots, spacing
Use CSS grid with subgrid for guaranteed column alignment. Remove repo
name, add PR icon, move duration next to status, show CI as dot next
to PR number, add typeahead search. Reverse sort (Merge first).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 14:10:50 -05:00
.cargo Disable empty doc-tests and add terse test output alias 2026-02-23 10:57:26 -05:00
apps/arc-web Refine workflow runs list: grid layout, search, CI dots, spacing 2026-02-28 14:10:50 -05:00
assets/brand move brand 2026-02-28 10:54:20 -05:00
crates Surface git metadata in pipeline events, manifest, and final.json 2026-02-28 11:09:36 -05:00
docker Fix Dockerfile.agent to use apt ripgrep for multi-arch support 2026-02-23 13:31:19 -05:00
docs Rename tool handler to script handler and add language attribute 2026-02-27 09:50:39 -05:00
pipelines move pipelines 2026-02-28 10:53:56 -05:00
test Add legacy_tool.dot workflow and CLI tests for backwards compat 2026-02-27 12:08:04 -05:00
.env.example Add Kimi, Z.AI, and Minimax API keys to .env.example 2026-02-28 04:07:03 -05:00
.gitignore gitignore 2026-02-25 14:26:17 -05:00
Cargo.lock Rename all crates from unprefixed to arc-* prefix 2026-02-28 04:20:47 -05:00
Cargo.toml Add MCP client support for connecting to external tool servers 2026-02-27 23:28:33 -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