Skip to main content

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;
}

ToolCall

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"))
    }
}

Tool Calling Modes

Native Tool Calling

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
    }
}

Prompt-Guided Tool Calling

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