Mithril Providers — How to Add & Understand Providers
Providers
5
Gemini, OpenAI, Anthropic, Groq, Local
Shared Interface
5 methods
ChatProvider trait
Pattern
Strategy
swap providers without touching orchestrator
What is a Provider?
A Provider is Mithril's adapter for a specific LLM API. It translates the unified
ChatProvider interface into the HTTP calls, headers, and payload format that each API expects.
The orchestrator never knows which provider it's using — it just calls provider.chat(messages)
and gets a response. This means you can swap Gemini for GPT-4 by changing one line in the fellowship YAML.
The ChatProvider Trait — 5 Methods Every Provider Implements
The Interface Contract
pub trait ChatProvider: Send + Sync {
/// Provider identifier (e.g. "gemini", "openai")
fn name(&self) -> &str;
/// Model being used (e.g. "gemini-2.5-flash", "gpt-4o")
fn model(&self) -> &str;
/// Simple chat: send messages, get full response back
async fn chat(&self, messages: &[ChatMessage]) -> Result<String>;
/// Streaming chat: tokens arrive one by one via a channel
async fn chat_stream(
&self, messages: &[ChatMessage], tx: Sender<String>
) -> Result<String>;
/// Chat with function calling: send tools + messages, get back
/// either a text response OR a list of tool calls to execute
async fn chat_with_tools(
&self, messages: &[ChatMessage], tools: &[ToolDefinition]
) -> Result<ChatResponse>;
/// Health check: is the API reachable and key valid?
async fn is_available(&self) -> bool;
}
What it does
Takes a conversation history (list of messages with roles: system, user, assistant) and returns
the model's complete response as a single string.
The flow (same for ALL providers)
Convert messages to the provider's native format
Build HTTP request with auth headers and JSON payload
POST to the provider's API endpoint
Parse response — extract the assistant's text from the provider-specific JSON
To add a new LLM provider (e.g. Mistral, Cohere, DeepSeek cloud), follow this template.
Create a new file src/providers/your_provider.rs:
New Provider Template (copy and fill in)
use anyhow::Result;
use async_trait::async_trait;
use crate::providers::{ChatMessage, ChatProvider, ChatResponse, ToolDefinition};
use tokio::sync::mpsc::Sender;
pub struct YourProvider {
api_key: String,
model: String,
client: reqwest::Client,
}
impl YourProvider {
pub fn new(api_key: String, model: &str) -> Self {
Self {
api_key,
model: model.to_string(),
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl ChatProvider for YourProvider {
fn name(&self) -> &str { "your_provider" }
fn model(&self) -> &str { &self.model }
async fn chat(&self, messages: &[ChatMessage]) -> Result<String> {
// 1. Convert messages to your API's format
let payload = build_payload(messages);
// 2. POST to your API
let response = self.client
.post("https://api.yourprovider.com/v1/chat")
.bearer_auth(&self.api_key)
.json(&payload)
.send().await?
.error_for_status()?
.json::<YourResponse>().await?;
// 3. Extract and return the assistant's text
Ok(response.choices[0].message.content.clone())
}
async fn chat_stream(
&self, messages: &[ChatMessage], tx: Sender<String>
) -> Result<String> {
// Same as chat() but with "stream": true
// Read SSE chunks, send each token via tx.send(token)
// Return the full accumulated text
todo!("Implement streaming")
}
async fn chat_with_tools(
&self, messages: &[ChatMessage], tools: &[ToolDefinition]
) -> Result<ChatResponse> {
// Same as chat() but include tool definitions in payload
// Parse response: if tool_calls present, return them
// Otherwise return text
todo!("Implement tool calling")
}
async fn is_available(&self) -> bool {
!self.api_key.is_empty()
}
}
Then register it in the factory (src/providers/mod.rs):
Factory Registration
// In create_provider_with_model():
"your_provider" => {
let key = config.get_credential("your_provider")?
.ok_or_else(|| anyhow!("API key not set"))?;
Ok(Box::new(YourProvider::new(key, model)))
}
That's it. The orchestrator, API layer, CLI, and TUI all work automatically
with your new provider. No other files need changes.
HTTP boilerplate: Every cloud provider repeats: build_payload → client.post() →
error_for_status() → parse. A shared helper could save ~40 lines per provider.
Struct fields: api_key + model + client are identical across 4 providers.
HttpProviderBase exists but isn't used yet.
Groq is 50% larger than others (436 vs ~280 LOC) without extra features.
Likely has verbose error handling that could be simplified.
These are tracked in the refactoring roadmap. The architecture is correct — the duplication
is cosmetic and doesn't affect functionality or extensibility.