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