LLM Providers
LLM provider configuration, prefix-based routing, built-in providers, custom providers, and local LLM support.
Overview
clawft uses a pluggable provider system for LLM access. All providers use the OpenAI-compatible chat completions API format (POST /v1/chat/completions). The provider layer lives in the clawft-llm crate and has no dependencies on other clawft crates.
Key design principles:
- Single protocol -- every provider speaks the same OpenAI-compatible request/response format
- Prefix-based routing -- model identifiers like
openai/gpt-4oencode both the target provider and the model name - Environment-driven keys -- API keys are resolved from environment variables at request time, never persisted in configuration files
- Zero boilerplate -- nine providers are pre-configured and ready to use
Provider Routing
When clawft receives a model identifier (e.g., anthropic/claude-sonnet-4-20250514), the ProviderRouter performs these steps:
- Scan registered prefixes using longest-prefix-first matching
- Match the prefix (e.g.,
"anthropic/") to a provider - Strip the prefix to get the model name (e.g.,
"claude-sonnet-4-20250514") - Call
provider.complete(ChatRequest { model: "claude-sonnet-4-20250514", ... })
If no prefix matches, the router falls back to the default provider (OpenAI) and passes the model string unchanged.
Built-in Providers
ProviderRouter::with_builtins() registers nine providers. Each requires only its corresponding environment variable.
| Provider | Base URL | API Key Env Var | Default Model |
|---|---|---|---|
openai | https://api.openai.com/v1 | OPENAI_API_KEY | gpt-4o |
anthropic | https://api.anthropic.com/v1 | ANTHROPIC_API_KEY | claude-sonnet-4-5-20250514 |
groq | https://api.groq.com/openai/v1 | GROQ_API_KEY | llama-3.1-70b-versatile |
deepseek | https://api.deepseek.com/v1 | DEEPSEEK_API_KEY | deepseek-chat |
mistral | https://api.mistral.ai/v1 | MISTRAL_API_KEY | mistral-large-latest |
together | https://api.together.xyz/v1 | TOGETHER_API_KEY | (none) |
openrouter | https://openrouter.ai/api/v1 | OPENROUTER_API_KEY | (none) |
gemini | https://generativelanguage.googleapis.com/v1beta/openai | GOOGLE_GEMINI_API_KEY | gemini-2.5-flash |
xai | https://api.x.ai/v1 | XAI_API_KEY | grok-3-mini |
Anthropic requests automatically include the anthropic-version: 2023-06-01 header.
Model Identifier Format
Model identifiers use provider/model-name. Only the first slash separates the prefix from the model name.
| Identifier | Provider | Model Sent to API |
|---|---|---|
openai/gpt-4o | openai | gpt-4o |
anthropic/claude-sonnet-4-5-20250514 | anthropic | claude-sonnet-4-5-20250514 |
together/meta-llama/Meta-Llama-3-70B | together | meta-llama/Meta-Llama-3-70B |
openrouter/meta/llama-3-70b | openrouter | meta/llama-3-70b |
gpt-4o | (default: openai) | gpt-4o |
Minimal Configuration
Set an environment variable, then reference the model:
export ANTHROPIC_API_KEY="sk-ant-..."{
"agents": {
"defaults": {
"model": "anthropic/claude-sonnet-4-20250514"
}
}
}No provider-specific configuration is needed for built-in providers.
Provider Overrides
Override base URLs and add custom headers per provider:
{
"providers": {
"anthropic": {
"api_base": "https://my-proxy.example.com/v1",
"extra_headers": {
"X-Org-Id": "my-org-123"
}
}
}
}When api_base is set, it replaces the built-in base URL. When extra_headers are provided, they merge with existing provider headers.
Custom Providers
Via Configuration
{
"providers": {
"custom": {
"api_base": "http://localhost:11434/v1",
"api_key": "not-needed"
}
}
}Programmatic
use clawft_llm::{ProviderConfig, ProviderRouter};
use std::collections::HashMap;
let mut configs = clawft_llm::config::builtin_providers();
configs.push(ProviderConfig {
name: "my-service".into(),
base_url: "https://my-llm-service.example.com/v1".into(),
api_key_env: "MY_SERVICE_API_KEY".into(),
model_prefix: Some("my-service/".into()),
default_model: Some("my-model-v2".into()),
headers: HashMap::new(),
});
let router = ProviderRouter::from_configs(configs);Implementing the Provider Trait
For services that do not follow the OpenAI format:
#[async_trait]
impl Provider for MyCustomProvider {
fn name(&self) -> &str { "my-custom" }
async fn complete(&self, request: &ChatRequest) -> Result<ChatResponse> {
// Transform request, call API, transform response
todo!()
}
}Using Local LLMs
Any local server with an OpenAI-compatible /v1/chat/completions endpoint works.
Ollama
ollama pull llama3{
"providers": {
"openai": {
"api_base": "http://localhost:11434/v1"
}
},
"agents": {
"defaults": {
"model": "openai/llama3"
}
}
}export OPENAI_API_KEY="not-needed"llama.cpp Server
./llama-server -m model.gguf --port 8080Point api_base to http://localhost:8080/v1.
vLLM
python -m vllm.entrypoints.openai.api_server --model meta-llama/Llama-3-8BPoint api_base to http://localhost:8000/v1.
Automatic Retry
Every provider is wrapped in a RetryPolicy with exponential backoff:
| Parameter | Default | Description |
|---|---|---|
max_retries | 3 | Maximum retry attempts |
base_delay | 1 second | Initial delay |
max_delay | 30 seconds | Upper bound on delay |
jitter_fraction | 0.25 | Random jitter fraction |
Retryable errors: HTTP 429 (rate limited), 500-599 (server errors), timeouts, and network failures. Non-retryable: authentication failures, model not found, invalid responses, and permanent billing issues.
Error Handling
All provider operations return Result<T, ProviderError>:
| Error Variant | HTTP Status | Typical Cause |
|---|---|---|
AuthFailed | 401, 403 | Invalid or expired API key |
ModelNotFound | 404 | Model does not exist on provider |
RateLimited | 429 | Too many requests |
RequestFailed | Other 4xx/5xx | Various server errors |
NotConfigured | (none) | Missing API key env var |
Timeout | (none) | Request exceeded timeout |
API Key Management
Keys are resolved at request time:
- Explicit key (if
OpenAiCompatProvider::with_api_key()was used) - Environment variable named in
ProviderConfig.api_key_env
If neither provides a key, ProviderError::NotConfigured is returned before any network request.
Best practices:
- Never commit API keys to source control
- Use environment variables or a secrets manager
- Rotate keys regularly
- The
Debugimplementation redacts keys, displaying***
Troubleshooting
"provider not configured" -- The environment variable is not set. The error message names the specific variable.
"authentication failed" (401/403) -- Verify the key is correct and not expired.
"model not found" (404) -- Check for typos. Remember the prefix is stripped: openai/gpt-4o sends gpt-4o to the API.
Requests go to the wrong provider -- Verify your model string starts with a registered prefix. Use RUST_LOG=clawft_llm=debug to see routing details.
Local LLM parse errors -- Ensure the server returns the exact OpenAI chat completion JSON format with all required fields.