mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
Add README.md for each crate with usage examples
Create comprehensive READMEs for unified-llm, unified-llm-cli, and coding-agent-loop crates, and expand the attractor README from a one-liner into full documentation covering key concepts, API usage, and code examples. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
34836200e5
commit
2bdec7e8e6
4 changed files with 742 additions and 1 deletions
|
|
@ -1,3 +1,172 @@
|
|||
# attractor
|
||||
|
||||
A DOT-based pipeline runner for multi-stage AI workflows.
|
||||
A DOT-based pipeline runner for multi-stage AI workflows. Define workflows as Graphviz `digraph` files and execute them with pluggable handlers, conditional routing, human-in-the-loop gates, parallel branching, retry policies, and checkpoint-based recovery.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Graph** -- A directed graph parsed from DOT syntax containing nodes, edges, and attributes. The graph carries a `goal` describing the pipeline's purpose.
|
||||
- **Node** -- A workflow step. Graphviz shapes map to handler types (e.g., `Mdiamond` = start, `Msquare` = exit, `box` = codergen/LLM, `diamond` = conditional, `hexagon` = human gate, `component` = parallel).
|
||||
- **Edge** -- A connection between nodes with optional `condition`, `label`, `weight`, and `fidelity` attributes that control routing.
|
||||
- **Handler** -- An async trait implementation that executes a node and returns an `Outcome`. Built-in handlers include `StartHandler`, `ExitHandler`, `CodergenHandler`, `ConditionalHandler`, `WaitHumanHandler`, `ParallelHandler`, `FanInHandler`, `ToolHandler`, and `ManagerLoopHandler`.
|
||||
- **Outcome** -- The result of executing a handler, carrying a `StageStatus` (Success, Fail, PartialSuccess, Retry, Skipped), optional routing hints (`preferred_label`, `suggested_next_ids`), and context updates.
|
||||
- **Context** -- A thread-safe key-value store shared across pipeline stages, supporting snapshots and isolated cloning for parallel branches.
|
||||
- **Interviewer** -- A trait for human-in-the-loop interactions. Implementations include `AutoApproveInterviewer`, `QueueInterviewer`, `CallbackInterviewer`, `ConsoleInterviewer`, and `RecordingInterviewer`.
|
||||
- **Checkpoint** -- A serializable snapshot of execution state (completed nodes, context values, logs) for crash recovery and resume.
|
||||
|
||||
## Pipeline Definition
|
||||
|
||||
Pipelines are defined using Graphviz DOT syntax:
|
||||
|
||||
```dot
|
||||
digraph MyPipeline {
|
||||
graph [goal="Implement and validate a feature"]
|
||||
rankdir=LR
|
||||
node [shape=box, timeout="900s"]
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
exit [shape=Msquare, label="Exit"]
|
||||
plan [label="Plan", prompt="Plan the implementation"]
|
||||
implement [label="Implement", prompt="Implement the plan"]
|
||||
validate [label="Validate", prompt="Run tests"]
|
||||
gate [shape=diamond, label="Tests passing?"]
|
||||
|
||||
start -> plan -> implement -> validate -> gate
|
||||
gate -> exit [label="Yes", condition="outcome=success"]
|
||||
gate -> implement [label="No", condition="outcome!=success"]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Parsing and Validating a Pipeline
|
||||
|
||||
```rust
|
||||
use attractor::pipeline::prepare_pipeline;
|
||||
|
||||
let dot_source = r#"digraph Simple {
|
||||
graph [goal="Run tests"]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
work [shape=box, prompt="Run the test suite"]
|
||||
start -> work -> exit
|
||||
}"#;
|
||||
|
||||
let graph = prepare_pipeline(dot_source)
|
||||
.expect("pipeline should parse and validate");
|
||||
assert_eq!(graph.name, "Simple");
|
||||
assert_eq!(graph.goal(), "Run tests");
|
||||
```
|
||||
|
||||
`prepare_pipeline` parses the DOT source, applies built-in transforms (variable expansion, stylesheet application, preamble injection), and validates the graph against 14 built-in lint rules.
|
||||
|
||||
### Running a Pipeline
|
||||
|
||||
```rust
|
||||
use attractor::engine::{PipelineEngine, RunConfig};
|
||||
use attractor::event::EventEmitter;
|
||||
use attractor::handler::HandlerRegistry;
|
||||
use attractor::handler::start::StartHandler;
|
||||
use attractor::handler::exit::ExitHandler;
|
||||
use attractor::handler::codergen::CodergenHandler;
|
||||
use attractor::pipeline::prepare_pipeline;
|
||||
|
||||
let graph = prepare_pipeline(dot_source).unwrap();
|
||||
|
||||
let mut registry = HandlerRegistry::new(Box::new(CodergenHandler::new(None)));
|
||||
registry.register("start", Box::new(StartHandler));
|
||||
registry.register("exit", Box::new(ExitHandler));
|
||||
registry.register("codergen", Box::new(CodergenHandler::new(None)));
|
||||
|
||||
let engine = PipelineEngine::new(registry, EventEmitter::new());
|
||||
let config = RunConfig {
|
||||
logs_root: "/tmp/pipeline-run".into(),
|
||||
};
|
||||
|
||||
// engine.run(&graph, &config).await
|
||||
```
|
||||
|
||||
### Custom Handlers
|
||||
|
||||
Implement the `Handler` trait to add custom node behavior:
|
||||
|
||||
```rust
|
||||
use attractor::handler::Handler;
|
||||
use attractor::context::Context;
|
||||
use attractor::graph::{Graph, Node};
|
||||
use attractor::outcome::Outcome;
|
||||
use attractor::error::AttractorError;
|
||||
use async_trait::async_trait;
|
||||
use std::path::Path;
|
||||
|
||||
struct MyHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl Handler for MyHandler {
|
||||
async fn execute(
|
||||
&self,
|
||||
node: &Node,
|
||||
context: &Context,
|
||||
graph: &Graph,
|
||||
logs_root: &Path,
|
||||
) -> Result<Outcome, AttractorError> {
|
||||
// Custom logic here
|
||||
Ok(Outcome::success())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Stylesheets
|
||||
|
||||
CSS-like stylesheets control LLM model assignment with specificity-based cascading:
|
||||
|
||||
```dot
|
||||
digraph Styled {
|
||||
graph [
|
||||
goal="Build feature",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; }
|
||||
"
|
||||
]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Selectors by specificity: `*` (universal, 0) < `shape` (1) < `.class` (2) < `#id` (3). Explicit node attributes are never overridden.
|
||||
|
||||
### Condition Expressions
|
||||
|
||||
Edge conditions use a simple expression syntax for routing:
|
||||
|
||||
```
|
||||
outcome=success
|
||||
outcome!=fail
|
||||
outcome=success && context.tests_passed=true
|
||||
my_flag
|
||||
```
|
||||
|
||||
Clauses support `=`, `!=`, and bare key truthiness checks, joined with `&&`.
|
||||
|
||||
### Human-in-the-Loop Gates
|
||||
|
||||
Nodes with `shape=hexagon` or `type="wait.human"` pause execution for human input. Outgoing edge labels become selectable options, with accelerator key parsing for patterns like `[A] Approve` and `F) Fix`.
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
Nodes with `shape=component` fan out to branches concurrently. Configurable join policies: `wait_all` (default), `first_success`, `k_of_n(N)`, `quorum(0.5)`. Error policies: `continue`, `fail_fast`, `ignore`.
|
||||
|
||||
### Checkpoints and Resume
|
||||
|
||||
The engine saves a checkpoint after each node. Resume from a checkpoint with `engine.run_from_checkpoint(&graph, &config, &checkpoint)`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
parser (DOT -> AST -> Graph)
|
||||
-> transform (variable expansion, stylesheet, preamble)
|
||||
-> validation (14 lint rules)
|
||||
-> engine (execution loop with retry, edge selection, goal gates)
|
||||
-> handler (pluggable node executors)
|
||||
-> interviewer (human-in-the-loop I/O)
|
||||
```
|
||||
|
|
|
|||
237
crates/coding-agent-loop/README.md
Normal file
237
crates/coding-agent-loop/README.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# coding-agent-loop
|
||||
|
||||
A programmable agentic loop for building coding agents. This crate provides the core session management, tool execution, and LLM interaction loop used to power interactive coding assistants.
|
||||
|
||||
## Architecture
|
||||
|
||||
The crate is organized around a central `Session` that drives an agentic loop:
|
||||
|
||||
1. **User input** is appended to a conversation `History`
|
||||
2. The session builds a `Request` with system prompt, history, and tools
|
||||
3. An LLM generates a response (text and/or tool calls) via `unified-llm`
|
||||
4. Tool calls are executed through a `ToolRegistry` against an `ExecutionEnvironment`
|
||||
5. Results are recorded and the loop continues until the LLM responds with text only (natural completion), a turn limit is reached, or the session is aborted
|
||||
|
||||
```
|
||||
User Input
|
||||
|
|
||||
v
|
||||
[Session::process_input]
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Build Request | <-- system prompt + history + tools
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| LLM Call | <-- via unified-llm Client
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+ +-------------------+
|
||||
| Tool Calls? -----+-yes-| Execute Tools |
|
||||
+-------------------+ | (parallel or seq) |
|
||||
| no +-------------------+
|
||||
v |
|
||||
[Done] +---> loop back to Build Request
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, abort handling, and event emission.
|
||||
- **`ProviderProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`.
|
||||
- **`ExecutionEnvironment`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalExecutionEnvironment` provides a real implementation; the trait enables sandboxing and testing.
|
||||
- **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile.
|
||||
- **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages.
|
||||
- **`EventEmitter`** -- Broadcasts `SessionEvent`s (tool calls, text, errors, warnings) over a `tokio::sync::broadcast` channel for UI or logging.
|
||||
- **`SubAgentManager`** -- Spawns child `Session`s on background tasks for delegated work, with depth limits.
|
||||
- **`SessionConfig`** -- Tunable parameters: max turns, tool round limits, command timeouts, loop detection, output truncation limits, and user instructions.
|
||||
|
||||
## Key Types and Traits
|
||||
|
||||
### `Session`
|
||||
|
||||
The main entry point. Created with an LLM client, a provider profile, an execution environment, and a config.
|
||||
|
||||
### `ProviderProfile`
|
||||
|
||||
```rust
|
||||
pub trait ProviderProfile: Send + Sync {
|
||||
fn id(&self) -> String;
|
||||
fn model(&self) -> String;
|
||||
fn tool_registry(&self) -> &ToolRegistry;
|
||||
fn build_system_prompt(
|
||||
&self,
|
||||
env: &dyn ExecutionEnvironment,
|
||||
env_context: &EnvContext,
|
||||
project_docs: &[String],
|
||||
user_instructions: Option<&str>,
|
||||
) -> String;
|
||||
fn capabilities(&self) -> ProfileCapabilities;
|
||||
fn knowledge_cutoff(&self) -> &str;
|
||||
// ... default methods for tools(), provider_options(), supports_*()
|
||||
}
|
||||
```
|
||||
|
||||
Built-in profiles:
|
||||
- **`AnthropicProfile`** -- 200K context, extended thinking beta headers, tools: `read_file`, `write_file`, `edit_file`, `shell`, `grep`, `glob`
|
||||
- **`OpenAiProfile`** -- 128K context, reasoning effort support, tools: `read_file`, `write_file`, `shell`, `grep`, `glob`, `apply_patch` (v4a format)
|
||||
- **`GeminiProfile`** -- 1M context, safety settings, tools: all Anthropic tools plus `read_many_files`, `list_dir`, `web_search`, `web_fetch`
|
||||
|
||||
### `ExecutionEnvironment`
|
||||
|
||||
```rust
|
||||
pub trait ExecutionEnvironment: Send + Sync {
|
||||
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String>;
|
||||
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
|
||||
async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult, String>;
|
||||
async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<String>, String>;
|
||||
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String>;
|
||||
// ... plus delete_file, file_exists, list_directory, initialize, cleanup, platform info
|
||||
}
|
||||
```
|
||||
|
||||
`LocalExecutionEnvironment` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback.
|
||||
|
||||
### `SessionConfig`
|
||||
|
||||
```rust
|
||||
pub struct SessionConfig {
|
||||
pub max_turns: usize, // 0 = unlimited
|
||||
pub max_tool_rounds_per_input: usize, // default: 200
|
||||
pub default_command_timeout_ms: u64, // default: 10s
|
||||
pub max_command_timeout_ms: u64, // default: 600s
|
||||
pub enable_loop_detection: bool, // default: true
|
||||
pub loop_detection_window: usize, // default: 10
|
||||
pub max_subagent_depth: usize, // default: 1
|
||||
pub user_instructions: Option<String>,
|
||||
pub reasoning_effort: Option<String>,
|
||||
// ... plus tool_output_limits, tool_line_limits, git_root
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use coding_agent_loop::{
|
||||
AnthropicProfile, LocalExecutionEnvironment, Session, SessionConfig,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use unified_llm::client::Client;
|
||||
|
||||
// 1. Create an LLM client (via unified-llm)
|
||||
let client: Client = /* configure unified-llm client */;
|
||||
|
||||
// 2. Choose a provider profile
|
||||
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-20250514"));
|
||||
|
||||
// 3. Create an execution environment
|
||||
let env = Arc::new(LocalExecutionEnvironment::new(
|
||||
PathBuf::from("/path/to/project"),
|
||||
));
|
||||
|
||||
// 4. Configure the session
|
||||
let config = SessionConfig {
|
||||
max_tool_rounds_per_input: 50,
|
||||
enable_loop_detection: true,
|
||||
user_instructions: Some("Always write tests first".into()),
|
||||
..SessionConfig::default()
|
||||
};
|
||||
|
||||
// 5. Create and initialize the session
|
||||
let mut session = Session::new(client, profile, env, config);
|
||||
session.initialize().await;
|
||||
|
||||
// 6. Subscribe to events (for UI rendering)
|
||||
let mut rx = session.subscribe();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = rx.recv().await {
|
||||
// Handle SessionEvent: tool calls, text, errors, etc.
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Process user input
|
||||
session.process_input("Fix the failing test in src/lib.rs").await?;
|
||||
```
|
||||
|
||||
### Steering and Follow-ups
|
||||
|
||||
Inject guidance mid-conversation or queue follow-up messages:
|
||||
|
||||
```rust
|
||||
// Inject a steering message before the next LLM call
|
||||
session.steer("Focus on the root cause, not symptoms".into());
|
||||
|
||||
// Queue a follow-up that runs after the current input completes
|
||||
session.follow_up("Now run the test suite to verify".into());
|
||||
```
|
||||
|
||||
### Abort
|
||||
|
||||
Cancel a running session from another thread:
|
||||
|
||||
```rust
|
||||
let abort_flag = session.abort_flag_handle();
|
||||
// From another task:
|
||||
abort_flag.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
```
|
||||
|
||||
### Custom Tools
|
||||
|
||||
Register additional tools via the profile's `ToolRegistry`:
|
||||
|
||||
```rust
|
||||
use coding_agent_loop::tool_registry::{RegisteredTool, ToolExecutor};
|
||||
use unified_llm::types::ToolDefinition;
|
||||
use std::sync::Arc;
|
||||
|
||||
let custom_tool = RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "my_tool".into(),
|
||||
description: "Does something useful".into(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {"type": "string"}
|
||||
},
|
||||
"required": ["input"]
|
||||
}),
|
||||
},
|
||||
executor: Arc::new(|args, env| {
|
||||
Box::pin(async move {
|
||||
let input = args["input"].as_str().unwrap_or("");
|
||||
Ok(format!("Processed: {input}"))
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
// Register on a mutable profile before creating the session
|
||||
profile.tool_registry_mut().register(custom_tool);
|
||||
```
|
||||
|
||||
### Subagents
|
||||
|
||||
Spawn child sessions for delegated tasks:
|
||||
|
||||
```rust
|
||||
use coding_agent_loop::subagent::SubAgentManager;
|
||||
|
||||
let mut profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
|
||||
let factory = Arc::new(|| { /* create a new Session */ });
|
||||
|
||||
// Registers spawn_agent, send_input, wait, close_agent tools
|
||||
profile.register_subagent_tools(manager, factory, 0);
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
|
||||
- **Loop detection** -- Detects repeating tool call patterns (period 1, 2, or 3) and injects a steering warning
|
||||
- **Context window monitoring** -- Emits `ContextWindowWarning` events when estimated usage exceeds 80%
|
||||
- **Tool argument validation** -- Validates arguments against JSON Schema before execution
|
||||
- **Tool output truncation** -- Per-tool character and line limits with head/tail or tail-only truncation modes
|
||||
- **Environment variable filtering** -- `LocalExecutionEnvironment` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments
|
||||
- **Command timeouts** -- Configurable per-command with process group cleanup (SIGTERM then SIGKILL)
|
||||
- **Project doc discovery** -- Automatically discovers `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, or `.codex/instructions.md` based on provider, with a 32KB budget
|
||||
95
crates/unified-llm-cli/README.md
Normal file
95
crates/unified-llm-cli/README.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# unified-llm-cli
|
||||
|
||||
A command-line interface for interacting with LLM providers through the [unified-llm](../unified-llm/) library. Supports Anthropic, OpenAI, Google Gemini, and other providers with a single tool.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
cargo install --path crates/unified-llm-cli
|
||||
```
|
||||
|
||||
This installs the `ullm` binary.
|
||||
|
||||
## Configuration
|
||||
|
||||
API keys are read from environment variables. You can set them directly or place them in a `.env` file:
|
||||
|
||||
```sh
|
||||
export ANTHROPIC_API_KEY="sk-..."
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
export GEMINI_API_KEY="..."
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `ullm prompt`
|
||||
|
||||
Send a prompt to an LLM and print the response.
|
||||
|
||||
```sh
|
||||
ullm prompt "What is the capital of France?"
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-m, --model <MODEL>` | Model to use (defaults to first catalog model) |
|
||||
| `-s, --system <TEXT>` | System prompt |
|
||||
| `-o, --option <KEY=VALUE>` | Generation options: `temperature`, `max_tokens`, `top_p`, or provider-specific keys |
|
||||
| `-u, --usage` | Show token usage on stderr |
|
||||
| `--no-stream` | Disable streaming (wait for full response) |
|
||||
|
||||
**Stdin support:** Pipe text into `ullm prompt` to use it as input. If both stdin and an argument are provided, they are concatenated.
|
||||
|
||||
```sh
|
||||
echo "Summarize this" | ullm prompt
|
||||
cat article.txt | ullm prompt "Give me the key points"
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Use a specific model
|
||||
ullm prompt -m claude-opus-4-6 "Explain quicksort"
|
||||
|
||||
# Set a system prompt
|
||||
ullm prompt -s "You are a helpful translator" "Translate to French: hello"
|
||||
|
||||
# Adjust generation parameters
|
||||
ullm prompt -o temperature=0.2 -o max_tokens=500 "Write a haiku"
|
||||
|
||||
# Show token usage
|
||||
ullm prompt -u --no-stream "Hello"
|
||||
```
|
||||
|
||||
### `ullm models`
|
||||
|
||||
List available models from all providers. Running `ullm models` with no subcommand defaults to `ullm models list`.
|
||||
|
||||
```sh
|
||||
ullm models
|
||||
```
|
||||
|
||||
#### `ullm models list`
|
||||
|
||||
```sh
|
||||
ullm models list
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --provider <NAME>` | Filter models by provider (e.g., `anthropic`, `openai`, `gemini`) |
|
||||
| `-q, --query <TEXT>` | Search models by ID, display name, or alias (case-insensitive) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# List only Anthropic models
|
||||
ullm models list --provider anthropic
|
||||
|
||||
# Search for models matching "opus"
|
||||
ullm models list --query opus
|
||||
```
|
||||
240
crates/unified-llm/README.md
Normal file
240
crates/unified-llm/README.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# unified-llm
|
||||
|
||||
A unified async Rust client library for multiple LLM providers. Write your LLM integration code once and switch between Anthropic, OpenAI, and Google Gemini without changing your application logic.
|
||||
|
||||
## Key concepts
|
||||
|
||||
- **Client** -- Routes requests to registered provider adapters. Can be created explicitly or auto-configured from environment variables.
|
||||
- **ProviderAdapter** -- The trait every provider implements (`complete` and `stream`). Built-in adapters: `AnthropicAdapter`, `OpenAiAdapter`, `GeminiAdapter`, `OpenAiCompatibleAdapter`.
|
||||
- **Middleware** -- Intercepts requests/responses for logging, caching, or transformation. Supports both blocking and streaming paths.
|
||||
- **generate()** -- High-level function that wraps `Client.complete()` with automatic tool execution loops, retries, timeouts, and cancellation.
|
||||
- **Tool** -- Active tools (with an execute handler) run automatically in the tool loop. Passive tools (no handler) surface tool calls back to the caller.
|
||||
- **Model catalog** -- Built-in metadata for common models. Advisory only; unknown model strings pass through.
|
||||
|
||||
## Providers
|
||||
|
||||
| Provider | Adapter | API | Env var |
|
||||
|----------|---------|-----|---------|
|
||||
| Anthropic | `AnthropicAdapter` | Messages API | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI | `OpenAiAdapter` | Responses API | `OPENAI_API_KEY` |
|
||||
| Google Gemini | `GeminiAdapter` | generateContent | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |
|
||||
| OpenAI-compatible | `OpenAiCompatibleAdapter` | Chat Completions | (custom) |
|
||||
|
||||
All adapters support streaming, tool calling, structured output (`response_format`), and provider-specific options via `provider_options`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Auto-configure from environment
|
||||
|
||||
```rust
|
||||
use unified_llm::client::Client;
|
||||
use unified_llm::types::{Message, Request};
|
||||
|
||||
let client = Client::from_env().await?;
|
||||
|
||||
let request = Request {
|
||||
model: "claude-sonnet-4-5".to_string(),
|
||||
messages: vec![Message::user("What is the capital of France?")],
|
||||
provider: None,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
response_format: None,
|
||||
temperature: Some(0.0),
|
||||
top_p: None,
|
||||
max_tokens: Some(100),
|
||||
stop_sequences: None,
|
||||
reasoning_effort: None,
|
||||
metadata: None,
|
||||
provider_options: None,
|
||||
};
|
||||
|
||||
let response = client.complete(&request).await?;
|
||||
println!("{}", response.text());
|
||||
```
|
||||
|
||||
### High-level generate()
|
||||
|
||||
```rust
|
||||
use unified_llm::generate::{generate, GenerateParams};
|
||||
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5")
|
||||
.prompt("Explain monads in one sentence")
|
||||
.system("You are a concise programming tutor.")
|
||||
.max_tokens(200)
|
||||
).await?;
|
||||
|
||||
println!("{}", result.text());
|
||||
```
|
||||
|
||||
### Tool calling
|
||||
|
||||
```rust
|
||||
use unified_llm::generate::{generate, GenerateParams};
|
||||
use unified_llm::tools::Tool;
|
||||
use std::sync::Arc;
|
||||
|
||||
let weather_tool = Tool::active(
|
||||
"get_weather",
|
||||
"Get the current weather for a city",
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string", "description": "City name"}
|
||||
},
|
||||
"required": ["city"]
|
||||
}),
|
||||
|args, _ctx| async move {
|
||||
let city = args["city"].as_str().unwrap_or("unknown");
|
||||
Ok(serde_json::json!({"temp": "72F", "city": city}))
|
||||
},
|
||||
);
|
||||
|
||||
let result = generate(
|
||||
GenerateParams::new("claude-sonnet-4-5")
|
||||
.prompt("What's the weather in San Francisco?")
|
||||
.tools(vec![weather_tool])
|
||||
.max_tool_rounds(3)
|
||||
).await?;
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
```rust
|
||||
use unified_llm::client::Client;
|
||||
use unified_llm::types::{Message, Request, StreamEvent};
|
||||
use futures::StreamExt;
|
||||
|
||||
let client = Client::from_env().await?;
|
||||
let request = Request {
|
||||
model: "claude-sonnet-4-5".to_string(),
|
||||
messages: vec![Message::user("Tell me a joke")],
|
||||
// ...other fields set to None/defaults
|
||||
# provider: None, tools: None, tool_choice: None,
|
||||
# response_format: None, temperature: None, top_p: None,
|
||||
# max_tokens: None, stop_sequences: None, reasoning_effort: None,
|
||||
# metadata: None, provider_options: None,
|
||||
};
|
||||
|
||||
let mut stream = client.stream(&request).await?;
|
||||
while let Some(event) = stream.next().await {
|
||||
match event? {
|
||||
StreamEvent::TextDelta { delta, .. } => print!("{delta}"),
|
||||
StreamEvent::Finish { response, .. } => {
|
||||
println!("\nTokens used: {}", response.usage.total_tokens);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```rust
|
||||
use unified_llm::middleware::{Middleware, NextFn, NextStreamFn};
|
||||
use unified_llm::types::{Request, Response};
|
||||
use unified_llm::provider::StreamEventStream;
|
||||
use unified_llm::error::SdkError;
|
||||
|
||||
struct LoggingMiddleware;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Middleware for LoggingMiddleware {
|
||||
async fn handle_complete(
|
||||
&self,
|
||||
request: Request,
|
||||
next: NextFn,
|
||||
) -> Result<Response, SdkError> {
|
||||
eprintln!("Request to model: {}", request.model);
|
||||
let response = next(request).await?;
|
||||
eprintln!("Response tokens: {}", response.usage.total_tokens);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_stream(
|
||||
&self,
|
||||
request: Request,
|
||||
next: NextStreamFn,
|
||||
) -> Result<StreamEventStream, SdkError> {
|
||||
next(request).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI-compatible providers
|
||||
|
||||
```rust
|
||||
use unified_llm::providers::OpenAiCompatibleAdapter;
|
||||
use std::sync::Arc;
|
||||
|
||||
let adapter = OpenAiCompatibleAdapter::new("your-api-key", "https://api.groq.com/openai/v1")
|
||||
.with_name("groq");
|
||||
```
|
||||
|
||||
### Model catalog
|
||||
|
||||
```rust
|
||||
use unified_llm::catalog::{get_model_info, list_models, get_latest_model};
|
||||
|
||||
let info = get_model_info("claude-opus-4-6");
|
||||
let anthropic_models = list_models(Some("anthropic"));
|
||||
let best_reasoner = get_latest_model("anthropic", Some("reasoning"));
|
||||
```
|
||||
|
||||
## Key types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Request` | Unified request with model, messages, tools, temperature, etc. |
|
||||
| `Response` | Unified response with message, finish reason, usage, rate limit info |
|
||||
| `Message` | A message with role, content parts, and optional tool call ID |
|
||||
| `ContentPart` | Text, Image, Audio, Document, ToolCall, ToolResult, Thinking |
|
||||
| `StreamEvent` | Events for streaming: TextDelta, ToolCallStart/Delta/End, Finish, etc. |
|
||||
| `SdkError` | Typed errors with retryability, status codes, and provider error kinds |
|
||||
| `GenerateParams` | Builder for the high-level `generate()` function |
|
||||
| `GenerateResult` | Result containing response, tool results, total usage, and step history |
|
||||
| `ToolDefinition` | Tool name, description, and JSON Schema parameters |
|
||||
| `ToolChoice` | Auto, None, Required, or Named tool selection |
|
||||
| `Usage` | Token counts including input, output, reasoning, and cache tokens |
|
||||
| `RetryPolicy` | Configurable retry with exponential backoff, jitter, and max delay |
|
||||
| `ModelInfo` | Metadata about a model (context window, capabilities, costs) |
|
||||
|
||||
## Error handling
|
||||
|
||||
`SdkError` provides structured error variants with built-in retryability classification:
|
||||
|
||||
- **Retryable**: `RateLimit`, `Server`, `Network`, `Stream`, `RequestTimeout`
|
||||
- **Non-retryable**: `Authentication`, `AccessDenied`, `InvalidRequest`, `ContextLength`, `Configuration`
|
||||
|
||||
The `retry()` function and `generate()` respect `Retry-After` headers and use exponential backoff with jitter.
|
||||
|
||||
## Provider-specific options
|
||||
|
||||
Pass provider-specific parameters via `provider_options` without losing portability:
|
||||
|
||||
```rust
|
||||
use unified_llm::types::Request;
|
||||
|
||||
let request = Request {
|
||||
provider_options: Some(serde_json::json!({
|
||||
"anthropic": {
|
||||
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
||||
"auto_cache": true
|
||||
},
|
||||
"openai": {
|
||||
"store": true,
|
||||
"previous_response_id": "resp_abc123"
|
||||
},
|
||||
"gemini": {
|
||||
"safetySettings": [
|
||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}
|
||||
]
|
||||
}
|
||||
})),
|
||||
// ...other fields
|
||||
# model: String::new(), messages: vec![], provider: None, tools: None,
|
||||
# tool_choice: None, response_format: None, temperature: None,
|
||||
# top_p: None, max_tokens: None, stop_sequences: None,
|
||||
# reasoning_effort: None, metadata: None,
|
||||
};
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue