Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Model Providers Specification

Overview

Model providers enable SmartAssist to interact with various LLM APIs including Anthropic, OpenAI, Google, and others.

Provider Trait

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use futures::Stream;
use std::pin::Pin;

/// Model provider trait.
#[async_trait]
pub trait ModelProvider: Send + Sync {
    /// Provider identifier.
    fn id(&self) -> &str;

    /// Provider display name.
    fn name(&self) -> &str;

    /// List available models.
    fn models(&self) -> &[ModelDefinition];

    /// Check if provider is configured.
    fn is_configured(&self) -> bool;

    /// Send a chat completion request.
    async fn chat(
        &self,
        request: ChatRequest,
    ) -> Result<ChatResponse, ProviderError>;

    /// Send a streaming chat request.
    async fn chat_stream(
        &self,
        request: ChatRequest,
    ) -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError>;
}

/// Model definition.
#[derive(Debug, Clone)]
pub struct ModelDefinition {
    /// Model identifier.
    pub id: String,

    /// Display name.
    pub name: String,

    /// API type.
    pub api: ModelApi,

    /// Whether model supports reasoning/thinking.
    pub reasoning: bool,

    /// Supported input types.
    pub input_types: Vec<InputType>,

    /// Cost per 1M tokens.
    pub cost: ModelCost,

    /// Context window size.
    pub context_window: usize,

    /// Maximum output tokens.
    pub max_tokens: usize,

    /// Custom headers for this model.
    pub headers: HashMap<String, String>,

    /// Compatibility settings.
    pub compat: ModelCompat,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelApi {
    AnthropicMessages,
    OpenAiCompletions,
    OpenAiResponses,
    GoogleGenerativeAi,
    GithubCopilot,
    BedrockConverseStream,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputType {
    Text,
    Image,
}

#[derive(Debug, Clone, Copy)]
pub struct ModelCost {
    /// Input cost per 1M tokens.
    pub input: f64,

    /// Output cost per 1M tokens.
    pub output: f64,

    /// Cache read cost per 1M tokens.
    pub cache_read: f64,

    /// Cache write cost per 1M tokens.
    pub cache_write: f64,
}

#[derive(Debug, Clone, Default)]
pub struct ModelCompat {
    /// Supports prompt caching.
    pub supports_store: bool,

    /// Supports developer/system role.
    pub supports_developer_role: bool,

    /// Supports reasoning effort control.
    pub supports_reasoning_effort: bool,

    /// Field name for max tokens.
    pub max_tokens_field: MaxTokensField,
}

#[derive(Debug, Clone, Copy, Default)]
pub enum MaxTokensField {
    #[default]
    MaxTokens,
    MaxCompletionTokens,
}
}

Chat Request/Response

#![allow(unused)]
fn main() {
/// Chat completion request.
#[derive(Debug, Clone)]
pub struct ChatRequest {
    /// Model identifier.
    pub model: String,

    /// Conversation messages.
    pub messages: Vec<ChatMessage>,

    /// System prompt.
    pub system: Option<String>,

    /// Maximum tokens to generate.
    pub max_tokens: Option<usize>,

    /// Temperature (0.0 - 1.0).
    pub temperature: Option<f32>,

    /// Top-p sampling.
    pub top_p: Option<f32>,

    /// Stop sequences.
    pub stop: Option<Vec<String>>,

    /// Available tools.
    pub tools: Option<Vec<ToolDefinition>>,

    /// Tool choice behavior.
    pub tool_choice: Option<ToolChoice>,

    /// Thinking/reasoning configuration.
    pub thinking: Option<ThinkingConfig>,

    /// Request timeout.
    pub timeout_ms: Option<u64>,
}

/// Chat message.
#[derive(Debug, Clone)]
pub struct ChatMessage {
    pub role: ChatRole,
    pub content: ChatContent,
    pub name: Option<String>,
    pub tool_call_id: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatRole {
    System,
    User,
    Assistant,
    Tool,
}

#[derive(Debug, Clone)]
pub enum ChatContent {
    Text(String),
    Blocks(Vec<ContentBlock>),
}

#[derive(Debug, Clone)]
pub enum ContentBlock {
    Text { text: String },
    Image { source: ImageSource },
    ToolUse { id: String, name: String, input: serde_json::Value },
    ToolResult { tool_use_id: String, content: String, is_error: bool },
    Thinking { thinking: String },
}

#[derive(Debug, Clone)]
pub enum ImageSource {
    Base64 { media_type: String, data: String },
    Url { url: String },
}

/// Tool definition for LLM.
#[derive(Debug, Clone)]
pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    pub input_schema: serde_json::Value,
}

#[derive(Debug, Clone)]
pub enum ToolChoice {
    Auto,
    None,
    Required,
    Tool { name: String },
}

/// Thinking/reasoning configuration.
#[derive(Debug, Clone)]
pub struct ThinkingConfig {
    pub enabled: bool,
    pub effort: ThinkingEffort,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThinkingEffort {
    Off,
    Minimal,
    Low,
    Medium,
    High,
    ExtraHigh,
}

/// Chat completion response.
#[derive(Debug, Clone)]
pub struct ChatResponse {
    /// Response ID.
    pub id: String,

    /// Model used.
    pub model: String,

    /// Generated content.
    pub content: Vec<ContentBlock>,

    /// Stop reason.
    pub stop_reason: StopReason,

    /// Token usage.
    pub usage: TokenUsage,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopReason {
    EndTurn,
    MaxTokens,
    StopSequence,
    ToolUse,
}

/// Token usage statistics.
#[derive(Debug, Clone, Copy, Default)]
pub struct TokenUsage {
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub cache_read_tokens: u64,
    pub cache_write_tokens: u64,
}

/// Streaming chat chunk.
#[derive(Debug, Clone)]
pub struct ChatChunk {
    pub delta: ChunkDelta,
    pub usage: Option<TokenUsage>,
}

#[derive(Debug, Clone)]
pub enum ChunkDelta {
    Text { text: String },
    ToolUseStart { id: String, name: String },
    ToolUseInput { input: String },
    Thinking { thinking: String },
    Stop { reason: StopReason },
}
}

Provider Configuration

#![allow(unused)]
fn main() {
/// Provider configuration.
#[derive(Debug, Clone)]
pub struct ProviderConfig {
    /// Base URL for API.
    pub base_url: String,

    /// API key.
    pub api_key: Option<SecretString>,

    /// Authentication mode.
    pub auth: AuthMode,

    /// API type.
    pub api: ModelApi,

    /// Custom headers.
    pub headers: HashMap<String, String>,

    /// Whether to include auth header.
    pub auth_header: bool,

    /// Available models.
    pub models: Vec<ModelDefinition>,
}

#[derive(Debug, Clone, Copy, Default)]
pub enum AuthMode {
    #[default]
    ApiKey,
    AwsSdk,
    OAuth,
    Token,
}
}

Built-in Providers

Anthropic Provider

#![allow(unused)]
fn main() {
pub struct AnthropicProvider {
    config: ProviderConfig,
    client: reqwest::Client,
}

impl AnthropicProvider {
    pub fn new(api_key: SecretString) -> Self {
        Self {
            config: ProviderConfig {
                base_url: "https://api.anthropic.com".to_string(),
                api_key: Some(api_key),
                auth: AuthMode::ApiKey,
                api: ModelApi::AnthropicMessages,
                headers: HashMap::from([
                    ("anthropic-version".to_string(), "2023-06-01".to_string()),
                ]),
                auth_header: true,
                models: vec![
                    ModelDefinition {
                        id: "claude-sonnet-4-20250514".to_string(),
                        name: "Claude Sonnet 4".to_string(),
                        api: ModelApi::AnthropicMessages,
                        reasoning: true,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 3.0,
                            output: 15.0,
                            cache_read: 0.3,
                            cache_write: 3.75,
                        },
                        context_window: 200000,
                        max_tokens: 16000,
                        headers: HashMap::new(),
                        compat: ModelCompat {
                            supports_store: true,
                            supports_developer_role: true,
                            supports_reasoning_effort: true,
                            ..Default::default()
                        },
                    },
                    ModelDefinition {
                        id: "claude-opus-4-20250514".to_string(),
                        name: "Claude Opus 4".to_string(),
                        api: ModelApi::AnthropicMessages,
                        reasoning: true,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 15.0,
                            output: 75.0,
                            cache_read: 1.5,
                            cache_write: 18.75,
                        },
                        context_window: 200000,
                        max_tokens: 32000,
                        headers: HashMap::new(),
                        compat: ModelCompat {
                            supports_store: true,
                            supports_developer_role: true,
                            supports_reasoning_effort: true,
                            ..Default::default()
                        },
                    },
                    ModelDefinition {
                        id: "claude-3-5-haiku-20241022".to_string(),
                        name: "Claude 3.5 Haiku".to_string(),
                        api: ModelApi::AnthropicMessages,
                        reasoning: true,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 1.0,
                            output: 5.0,
                            cache_read: 0.1,
                            cache_write: 1.25,
                        },
                        context_window: 200000,
                        max_tokens: 8192,
                        headers: HashMap::new(),
                        compat: ModelCompat {
                            supports_store: true,
                            supports_developer_role: true,
                            supports_reasoning_effort: true,
                            ..Default::default()
                        },
                    },
                ],
            },
            client: reqwest::Client::new(),
        }
    }
}

#[async_trait]
impl ModelProvider for AnthropicProvider {
    fn id(&self) -> &str { "anthropic" }
    fn name(&self) -> &str { "Anthropic" }
    fn models(&self) -> &[ModelDefinition] { &self.config.models }
    fn is_configured(&self) -> bool { self.config.api_key.is_some() }

    async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
        let api_key = self.config.api_key.as_ref()
            .ok_or(ProviderError::NotConfigured)?;

        let body = self.build_request_body(&request)?;

        let response = self.client
            .post(format!("{}/v1/messages", self.config.base_url))
            .header("x-api-key", api_key.expose_secret())
            .header("anthropic-version", "2023-06-01")
            .json(&body)
            .send()
            .await?;

        if !response.status().is_success() {
            let error: serde_json::Value = response.json().await?;
            return Err(ProviderError::Api {
                code: error["error"]["type"].as_str().unwrap_or("unknown").to_string(),
                message: error["error"]["message"].as_str().unwrap_or("Unknown error").to_string(),
            });
        }

        let result: serde_json::Value = response.json().await?;
        self.parse_response(result)
    }

    async fn chat_stream(
        &self,
        request: ChatRequest,
    ) -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError> {
        // Streaming implementation parses SSE events from the provider API
    }
}
}

OpenAI Provider

#![allow(unused)]
fn main() {
pub struct OpenAiProvider {
    config: ProviderConfig,
    client: reqwest::Client,
}

impl OpenAiProvider {
    pub fn new(api_key: SecretString) -> Self {
        Self {
            config: ProviderConfig {
                base_url: "https://api.openai.com/v1".to_string(),
                api_key: Some(api_key),
                auth: AuthMode::ApiKey,
                api: ModelApi::OpenAiCompletions,
                headers: HashMap::new(),
                auth_header: true,
                models: vec![
                    ModelDefinition {
                        id: "gpt-4o-mini".to_string(),
                        name: "GPT-4o Mini".to_string(),
                        api: ModelApi::OpenAiCompletions,
                        reasoning: false,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 0.15,
                            output: 0.6,
                            cache_read: 0.0,
                            cache_write: 0.0,
                        },
                        context_window: 128000,
                        max_tokens: 16384,
                        headers: HashMap::new(),
                        compat: ModelCompat {
                            max_tokens_field: MaxTokensField::MaxCompletionTokens,
                            ..Default::default()
                        },
                    },
                    ModelDefinition {
                        id: "gpt-4o".to_string(),
                        name: "GPT-4o".to_string(),
                        api: ModelApi::OpenAiCompletions,
                        reasoning: false,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 2.5,
                            output: 10.0,
                            cache_read: 0.0,
                            cache_write: 0.0,
                        },
                        context_window: 128000,
                        max_tokens: 16384,
                        headers: HashMap::new(),
                        compat: ModelCompat {
                            max_tokens_field: MaxTokensField::MaxCompletionTokens,
                            ..Default::default()
                        },
                    },
                ],
            },
            client: reqwest::Client::new(),
        }
    }
}

#[async_trait]
impl ModelProvider for OpenAiProvider {
    fn id(&self) -> &str { "openai" }
    fn name(&self) -> &str { "OpenAI" }
    fn models(&self) -> &[ModelDefinition] { &self.config.models }
    fn is_configured(&self) -> bool { self.config.api_key.is_some() }

    async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
        let api_key = self.config.api_key.as_ref()
            .ok_or(ProviderError::NotConfigured)?;

        let body = self.build_request_body(&request)?;

        let response = self.client
            .post(format!("{}/chat/completions", self.config.base_url))
            .bearer_auth(api_key.expose_secret())
            .json(&body)
            .send()
            .await?;

        if !response.status().is_success() {
            let error: serde_json::Value = response.json().await?;
            return Err(ProviderError::Api {
                code: error["error"]["type"].as_str().unwrap_or("unknown").to_string(),
                message: error["error"]["message"].as_str().unwrap_or("Unknown error").to_string(),
            });
        }

        let result: serde_json::Value = response.json().await?;
        self.parse_response(result)
    }

    async fn chat_stream(
        &self,
        request: ChatRequest,
    ) -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError> {
        // Streaming implementation parses SSE events from the provider API
    }
}
}

Google Provider

#![allow(unused)]
fn main() {
pub struct GoogleProvider {
    config: ProviderConfig,
    client: reqwest::Client,
}

impl GoogleProvider {
    pub fn new(api_key: SecretString) -> Self {
        Self {
            config: ProviderConfig {
                base_url: "https://generativelanguage.googleapis.com/v1beta".to_string(),
                api_key: Some(api_key),
                auth: AuthMode::ApiKey,
                api: ModelApi::GoogleGenerativeAi,
                headers: HashMap::new(),
                auth_header: false,
                models: vec![
                    ModelDefinition {
                        id: "gemini-2.0-flash".to_string(),
                        name: "Gemini 2.0 Flash".to_string(),
                        api: ModelApi::GoogleGenerativeAi,
                        reasoning: false,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 0.1,
                            output: 0.4,
                            cache_read: 0.0,
                            cache_write: 0.0,
                        },
                        context_window: 1000000,
                        max_tokens: 8192,
                        headers: HashMap::new(),
                        compat: Default::default(),
                    },
                    ModelDefinition {
                        id: "gemini-1.5-pro".to_string(),
                        name: "Gemini 1.5 Pro".to_string(),
                        api: ModelApi::GoogleGenerativeAi,
                        reasoning: false,
                        input_types: vec![InputType::Text, InputType::Image],
                        cost: ModelCost {
                            input: 1.25,
                            output: 5.0,
                            cache_read: 0.0,
                            cache_write: 0.0,
                        },
                        context_window: 2000000,
                        max_tokens: 8192,
                        headers: HashMap::new(),
                        compat: Default::default(),
                    },
                ],
            },
            client: reqwest::Client::new(),
        }
    }
}
}

Provider Registry

#![allow(unused)]
fn main() {
/// Provider registry.
pub struct ProviderRegistry {
    providers: HashMap<String, Arc<dyn ModelProvider>>,
}

impl ProviderRegistry {
    pub fn new() -> Self {
        Self {
            providers: HashMap::new(),
        }
    }

    pub fn register(&mut self, provider: Arc<dyn ModelProvider>) {
        self.providers.insert(provider.id().to_string(), provider);
    }

    pub fn get(&self, id: &str) -> Option<Arc<dyn ModelProvider>> {
        self.providers.get(id).cloned()
    }

    pub fn get_for_model(&self, model_id: &str) -> Option<Arc<dyn ModelProvider>> {
        // Parse provider/model format
        if let Some((provider_id, _)) = model_id.split_once('/') {
            return self.get(provider_id);
        }

        // Search all providers for model
        for provider in self.providers.values() {
            if provider.models().iter().any(|m| m.id == model_id) {
                return Some(provider.clone());
            }
        }

        None
    }

    pub fn list_models(&self) -> Vec<&ModelDefinition> {
        self.providers.values()
            .flat_map(|p| p.models().iter())
            .collect()
    }

    pub fn list_configured(&self) -> Vec<&str> {
        self.providers.values()
            .filter(|p| p.is_configured())
            .map(|p| p.id())
            .collect()
    }
}

/// Create default provider registry.
pub fn create_default_registry(config: &SmartAssistConfig) -> ProviderRegistry {
    let mut registry = ProviderRegistry::new();

    // Anthropic
    if let Some(key) = config.providers.anthropic.api_key.as_ref() {
        registry.register(Arc::new(AnthropicProvider::new(key.clone())));
    }

    // OpenAI
    if let Some(key) = config.providers.openai.api_key.as_ref() {
        registry.register(Arc::new(OpenAiProvider::new(key.clone())));
    }

    // Google
    if let Some(key) = config.providers.google.api_key.as_ref() {
        registry.register(Arc::new(GoogleProvider::new(key.clone())));
    }

    // Add more providers...

    registry
}
}

Error Handling

#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum ProviderError {
    #[error("Provider not configured")]
    NotConfigured,

    #[error("Model not found: {0}")]
    ModelNotFound(String),

    #[error("API error: {code} - {message}")]
    Api { code: String, message: String },

    #[error("Rate limited, retry after {retry_after_ms}ms")]
    RateLimited { retry_after_ms: u64 },

    #[error("Auth error: {0}")]
    Auth(String),

    #[error("Timeout")]
    Timeout,

    #[error("Network error: {0}")]
    Network(#[from] reqwest::Error),

    #[error("Parse error: {0}")]
    Parse(#[from] serde_json::Error),
}
}

Auth Profile Management

#![allow(unused)]
fn main() {
/// Auth profile store.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthProfileStore {
    pub version: u32,
    pub profiles: HashMap<String, AuthProfileCredential>,
    pub order: Option<HashMap<String, Vec<String>>>,
    pub last_good: Option<HashMap<String, String>>,
    pub usage_stats: Option<HashMap<String, ProfileUsageStats>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AuthProfileCredential {
    #[serde(rename = "api_key")]
    ApiKey {
        provider: String,
        key: String,
        email: Option<String>,
    },
    #[serde(rename = "token")]
    Token {
        provider: String,
        token: String,
        expires: Option<u64>,
        email: Option<String>,
    },
    #[serde(rename = "oauth")]
    OAuth {
        provider: String,
        access_token: Option<String>,
        refresh_token: Option<String>,
        expires_at: Option<u64>,
        client_id: Option<String>,
        email: Option<String>,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileUsageStats {
    pub last_used: Option<u64>,
    pub cooldown_until: Option<u64>,
    pub disabled_until: Option<u64>,
    pub disabled_reason: Option<FailureReason>,
    pub error_count: Option<u32>,
    pub failure_counts: Option<HashMap<FailureReason, u32>>,
    pub last_failure_at: Option<u64>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FailureReason {
    Auth,
    Format,
    RateLimit,
    Billing,
    Timeout,
    Unknown,
}
}