Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dallay/corvus/llms.txt

Use this file to discover all available pages before exploring further.

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

Build docs developers (and LLMs) love