Find a file
Bryan Helmkamp 05903892f6 Add execution environment and setup command observability events
Introduces ExecutionEnvEvent enum (15 variants covering lifecycle, Docker
image pull, Daytona snapshot, and git clone operations) with callback
injection on all execution environment implementations. Adds
PipelineEvent::ExecutionEnv wrapper and Setup* variants for setup command
tracking. Wires callbacks in run.rs so exec env and setup events flow
through the pipeline event emitter to stderr and progress.ndjson.

Also changes PipelineEngine::new to accept Arc<EventEmitter> so the
emitter can be shared with exec env callbacks before engine construction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:09:41 -05:00
.cargo Disable empty doc-tests and add terse test output alias 2026-02-23 10:57:26 -05:00
attractor-web Add missing data to agent events and cost tracking in pipeline logs 2026-02-25 17:17:13 -05:00
crates Add execution environment and setup command observability events 2026-02-26 22:09:41 -05:00
docker Fix Dockerfile.agent to use apt ripgrep for multi-arch support 2026-02-23 13:31:19 -05:00
docs Add missing data to agent events and cost tracking in pipeline logs 2026-02-25 17:17:13 -05:00
pipelines daytona-check pipeline 2026-02-26 21:52:33 -05:00
test Implement GET /pipelines/{id}/graph endpoint for SVG rendering 2026-02-23 09:13:36 -05:00
.env.example Add Daytona execution environment and replace --docker with --execution-env 2026-02-26 18:35:33 -05:00
.gitignore gitignore 2026-02-25 14:26:17 -05:00
Cargo.lock Add Daytona execution environment and replace --docker with --execution-env 2026-02-26 18:35:33 -05:00
Cargo.toml Add Daytona execution environment and replace --docker with --execution-env 2026-02-26 18:35:33 -05:00
README.md Enable all clippy lints and fix all warnings 2026-02-19 15:41:43 -04:00
spec-dod-multimodel.dot Update spec-dod-multimodel.dot for Rust: fix paths, build commands, and language references 2026-02-23 17:50:10 -05:00
spec-dod.dot Update spec-dod fix prompt to run cargo test before and after changes 2026-02-23 16:34:41 -05: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