Adding a Provider
Providers connect ClawDesk to AI model backends (Anthropic, OpenAI, Ollama, etc.). This guide walks through adding a new provider integration.
Provider Architecture
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Agent │────▶│ Provider │────▶│ External │
│ Pipeline │ │ Registry │ │ API │
└──────────────┘ └──────────────────┘ └──────────────┘
│
┌─────┴──────┐
│ Negotiator │ ← Capability matching
└────────────┘
│ Fallback │ ← Automatic retry
└────────────┘
Step 1: Create the Provider Module
Add a new module in crates/clawdesk-providers/src/:
crates/clawdesk-providers/src/
├── lib.rs
├── anthropic.rs
├── openai.rs
├── ollama.rs
├── gemini.rs
├── bedrock.rs
└── mistral.rs ← New file
Step 2: Define Configuration
// crates/clawdesk-providers/src/mistral.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MistralConfig {
pub api_key: String,
pub base_url: Option<String>,
pub default_model: String,
pub max_retries: u32,
pub timeout_secs: u64,
}
impl Default for MistralConfig {
fn default() -> Self {
Self {
api_key: String::new(),
base_url: Some("https://api.mistral.ai/v1".into()),
default_model: "mistral-large-latest".into(),
max_retries: 3,
timeout_secs: 30,
}
}
}
Step 3: Implement the Provider Trait
use async_trait::async_trait;
use clawdesk_types::{Message, ModelId, ProviderError};
pub struct MistralProvider {
config: MistralConfig,
client: reqwest::Client,
}
#[async_trait]
impl Provider for MistralProvider {
fn name(&self) -> &str {
"mistral"
}
fn models(&self) -> Vec<ModelId> {
vec![
ModelId::from("mistral-large-latest"),
ModelId::from("mistral-medium-latest"),
ModelId::from("mistral-small-latest"),
ModelId::from("open-mixtral-8x22b"),
]
}
async fn chat(
&self,
model: &ModelId,
messages: &[Message],
options: &ChatOptions,
) -> Result<Message, ProviderError> {
let request = self.build_request(model, messages, options);
let response = self
.client
.post(format!("{}/chat/completions", self.base_url()))
.bearer_auth(&self.config.api_key)
.json(&request)
.send()
.await
.map_err(|e| ProviderError::ConnectionFailed(e.to_string()))?;
self.parse_response(response).await
}
async fn stream(
&self,
model: &ModelId,
messages: &[Message],
options: &ChatOptions,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, ProviderError>>>>, ProviderError> {
// SSE streaming implementation
todo!()
}
}
Step 4: Map Errors
Map provider-specific errors to ProviderError:
impl MistralProvider {
fn map_api_error(&self, status: StatusCode, body: &str) -> ProviderError {
match status {
StatusCode::UNAUTHORIZED => ProviderError::AuthFailed,
StatusCode::TOO_MANY_REQUESTS => {
let retry_after = self.parse_retry_after(body);
ProviderError::RateLimited { retry_after }
}
StatusCode::BAD_REQUEST => ProviderError::InvalidRequest(body.to_string()),
_ => ProviderError::ApiError {
status: status.as_u16(),
message: body.to_string(),
},
}
}
}
The fallback system uses error types to decide whether to retry with a different provider. Return ProviderError::RateLimited for 429s to enable automatic fallback.
Step 5: Declare Capabilities
Register the provider's capabilities with the negotiator:
use clawdesk_providers::capability::{Capability, CapabilitySet};
impl MistralProvider {
pub fn capabilities(&self, model: &ModelId) -> CapabilitySet {
let mut caps = CapabilitySet::new();
caps.insert(Capability::Chat);
caps.insert(Capability::Streaming);
if model.as_str().contains("large") {
caps.insert(Capability::FunctionCalling);
caps.insert(Capability::JsonMode);
}
caps
}
}
Step 6: Register the Provider
Add to the provider registry in crates/clawdesk-providers/src/registry.rs:
pub fn register_providers(
registry: &mut ProviderRegistry,
config: &AppConfig,
) -> Result<(), ProviderError> {
// ... existing providers ...
if let Some(mistral_cfg) = &config.providers.mistral {
let provider = MistralProvider::new(mistral_cfg.clone())?;
registry.register(Box::new(provider));
}
Ok(())
}
Step 7: Integrate with Fallback
The fallback system in clawdesk-domain automatically picks up registered providers. Configure the fallback chain in config.toml:
[fallback]
chain = ["anthropic", "mistral", "ollama"]
strategy = "sequential" # or "capability-match"
max_retries_per_provider = 2
Step 8: Write Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mistral_default_config_is_valid() {
let config = MistralConfig::default();
assert_eq!(config.default_model, "mistral-large-latest");
assert!(config.base_url.is_some());
}
#[test]
fn mistral_lists_models() {
let provider = MistralProvider::new(MistralConfig::default()).unwrap();
let models = provider.models();
assert!(models.len() >= 3);
}
#[test]
fn error_mapping_rate_limit() {
let provider = MistralProvider::new(MistralConfig::default()).unwrap();
let err = provider.map_api_error(StatusCode::TOO_MANY_REQUESTS, "{}");
assert!(matches!(err, ProviderError::RateLimited { .. }));
}
#[tokio::test]
async fn mistral_chat_handles_auth_failure() {
let config = MistralConfig {
api_key: "invalid".into(),
..Default::default()
};
let provider = MistralProvider::new(config).unwrap();
let result = provider
.chat(
&ModelId::from("mistral-small-latest"),
&[Message::user("test")],
&ChatOptions::default(),
)
.await;
assert!(result.is_err());
}
}
Checklist
- Implement
Providertrait (name,models,chat,stream) - Define
{Provider}Configwith serde - Map all API errors to
ProviderErrorvariants - Declare
CapabilitySetper model - Register in
ProviderRegistry - Configure fallback chain entry
- Write unit tests for config, models, and error mapping
- Write integration tests for chat round-trip
- Add environment variable for API key
- Update provider docs
See the Anthropic and OpenAI implementations in crates/clawdesk-providers/src/ for complete reference examples.