mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Resolve the model catalog table conflict in docs/public/core-concepts/models.mdx by keeping both changes: this branch's `kimi` -> `moonshot` provider rename for the Kimi rows, and main's new DeepSeek V4 rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
991 lines
34 KiB
Text
991 lines
34 KiB
Text
---
|
|
title: "Fabro SDK"
|
|
description: "Using Fabro as a Rust library for AI agents and multi-provider LLM completions"
|
|
---
|
|
|
|
Fabro can be used as a Rust SDK with two primary entry points:
|
|
|
|
- **`fabro-agent`** — a full AI coding agent with tool use, sandboxed execution, event streaming, and context management. Use this when you want to build an agent that can read files, run commands, and interact with a codebase.
|
|
- **`fabro-llm`** — a standalone LLM client for multi-provider completions, streaming, and tool execution loops. Use this when you want direct control over LLM calls without the agent layer.
|
|
|
|
Both crates can be used independently of Fabro's workflow engine.
|
|
|
|
## Agent (`fabro-agent`)
|
|
|
|
The `fabro-agent` crate provides a session-based AI agent that runs an LLM with tool use in a sandboxed environment. The agent loop streams LLM responses, executes tool calls (`shell`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `web_fetch`, `web_search`), feeds results back, and repeats until the model responds with text or hits a safety limit.
|
|
|
|
```toml title="Cargo.toml"
|
|
[dependencies]
|
|
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-agent = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-model = { git = "https://github.com/fabro-sh/fabro" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
```
|
|
|
|
### Quick start
|
|
|
|
```rust
|
|
use fabro_agent::{
|
|
AnthropicProfile, LocalSandbox, Session, SessionOptions,
|
|
};
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_model::catalog::LlmCatalogSettings;
|
|
use fabro_model::Catalog;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let source = EnvCredentialSource::new();
|
|
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
|
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
|
let sandbox = Arc::new(LocalSandbox::new(PathBuf::from(".")));
|
|
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-5"));
|
|
let config = SessionOptions::default();
|
|
|
|
let mut session = Session::new(client, profile, sandbox, config);
|
|
session.initialize().await?;
|
|
|
|
// Subscribe to events before sending input
|
|
let mut events = session.subscribe();
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = events.recv().await {
|
|
if let fabro_agent::AgentEvent::TextDelta { delta } = &event.event {
|
|
print!("{delta}");
|
|
}
|
|
}
|
|
});
|
|
|
|
session.process_input("List the files in this directory").await?;
|
|
session.close();
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Session
|
|
|
|
`Session` is the core type. It holds the LLM client, a provider profile, a sandbox, and configuration. The main loop lives inside `process_input()`.
|
|
|
|
**Constructor:**
|
|
|
|
```rust
|
|
pub fn new(
|
|
llm_client: Client,
|
|
provider_profile: Arc<dyn AgentProfile>,
|
|
sandbox: Arc<dyn Sandbox>,
|
|
config: SessionOptions,
|
|
) -> Self
|
|
```
|
|
|
|
**Lifecycle methods:**
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `initialize().await` | Discovers project docs, skills, and MCP servers. Call before `process_input`. |
|
|
| `process_input(input).await` | Sends user input and runs the agent loop until the model stops, the session is interrupted, or an error occurs. |
|
|
| `close()` | Ends the session and emits `SessionEnded`. |
|
|
| `interrupt()` | Cancels the current `process_input` call. |
|
|
| `cancel_token()` | Returns a `CancellationToken` for external cancellation. |
|
|
|
|
**Inspection:**
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `state()` | Returns `SessionState`: `Idle`, `Thinking`, `Executing`, or `Closed`. |
|
|
| `history()` | Returns the conversation as `&History` (a sequence of `Turn` values). |
|
|
| `subscribe()` | Returns a broadcast receiver for `SessionEvent` values. |
|
|
|
|
**Steering:**
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `steer(message)` | Injects a system-level guidance message into the next LLM call. |
|
|
| `follow_up(message)` | Queues a follow-up user message after the current turn completes. |
|
|
|
|
### SessionOptions
|
|
|
|
All fields are public. Key settings with their defaults:
|
|
|
|
| Field | Default | Description |
|
|
|---|---|---|
|
|
| `default_command_timeout_ms` | `10,000` | Default timeout for Bash tool commands. |
|
|
| `max_command_timeout_ms` | `600,000` | Maximum allowed timeout for Bash tool commands. |
|
|
| `enable_loop_detection` | `true` | Detect and break out of repetitive tool call patterns. |
|
|
| `enable_context_compaction` | `true` | Automatically summarize old turns when approaching the context window limit. |
|
|
| `compaction_threshold_percent` | `80` | Context window usage percentage that triggers compaction. |
|
|
| `max_subagent_depth` | `1` | Maximum nesting depth for sub-agents. |
|
|
| `wall_clock_timeout` | `None` | Hard timeout for `process_input`. Triggers `InterruptReason::WallClockTimeout`. |
|
|
| `tool_hooks` | `None` | Pre/post hooks around tool execution (see [Tool hooks](#tool-hooks)). |
|
|
| `mcp_servers` | `[]` | MCP server configurations to connect on startup. |
|
|
| `skill_dirs` | `None` | Directories to discover `SKILL.md` files. `None` uses convention defaults. |
|
|
|
|
### Sandbox
|
|
|
|
The `Sandbox` trait abstracts where tools execute — local filesystem, Docker container, SSH remote, or a cloud sandbox. All tool operations go through this interface.
|
|
|
|
```rust
|
|
#[async_trait]
|
|
pub trait Sandbox: Send + Sync {
|
|
async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>, String>;
|
|
async fn read_file_text(&self, path: &str) -> Result<String, String>;
|
|
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 delete_file(&self, path: &str) -> Result<(), String>;
|
|
async fn file_exists(&self, path: &str) -> Result<bool, String>;
|
|
async fn list_directory(&self, path: &str, depth: Option<usize>) -> Result<Vec<DirEntry>, String>;
|
|
async fn exec_command(
|
|
&self,
|
|
command: &str,
|
|
timeout_ms: u64,
|
|
working_dir: Option<&str>,
|
|
env_vars: Option<&HashMap<String, String>>,
|
|
cancel_token: Option<CancellationToken>,
|
|
) -> Result<ExecResult, String>;
|
|
async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<String>, String>;
|
|
async fn walk_files(&self, base: &str, relative_start: &str, options: &WalkOptions) -> Result<Vec<SandboxFile>, String>;
|
|
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String>;
|
|
async fn initialize(&self) -> Result<(), String>;
|
|
async fn cleanup(&self) -> Result<(), String>;
|
|
fn working_directory(&self) -> &str;
|
|
fn platform(&self) -> &str;
|
|
fn os_version(&self) -> String;
|
|
// ... optional methods with defaults: setup_git(), git_push_ref(), etc.
|
|
}
|
|
```
|
|
|
|
**Built-in implementations:**
|
|
|
|
| Type | Description |
|
|
|---|---|
|
|
| `LocalSandbox` | Executes directly on the local filesystem. |
|
|
| `DockerSandbox` | Runs inside a Docker container (feature-gated: `docker`). |
|
|
|
|
The `DaytonaSandbox` implementation (feature-gated: `daytona`) runs inside a Daytona cloud sandbox.
|
|
|
|
### Provider profiles
|
|
|
|
The `AgentProfile` trait encapsulates LLM-specific system prompts, tool definitions, and capability metadata. It controls how the agent presents itself to the model.
|
|
|
|
```rust
|
|
pub trait AgentProfile: Send + Sync {
|
|
fn provider(&self) -> Provider;
|
|
fn model(&self) -> &str;
|
|
fn tool_registry(&self) -> &ToolRegistry;
|
|
fn tool_registry_mut(&mut self) -> &mut ToolRegistry;
|
|
fn build_system_prompt(&self, env: &dyn Sandbox, ...) -> String;
|
|
fn capabilities(&self) -> ProfileCapabilities;
|
|
fn tools(&self) -> Vec<ToolDefinition>;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Built-in profiles: `AnthropicProfile`, `OpenAiProfile`, `GeminiProfile`.
|
|
|
|
### Events
|
|
|
|
All operations emit `AgentEvent` values through a tokio broadcast channel. Subscribe before calling `process_input()`.
|
|
|
|
```rust
|
|
let mut rx = session.subscribe();
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = rx.recv().await {
|
|
match event.event {
|
|
AgentEvent::TextDelta { delta } => print!("{delta}"),
|
|
AgentEvent::ToolCallStarted { tool_name, .. } => {
|
|
println!("[calling {tool_name}]");
|
|
}
|
|
AgentEvent::ToolCallCompleted { tool_name, is_error, .. } => {
|
|
println!("[{tool_name} done, error={is_error}]");
|
|
}
|
|
AgentEvent::LoopDetected => println!("[loop detected]"),
|
|
AgentEvent::CompactionCompleted { .. } => println!("[context compacted]"),
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Key `AgentEvent` variants:
|
|
|
|
| Variant | Description |
|
|
|---|---|
|
|
| `SessionStarted` / `SessionEnded` | Session lifecycle. |
|
|
| `TextDelta { delta }` | Incremental text from the model. |
|
|
| `ReasoningDelta { delta }` | Incremental reasoning/thinking text. |
|
|
| `AssistantMessage { text, model, usage, tool_call_count }` | Complete assistant turn with token usage. |
|
|
| `ToolCallStarted { tool_name, tool_call_id, arguments }` | A tool call is about to execute. |
|
|
| `ToolCallCompleted { tool_name, tool_call_id, output, is_error }` | A tool call finished. |
|
|
| `Error { error }` | An `AgentError` occurred. |
|
|
| `LoopDetected` | The agent is repeating itself. |
|
|
| `CompactionStarted` / `CompactionCompleted` | Context window compaction. |
|
|
| `SubAgentSpawned` / `SubAgentCompleted` | Sub-agent lifecycle. |
|
|
| `McpServerReady` / `McpServerFailed` | MCP server connection status. |
|
|
|
|
### Tool hooks
|
|
|
|
Implement `ToolHookCallback` to intercept tool calls for approval, logging, or transformation:
|
|
|
|
```rust
|
|
use fabro_agent::{ToolHookCallback, ToolHookDecision};
|
|
use async_trait::async_trait;
|
|
|
|
struct MyHooks;
|
|
|
|
#[async_trait]
|
|
impl ToolHookCallback for MyHooks {
|
|
async fn pre_tool_use(
|
|
&self,
|
|
tool_name: &str,
|
|
tool_input: &serde_json::Value,
|
|
) -> ToolHookDecision {
|
|
if tool_name == "shell" {
|
|
println!("Agent wants to run: {}", tool_input["command"]);
|
|
}
|
|
ToolHookDecision::Proceed // or Block { reason }
|
|
}
|
|
|
|
async fn post_tool_use(&self, tool_name: &str, _call_id: &str, _output: &str) {
|
|
println!("{tool_name} completed");
|
|
}
|
|
|
|
async fn post_tool_use_failure(&self, tool_name: &str, _call_id: &str, error: &str) {
|
|
eprintln!("{tool_name} failed: {error}");
|
|
}
|
|
}
|
|
```
|
|
|
|
Pass hooks via `SessionOptions`:
|
|
|
|
```rust
|
|
let config = SessionOptions {
|
|
tool_hooks: Some(Arc::new(MyHooks)),
|
|
..Default::default()
|
|
};
|
|
```
|
|
|
|
For simple sync approval, use `ToolApprovalAdapter` to wrap a closure:
|
|
|
|
```rust
|
|
use fabro_agent::ToolApprovalAdapter;
|
|
use std::sync::Arc;
|
|
|
|
let config = SessionOptions {
|
|
tool_hooks: Some(Arc::new(ToolApprovalAdapter(Arc::new(|tool_name, _args| {
|
|
if tool_name == "shell" {
|
|
Err("shell is not allowed".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})))),
|
|
..Default::default()
|
|
};
|
|
```
|
|
|
|
### Error handling
|
|
|
|
All fallible `Session` methods return `Result<T, AgentError>`:
|
|
|
|
| Variant | Description |
|
|
|---|---|
|
|
| `Llm(SdkError)` | An error from the LLM provider (wraps `fabro_llm::error::SdkError`). |
|
|
| `SessionClosed` | `process_input` was called on a closed session. |
|
|
| `InvalidState(String)` | The session is in an unexpected state. |
|
|
| `ToolExecution(String)` | A tool execution failed. |
|
|
| `Interrupted(InterruptReason)` | The session was cancelled or timed out. |
|
|
|
|
---
|
|
|
|
## LLM client (`fabro-llm`)
|
|
|
|
The `fabro-llm` crate is a standalone Rust library for calling LLM providers. It provides a unified client that routes requests to Anthropic, OpenAI, Gemini, and other providers, with built-in streaming, tool execution, retries, and middleware.
|
|
|
|
You can use it independently of Fabro's workflow engine — add it as a dependency in any Rust project.
|
|
|
|
```toml title="Cargo.toml"
|
|
[dependencies]
|
|
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-model = { git = "https://github.com/fabro-sh/fabro" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
serde_json = "1"
|
|
```
|
|
|
|
### Quick start
|
|
|
|
The simplest path is an environment-backed `CredentialSource`, an explicit `Arc<Catalog>`, then `Client::from_source(&source, catalog)`. That keeps credential and model resolution explicit while still auto-reading environment variables such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GEMINI_API_KEY`.
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::generate::{generate, GenerateParams};
|
|
use fabro_model::catalog::LlmCatalogSettings;
|
|
use fabro_model::Catalog;
|
|
use std::sync::Arc;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let source = EnvCredentialSource::new();
|
|
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
|
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
|
|
|
let result = generate(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("Explain ownership in Rust in two sentences.")
|
|
).await?;
|
|
|
|
println!("{}", result.text());
|
|
println!("Tokens used: {}", result.total_usage.total_tokens);
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Client
|
|
|
|
`Client` is the core type that holds provider adapters and middleware. It routes each request to the appropriate provider.
|
|
|
|
#### Creating from a credential source
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_model::catalog::LlmCatalogSettings;
|
|
use fabro_model::Catalog;
|
|
use std::sync::Arc;
|
|
|
|
let source = EnvCredentialSource::new();
|
|
let catalog = Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
|
let client = Client::from_source(&source, Arc::clone(&catalog)).await?;
|
|
```
|
|
|
|
For env-backed usage, `EnvCredentialSource` checks for API key environment variables and registers adapters for each provider found:
|
|
|
|
| Environment variable | Provider |
|
|
|---|---|
|
|
| `ANTHROPIC_API_KEY` | Anthropic |
|
|
| `OPENAI_API_KEY` | OpenAI |
|
|
| `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Gemini |
|
|
| `MOONSHOT_API_KEY` or `KIMI_API_KEY` | Moonshot AI; `MOONSHOT_API_KEY` takes precedence |
|
|
| `ZAI_API_KEY` | ZAI |
|
|
| `MINIMAX_API_KEY` | Minimax |
|
|
| `INCEPTION_API_KEY` | Inception |
|
|
| `POOLSIDE_API_KEY` | Poolside |
|
|
| `DEEPSEEK_API_KEY` | DeepSeek |
|
|
| `OPENROUTER_API_KEY` | OpenRouter, when enabled in settings |
|
|
|
|
The first provider registered becomes the default. Provider base URLs come from the model catalog. For vault-backed usage inside Fabro, use `fabro_auth::VaultCredentialSource` instead.
|
|
|
|
The built-in Modal definition reads two proxy-token headers from the vault, so `EnvCredentialSource` does not configure it automatically. For direct SDK use, enable Modal and set its endpoint URL in the catalog:
|
|
|
|
```toml
|
|
[llm.providers.modal]
|
|
enabled = true
|
|
base_url = "https://your-endpoint.modal.run/v1"
|
|
```
|
|
|
|
Then read the two environment variables explicitly and create a typed credential after constructing `catalog` from those settings:
|
|
|
|
```rust
|
|
use fabro_auth::ApiCredential;
|
|
use fabro_llm::client::Client;
|
|
use std::collections::HashMap;
|
|
|
|
let credential = ApiCredential::with_extra_headers(
|
|
"modal",
|
|
HashMap::from([
|
|
("Modal-Key".to_string(), std::env::var("MODAL_TOKEN_ID")?),
|
|
(
|
|
"Modal-Secret".to_string(),
|
|
std::env::var("MODAL_TOKEN_SECRET")?,
|
|
),
|
|
]),
|
|
);
|
|
let client = Client::from_credentials(vec![credential], catalog).await?;
|
|
```
|
|
|
|
#### Creating manually
|
|
|
|
```rust
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::providers::AnthropicAdapter;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
let adapter = AnthropicAdapter::new("sk-ant-...")
|
|
.with_base_url("https://custom-proxy.example.com");
|
|
|
|
let mut providers = HashMap::new();
|
|
providers.insert("anthropic".to_string(), Arc::new(adapter) as _);
|
|
|
|
let client = Client::new(providers, Some("anthropic".to_string()), vec![]);
|
|
```
|
|
|
|
#### Low-level calls
|
|
|
|
For direct control without the tool loop, use `complete()` and `stream()` on the client:
|
|
|
|
```rust
|
|
use fabro_llm::types::{Request, Message};
|
|
|
|
let request = Request {
|
|
model: "claude-sonnet-4-5".into(),
|
|
messages: vec![Message::user("Hello")],
|
|
..Default::default()
|
|
};
|
|
|
|
let response = client.complete(&request).await?;
|
|
println!("{}", response.text());
|
|
```
|
|
|
|
### High-level generation
|
|
|
|
The `generate()` function wraps the client with automatic tool execution loops, retries, and timeouts. It is the recommended entry point for most use cases.
|
|
|
|
#### Basic completion
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::generate::{generate, GenerateParams};
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let result = generate(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.system("You are a helpful assistant.")
|
|
.prompt("What is the capital of France?")
|
|
.temperature(0.0)
|
|
).await?;
|
|
|
|
println!("{}", result.text());
|
|
```
|
|
|
|
#### Multi-turn conversations
|
|
|
|
Use `.messages()` instead of `.prompt()` to pass a full conversation history:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::types::Message;
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let result = generate(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.messages(vec![
|
|
Message::user("My name is Alice."),
|
|
Message::assistant("Hello Alice! How can I help you?"),
|
|
Message::user("What's my name?"),
|
|
])
|
|
).await?;
|
|
```
|
|
|
|
<Note>
|
|
You cannot use both `.prompt()` and `.messages()` on the same request — this returns `SdkError::Configuration`.
|
|
</Note>
|
|
|
|
#### GenerateParams reference
|
|
|
|
| Method | Type | Description |
|
|
|---|---|---|
|
|
| `new(model, client)` | `(impl Into<String>, Arc<Client>)` | Required. Model ID or alias plus the client to use |
|
|
| `.prompt(text)` | `impl Into<String>` | Convenience: sends a single user message |
|
|
| `.messages(msgs)` | `Vec<Message>` | Full conversation history |
|
|
| `.system(text)` | `impl Into<String>` | System prompt |
|
|
| `.tools(tools)` | `Vec<Tool>` | Tools available to the model |
|
|
| `.tool_choice(choice)` | `ToolChoice` | How the model selects tools |
|
|
| `.max_tool_rounds(n)` | `u32` | Max tool execution rounds (default: 1) |
|
|
| `.temperature(t)` | `f64` | Sampling temperature |
|
|
| `.top_p(p)` | `f64` | Nucleus sampling |
|
|
| `.max_tokens(n)` | `i64` | Maximum output tokens |
|
|
| `.stop_sequences(seqs)` | `Vec<String>` | Stop sequences |
|
|
| `.reasoning_effort(level)` | `impl Into<String>` | e.g. `"low"`, `"medium"`, `"high"` |
|
|
| `.provider(name)` | `impl Into<String>` | Force a specific provider |
|
|
| `.max_retries(n)` | `u32` | Retry count for transient errors (default: 2) |
|
|
| `.timeout(config)` | `TimeoutConfig` | Total and per-step timeouts |
|
|
| `.abort_signal(token)` | `CancellationToken` | Cancel generation |
|
|
| `.stop_when(f)` | `Fn(&[StepResult]) -> bool` | Custom stop condition after each tool round |
|
|
|
|
#### GenerateResult
|
|
|
|
`GenerateResult` dereferences to `Response`, so you can call response methods directly:
|
|
|
|
```rust
|
|
let result = generate(params).await?;
|
|
|
|
// Response methods (via Deref)
|
|
result.text(); // concatenated text output
|
|
result.tool_calls(); // Vec<ToolCall> from the final response
|
|
result.reasoning(); // Option<String> — extended thinking content
|
|
|
|
// GenerateResult fields
|
|
result.response; // Response — the final LLM response
|
|
result.tool_results; // Vec<ToolResult> — from the final step
|
|
result.total_usage; // Usage — aggregated across all steps
|
|
result.steps; // Vec<StepResult> — one per tool round
|
|
result.output; // Option<Value> — for structured output
|
|
```
|
|
|
|
### Tools
|
|
|
|
Tools let the model call functions during generation. There are two kinds:
|
|
|
|
- **Active tools** have an execute handler — Fabro runs them automatically and feeds results back to the model.
|
|
- **Passive tools** have no handler — Fabro returns the tool calls to you in the response.
|
|
|
|
#### Defining an active tool
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::tools::Tool;
|
|
use serde_json::json;
|
|
|
|
let weather = Tool::active(
|
|
"get_weather",
|
|
"Get the current weather for a city",
|
|
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(json!({ "temperature": "72°F", "city": city }))
|
|
},
|
|
);
|
|
```
|
|
|
|
#### Using tools with generate
|
|
|
|
```rust
|
|
# use fabro_auth::EnvCredentialSource;
|
|
# use fabro_llm::client::Client;
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let result = generate(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("What's the weather in San Francisco?")
|
|
.tools(vec![weather])
|
|
.max_tool_rounds(5)
|
|
).await?;
|
|
|
|
// Inspect the tool execution history
|
|
for (i, step) in result.steps.iter().enumerate() {
|
|
let calls = step.response.tool_calls();
|
|
println!("Step {i}: {} tool calls, {} results", calls.len(), step.tool_results.len());
|
|
}
|
|
```
|
|
|
|
The `generate()` function loops automatically: the model calls tools, Fabro executes them, feeds results back, and repeats until the model stops or `max_tool_rounds` is reached.
|
|
|
|
#### Tool choice
|
|
|
|
Control how the model selects tools:
|
|
|
|
```rust
|
|
use fabro_llm::types::ToolChoice;
|
|
|
|
// Let the model decide (default)
|
|
# use fabro_auth::EnvCredentialSource;
|
|
# use fabro_llm::client::Client;
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Auto);
|
|
|
|
// Force a specific tool
|
|
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Named {
|
|
tool_name: "get_weather".into()
|
|
});
|
|
|
|
// Force the model to use some tool
|
|
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::Required);
|
|
|
|
// Prevent tool use
|
|
GenerateParams::new("opus", client.clone()).tool_choice(ToolChoice::None);
|
|
```
|
|
|
|
#### Passive tools
|
|
|
|
Passive tools let you handle execution yourself:
|
|
|
|
```rust
|
|
# use fabro_auth::EnvCredentialSource;
|
|
# use fabro_llm::client::Client;
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let search = Tool::passive(
|
|
"search",
|
|
"Search the codebase",
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"query": { "type": "string" }
|
|
},
|
|
"required": ["query"]
|
|
}),
|
|
);
|
|
|
|
let result = generate(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("Find all uses of the Config struct")
|
|
.tools(vec![search])
|
|
).await?;
|
|
|
|
// Handle tool calls yourself
|
|
for call in result.tool_calls() {
|
|
println!("Model wants to call {} with {}", call.name, call.arguments);
|
|
}
|
|
```
|
|
|
|
### Streaming
|
|
|
|
#### Text stream
|
|
|
|
For simple cases where you only need the text deltas:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::generate::{stream, GenerateParams};
|
|
use futures::StreamExt;
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let stream_result = stream(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("Write a haiku about Rust")
|
|
).await?;
|
|
|
|
let mut text_stream = stream_result.text_stream();
|
|
while let Some(chunk) = text_stream.next().await {
|
|
print!("{}", chunk?);
|
|
}
|
|
```
|
|
|
|
#### Full event stream
|
|
|
|
For fine-grained control, consume `StreamEvent` variants directly:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::generate::{stream, GenerateParams};
|
|
use fabro_llm::types::StreamEvent;
|
|
use futures::StreamExt;
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let mut stream_result = stream(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("Explain monads")
|
|
).await?;
|
|
|
|
while let Some(event) = stream_result.next().await {
|
|
match event? {
|
|
StreamEvent::TextDelta { delta, .. } => print!("{delta}"),
|
|
StreamEvent::ReasoningDelta { delta } => eprint!("[thinking] {delta}"),
|
|
StreamEvent::ToolCallStart { tool_call } => {
|
|
println!("\n> Calling tool: {}", tool_call.name);
|
|
}
|
|
StreamEvent::StepFinish { usage, .. } => {
|
|
println!("\n[step done, {} tokens]", usage.total_tokens);
|
|
}
|
|
StreamEvent::Finish { response, .. } => {
|
|
println!("\n[done: {:?}]", response.finish_reason);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### StreamEvent variants
|
|
|
|
| Variant | Description |
|
|
|---|---|
|
|
| `StreamStart` | Stream opened |
|
|
| `TextStart { text_id }` | Text block started |
|
|
| `TextDelta { delta, text_id }` | Incremental text chunk |
|
|
| `TextEnd { text_id }` | Text block ended |
|
|
| `ReasoningStart` | Extended thinking started |
|
|
| `ReasoningDelta { delta }` | Incremental reasoning chunk |
|
|
| `ReasoningEnd` | Extended thinking ended |
|
|
| `ToolCallStart { tool_call }` | Tool call started |
|
|
| `ToolCallDelta { tool_call }` | Incremental tool call arguments |
|
|
| `ToolCallEnd { tool_call }` | Tool call complete |
|
|
| `StepFinish { finish_reason, usage, response, tool_calls, tool_results }` | A tool round completed (more rounds may follow) |
|
|
| `Finish { finish_reason, usage, response }` | Generation complete |
|
|
| `Error { error, raw }` | Provider error |
|
|
|
|
### Structured output
|
|
|
|
Generate typed JSON objects that conform to a JSON Schema:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_llm::generate::{generate_object, GenerateParams};
|
|
use serde_json::json;
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"age": { "type": "integer" },
|
|
"hobbies": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
}
|
|
},
|
|
"required": ["name", "age", "hobbies"]
|
|
});
|
|
|
|
let result = generate_object(
|
|
GenerateParams::new("claude-sonnet-4-5", client.clone())
|
|
.prompt("Generate a profile for a fictional character"),
|
|
schema,
|
|
).await?;
|
|
|
|
let profile = result.output.expect("structured output");
|
|
println!("Name: {}", profile["name"]);
|
|
```
|
|
|
|
### Middleware
|
|
|
|
Middleware intercepts requests and responses for logging, caching, or transformation:
|
|
|
|
```rust
|
|
use fabro_llm::middleware::{Middleware, NextFn, NextStreamFn};
|
|
use fabro_llm::provider::StreamEventStream;
|
|
use fabro_llm::types::{Request, Response};
|
|
use fabro_llm::error::SdkError;
|
|
use async_trait::async_trait;
|
|
|
|
struct LoggingMiddleware;
|
|
|
|
#[async_trait]
|
|
impl Middleware for LoggingMiddleware {
|
|
async fn handle_complete(
|
|
&self,
|
|
request: Request,
|
|
next: NextFn,
|
|
) -> Result<Response, SdkError> {
|
|
println!("Request to model: {}", request.model);
|
|
let response = next(request).await?;
|
|
println!("Response: {} tokens", response.usage.total_tokens);
|
|
Ok(response)
|
|
}
|
|
|
|
async fn handle_stream(
|
|
&self,
|
|
request: Request,
|
|
next: NextStreamFn,
|
|
) -> Result<StreamEventStream, SdkError> {
|
|
println!("Streaming request to model: {}", request.model);
|
|
next(request).await
|
|
}
|
|
}
|
|
```
|
|
|
|
Add middleware to the client:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use fabro_model::catalog::LlmCatalogSettings;
|
|
use fabro_model::Catalog;
|
|
|
|
let source = EnvCredentialSource::new();
|
|
let catalog = std::sync::Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())?);
|
|
let mut client = Client::from_source(&source, catalog).await?;
|
|
client.add_middleware(std::sync::Arc::new(LoggingMiddleware));
|
|
```
|
|
|
|
### Model catalog
|
|
|
|
The crate embeds a catalog of known models with metadata:
|
|
|
|
```rust
|
|
use fabro_llm::catalog;
|
|
|
|
// Look up a model by ID or alias
|
|
let info = catalog::get_model_info("opus").unwrap();
|
|
println!("{} ({})", info.display_name, info.provider);
|
|
println!("Context: {} tokens", info.limits.context_window);
|
|
println!("Tools: {}, Vision: {}", info.features.tools, info.features.vision);
|
|
|
|
// List all models for a provider
|
|
let models = catalog::list_models(Some("anthropic"));
|
|
|
|
// Get the default model for a provider
|
|
let default = catalog::default_model_for_provider("openai").unwrap();
|
|
|
|
// Find a capability-matched model on a different provider
|
|
let equivalent = catalog::closest_model("gemini", &info);
|
|
```
|
|
|
|
See [Models](/core-concepts/models) for the full catalog table.
|
|
|
|
### Error handling
|
|
|
|
All fallible operations return `Result<T, SdkError>`. The error type classifies failures to enable retry and failover decisions:
|
|
|
|
```rust
|
|
use fabro_llm::error::SdkError;
|
|
|
|
match result {
|
|
Err(SdkError::Provider { kind, detail }) => {
|
|
println!("Provider error ({}): {}", detail.provider, detail.message);
|
|
if let Some(code) = detail.status_code {
|
|
println!("HTTP {code}");
|
|
}
|
|
}
|
|
Err(SdkError::RequestTimeout { message, .. }) => println!("Timeout: {message}"),
|
|
Err(SdkError::Network { message, .. }) => println!("Network: {message}"),
|
|
Err(SdkError::Interrupt { message }) => println!("Cancelled: {message}"),
|
|
Err(e) => println!("Other: {e}"),
|
|
Ok(_) => {}
|
|
}
|
|
```
|
|
|
|
#### Error classification
|
|
|
|
Every `SdkError` exposes classification methods:
|
|
|
|
| Method | Returns | Description |
|
|
|---|---|---|
|
|
| `retryable()` | `bool` | Safe to retry with the same provider (e.g. rate limit, server error) |
|
|
| `failover_eligible()` | `bool` | Safe to try a different provider |
|
|
| `retry_after()` | `Option<f64>` | Seconds to wait before retrying (from provider `Retry-After` header) |
|
|
| `status_code()` | `Option<u16>` | HTTP status code, if applicable |
|
|
| `provider_name()` | `&str` | Which provider returned the error |
|
|
|
|
#### Provider error kinds
|
|
|
|
| Kind | HTTP status | Retryable | Failover |
|
|
|---|---|---|---|
|
|
| `Authentication` | 401 | No | No |
|
|
| `AccessDenied` | 403 | No | No |
|
|
| `NotFound` | 404 | No | No |
|
|
| `InvalidRequest` | 400 | No | No |
|
|
| `RateLimit` | 429 | Yes | Yes |
|
|
| `Server` | 500, 502, 503 | Yes | Yes |
|
|
| `ContentFilter` | varies | No | No |
|
|
| `ContextLength` | varies | No | No |
|
|
| `QuotaExceeded` | varies | No | Yes |
|
|
|
|
### Retries
|
|
|
|
The `generate()` function retries automatically based on `max_retries` (default: 2). For low-level use, the `retry` function wraps any async operation:
|
|
|
|
```rust
|
|
use fabro_llm::retry::retry;
|
|
use fabro_llm::types::RetryPolicy;
|
|
|
|
let policy = RetryPolicy {
|
|
max_retries: 3,
|
|
base_delay: 1.0,
|
|
max_delay: 60.0,
|
|
backoff_multiplier: 2.0,
|
|
jitter: true,
|
|
on_retry: None,
|
|
};
|
|
|
|
let response = retry(&policy, || {
|
|
let c = client.clone();
|
|
let r = request.clone();
|
|
async move { c.complete(&r).await }
|
|
}).await?;
|
|
```
|
|
|
|
Retry only fires when `error.retryable()` returns `true` and respects `Retry-After` headers.
|
|
|
|
### Cancellation
|
|
|
|
Pass a `CancellationToken` to interrupt long-running generation:
|
|
|
|
```rust
|
|
use fabro_auth::EnvCredentialSource;
|
|
use fabro_llm::client::Client;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
# let source = EnvCredentialSource::new();
|
|
# let catalog = std::sync::Arc::new(fabro_model::Catalog::from_builtin_with_overrides(&fabro_model::catalog::LlmCatalogSettings::default()).unwrap());
|
|
# let client = Client::from_source(&source, catalog).await?;
|
|
let token = CancellationToken::new();
|
|
let token_clone = token.clone();
|
|
|
|
// Cancel after 30 seconds
|
|
tokio::spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
|
token_clone.cancel();
|
|
});
|
|
|
|
let result = generate(
|
|
GenerateParams::new("opus", client.clone())
|
|
.prompt("Write a novel")
|
|
.abort_signal(token)
|
|
).await;
|
|
// Returns SdkError::Interrupt if cancelled
|
|
```
|
|
|
|
### Provider adapters
|
|
|
|
Each provider has a dedicated adapter. All adapters implement the `ProviderAdapter` trait and are interchangeable.
|
|
|
|
| Adapter | Provider | Constructor |
|
|
|---|---|---|
|
|
| `AnthropicAdapter` | Anthropic Messages API | `::new(api_key)` |
|
|
| `OpenAiAdapter` | OpenAI Responses API | `::new(api_key)` |
|
|
| `GeminiAdapter` | Google Gemini API | `::new(api_key)` |
|
|
| `OpenAiCompatibleAdapter` | Any OpenAI-compatible endpoint | `::new(api_key, base_url)` |
|
|
|
|
All adapters support `.with_base_url()` for proxies or custom endpoints. `OpenAiAdapter` also supports `.with_org_id()` and `.with_project_id()`.
|
|
|
|
#### Custom provider
|
|
|
|
Implement the `ProviderAdapter` trait to add a new provider:
|
|
|
|
```rust
|
|
use fabro_llm::provider::{ProviderAdapter, StreamEventStream};
|
|
use fabro_llm::types::{Request, Response};
|
|
use fabro_llm::error::SdkError;
|
|
use async_trait::async_trait;
|
|
|
|
struct MyProvider;
|
|
|
|
#[async_trait]
|
|
impl ProviderAdapter for MyProvider {
|
|
fn name(&self) -> &str { "my-provider" }
|
|
|
|
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
|
// Call your provider's API
|
|
todo!()
|
|
}
|
|
|
|
async fn stream(&self, request: &Request) -> Result<StreamEventStream, SdkError> {
|
|
// Return a stream of events
|
|
todo!()
|
|
}
|
|
}
|
|
```
|
|
|
|
Register it on the client:
|
|
|
|
```rust
|
|
client.register_provider(Arc::new(MyProvider)).await?;
|
|
```
|