mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-20 00:11:34 +00:00
Enable clippy::all, pedantic, nursery, and cargo lint groups at the workspace level. Fix all resulting warnings: merge identical match arms, add #[must_use] and doc sections, derive Eq, use clone_from, add type alias for complex types, extract helpers to reduce function length, and add crate metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
257 lines
6.5 KiB
Markdown
257 lines
6.5 KiB
Markdown
# 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`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
unified-llm = { path = "crates/unified-llm" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Simple Generation
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|