diff --git a/crates/attractor/README.md b/crates/attractor/README.md index b96a20a68..e8538eced 100644 --- a/crates/attractor/README.md +++ b/crates/attractor/README.md @@ -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 { + // 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) +``` diff --git a/crates/coding-agent-loop/README.md b/crates/coding-agent-loop/README.md new file mode 100644 index 000000000..ab1f2a084 --- /dev/null +++ b/crates/coding-agent-loop/README.md @@ -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, limit: Option) -> Result; + async fn write_file(&self, path: &str, content: &str) -> Result<(), String>; + async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result; + async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result, String>; + async fn glob(&self, pattern: &str, path: Option<&str>) -> Result, 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, + pub reasoning_effort: Option, + // ... 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 diff --git a/crates/unified-llm-cli/README.md b/crates/unified-llm-cli/README.md new file mode 100644 index 000000000..2f0c98f0f --- /dev/null +++ b/crates/unified-llm-cli/README.md @@ -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 to use (defaults to first catalog model) | +| `-s, --system ` | System prompt | +| `-o, --option ` | 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 ` | Filter models by provider (e.g., `anthropic`, `openai`, `gemini`) | +| `-q, --query ` | 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 +``` diff --git a/crates/unified-llm/README.md b/crates/unified-llm/README.md new file mode 100644 index 000000000..92f0ed792 --- /dev/null +++ b/crates/unified-llm/README.md @@ -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 { + 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 { + 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, +}; +```