Provider Trait
The Provider trait defines the interface for all AI model providers in Corvus. Every LLM backend (OpenAI, Anthropic, Ollama, etc.) implements this trait.
Source: src/providers/traits.rs:229-415
Trait Definition
use async_trait::async_trait;
use anyhow::Result;
#[async_trait]
pub trait Provider: Send + Sync {
/// Query provider capabilities
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities::default()
}
/// Convert tool specifications to provider-native format
fn convert_tools(&self, tools: &[ToolSpec]) -> ToolsPayload {
ToolsPayload::PromptGuided {
instructions: build_tool_instructions_text(tools),
}
}
/// Simple one-shot chat
async fn simple_chat(
&self,
message: &str,
model: &str,
temperature: f64,
) -> Result<String>;
/// Chat with optional system prompt
async fn chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
) -> Result<String>;
/// Multi-turn conversation
async fn chat_with_history(
&self,
messages: &[ChatMessage],
model: &str,
temperature: f64,
) -> Result<String>;
/// Structured chat API for agent loop
async fn chat(
&self,
request: ChatRequest<'_>,
model: &str,
temperature: f64,
) -> Result<ChatResponse>;
/// Whether provider supports native tool calls
fn supports_native_tools(&self) -> bool {
self.capabilities().native_tool_calling
}
/// Warm up HTTP connection pool
async fn warmup(&self) -> Result<()> {
Ok(())
}
/// Chat with native tool calling
async fn chat_with_tools(
&self,
messages: &[ChatMessage],
tools: &[serde_json::Value],
model: &str,
temperature: f64,
) -> Result<ChatResponse>;
/// Whether provider supports streaming
fn supports_streaming(&self) -> bool {
false
}
/// Streaming chat
fn stream_chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
options: StreamOptions,
) -> stream::BoxStream<'static, StreamResult<StreamChunk>>;
/// Streaming chat with history
fn stream_chat_with_history(
&self,
messages: &[ChatMessage],
model: &str,
temperature: f64,
options: StreamOptions,
) -> stream::BoxStream<'static, StreamResult<StreamChunk>>;
}
Core Types
ChatMessage
pub struct ChatMessage {
pub role: String, // "system", "user", "assistant", "tool"
pub content: String,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self;
pub fn user(content: impl Into<String>) -> Self;
pub fn assistant(content: impl Into<String>) -> Self;
pub fn tool(content: impl Into<String>) -> Self;
}
ChatResponse
pub struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
}
impl ChatResponse {
pub fn has_tool_calls(&self) -> bool;
pub fn text_or_empty(&self) -> &str;
}
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String, // JSON string
}
ProviderCapabilities
pub struct ProviderCapabilities {
pub native_tool_calling: bool,
}
impl Default for ProviderCapabilities {
fn default() -> Self {
Self { native_tool_calling: false }
}
}
Implementing a Provider
Minimal example from examples/custom_provider.rs:19-59:
use async_trait::async_trait;
use anyhow::Result;
pub struct OllamaProvider {
base_url: String,
client: reqwest::Client,
}
impl OllamaProvider {
pub fn new(base_url: Option<&str>) -> Self {
Self {
base_url: base_url.unwrap_or("http://localhost:11434").to_string(),
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl Provider for OllamaProvider {
async fn chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
) -> Result<String> {
let url = format!("{}/api/generate", self.base_url);
let prompt = match system_prompt {
Some(sys) => format!("{sys}\n\n{message}"),
None => message.to_string(),
};
let body = serde_json::json!({
"model": model,
"prompt": prompt,
"temperature": temperature,
"stream": false,
});
let resp = self.client
.post(&url)
.json(&body)
.send()
.await?
.json::<serde_json::Value>()
.await?;
resp["response"]
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("No response field"))
}
}
Providers that support native tool calling (OpenAI, Anthropic, Gemini) return ToolCall objects:
impl Provider for AnthropicProvider {
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
native_tool_calling: true,
}
}
async fn chat_with_tools(
&self,
messages: &[ChatMessage],
tools: &[serde_json::Value],
model: &str,
temperature: f64,
) -> Result<ChatResponse> {
// Convert tools to Anthropic format
// Make API call
// Parse tool_use blocks
// Return ChatResponse with tool_calls
}
}
Providers without native support use XML tags in text:
impl Provider for OllamaProvider {
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
native_tool_calling: false,
}
}
// Default implementation injects tool instructions into system prompt
}
The LLM returns:
<tool_call>
{"name": "shell", "arguments": {"command": "ls -la"}}
</tool_call>
Streaming Support
impl Provider for OpenAIProvider {
fn supports_streaming(&self) -> bool {
true
}
fn stream_chat_with_system(
&self,
system_prompt: Option<&str>,
message: &str,
model: &str,
temperature: f64,
options: StreamOptions,
) -> stream::BoxStream<'static, StreamResult<StreamChunk>> {
// Return async stream of chunks
let stream = async_stream::stream! {
// Yield StreamChunk::delta("text")
// Yield StreamChunk::final_chunk()
};
Box::pin(stream)
}
}
Registration
Register your provider in src/providers/mod.rs:
pub fn create_provider(name: &str, config: &Config) -> Result<Box<dyn Provider>> {
match name {
"openai" => Ok(Box::new(openai::OpenAIProvider::new(config))),
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(config))),
"ollama" => Ok(Box::new(ollama::OllamaProvider::new(None))),
_ => Err(anyhow::anyhow!("Unknown provider: {}", name)),
}
}
Built-in Providers
Corvus ships with 22+ providers:
- OpenAI, OpenAI Codex, OpenRouter
- Anthropic, Anthropic Custom
- Google Gemini
- Ollama (local)
- Groq, Mistral, DeepSeek, Together, Fireworks
- Perplexity, Cohere
- AWS Bedrock
- Azure OpenAI
- xAI Grok
- Venice, GLM
- Custom (any OpenAI-compatible endpoint)
See Providers Guide for full list.
Best Practices
Implement warmup() to pre-establish HTTP/2 connections and DNS resolution
Use connection pooling (reqwest::Client::new()) for HTTP clients
Never log API keys or sensitive request/response data