Skip to main content

mixtape_core/provider/
mod.rs

1//! Model providers for LLM interactions
2//!
3//! This module contains the `ModelProvider` trait and implementations for
4//! different LLM backends (Bedrock, Anthropic, etc.)
5
6#[cfg(feature = "anthropic")]
7pub mod anthropic;
8#[cfg(feature = "bedrock")]
9pub mod bedrock;
10pub mod retry;
11
12use crate::events::TokenUsage;
13use crate::types::{Message, StopReason, ToolDefinition, ToolUseBlock};
14use futures::stream::BoxStream;
15use std::error::Error;
16
17// Re-export provider types at provider level
18#[cfg(feature = "anthropic")]
19pub use anthropic::AnthropicProvider;
20#[cfg(feature = "bedrock")]
21pub use bedrock::{
22    BedrockChatCompletionsProvider, BedrockProvider, BedrockResponsesProvider, InferenceProfile,
23};
24pub use retry::{RetryCallback, RetryConfig, RetryInfo};
25
26// Re-export ModelResponse from model module
27pub use crate::model::ModelResponse;
28
29/// Events from streaming model responses
30#[derive(Debug, Clone)]
31pub enum StreamEvent {
32    /// Incremental text delta
33    TextDelta(String),
34    /// Tool use detected
35    ToolUse(ToolUseBlock),
36    /// Incremental thinking delta (extended thinking)
37    ThinkingDelta(String),
38    /// A complete content block for conversation replay, in provider order.
39    /// Providers emitting this event must emit every block, including text and
40    /// tools. TextDelta/ToolUse remain available for existing streaming consumers.
41    ContentBlock(crate::types::ContentBlock),
42    /// Streaming stopped
43    Stop {
44        /// Why the model stopped
45        stop_reason: StopReason,
46        /// Token usage for this response (if available)
47        usage: Option<TokenUsage>,
48    },
49}
50
51/// Error types for model providers
52#[derive(Debug, thiserror::Error)]
53pub enum ProviderError {
54    /// Authentication or authorization failed (expired tokens, invalid credentials, etc.)
55    #[error("Authentication failed: {0}")]
56    Authentication(String),
57
58    /// Rate limiting or throttling
59    #[error("Rate limited: {0}")]
60    RateLimited(String),
61
62    /// Network or connectivity issues
63    #[error("Network error: {0}")]
64    Network(String),
65
66    /// Model-specific errors (content filtered, context too long, etc.)
67    #[error("Model error: {0}")]
68    Model(String),
69
70    /// Service unavailable or temporary issues
71    #[error("Service unavailable: {0}")]
72    ServiceUnavailable(String),
73
74    /// Invalid configuration (bad model ID, missing parameters, etc.)
75    #[error("Invalid configuration: {0}")]
76    Configuration(String),
77
78    /// Other provider-specific errors that don't fit above categories
79    #[error("{0}")]
80    Other(String),
81
82    /// Communication error (legacy, kept for compatibility)
83    #[error("Communication error: {0}")]
84    Communication(#[from] Box<dyn Error + Send + Sync>),
85}
86
87/// Trait for model providers
88///
89/// This trait abstracts over different LLM providers (Bedrock, Anthropic, etc.)
90/// allowing the Agent to work with any provider implementation.
91///
92/// A provider combines model metadata (name, token limits) with API interaction
93/// (generate, streaming). Use the builder to create agents:
94///
95/// ```ignore
96/// let agent = Agent::builder()
97///     .bedrock(ClaudeSonnet4_5)
98///     .build()
99///     .await?;
100/// ```
101#[async_trait::async_trait]
102pub trait ModelProvider: Send + Sync {
103    /// Get the model name for display (e.g., "Claude Sonnet 4.5")
104    fn name(&self) -> &str;
105
106    /// Maximum input context tokens for this model
107    fn max_context_tokens(&self) -> usize;
108
109    /// Maximum output tokens this model can generate
110    fn max_output_tokens(&self) -> usize;
111
112    /// Estimate token count for text
113    ///
114    /// Providers should implement this to match their model's tokenization.
115    /// Default implementation uses ~4 characters per token heuristic.
116    fn estimate_token_count(&self, text: &str) -> usize {
117        text.len().div_ceil(4)
118    }
119
120    /// Estimate token count for a conversation
121    fn estimate_message_tokens(&self, messages: &[Message]) -> usize {
122        let mut total = 0;
123        for message in messages {
124            total += 4; // Role overhead
125            for block in &message.content {
126                total += self.estimate_token_count(&format!("{:?}", block));
127            }
128        }
129        total
130    }
131
132    /// Send a request to the model and get a response
133    ///
134    /// # Arguments
135    /// * `messages` - The conversation history
136    /// * `tools` - Available tools for the model to use
137    /// * `system_prompt` - Optional system prompt
138    async fn generate(
139        &self,
140        messages: Vec<Message>,
141        tools: Vec<ToolDefinition>,
142        system_prompt: Option<String>,
143    ) -> Result<ModelResponse, ProviderError>;
144
145    /// Send a request and stream the response token-by-token (optional)
146    ///
147    /// # Arguments
148    /// * `messages` - The conversation history
149    /// * `tools` - Available tools for the model to use
150    /// * `system_prompt` - Optional system prompt
151    async fn generate_stream(
152        &self,
153        messages: Vec<Message>,
154        tools: Vec<ToolDefinition>,
155        system_prompt: Option<String>,
156    ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
157        // Default implementation: call generate and return complete response
158        let response = self.generate(messages, tools, system_prompt).await?;
159
160        // Display deltas are separate from the complete, ordered replay content.
161        let mut events = Vec::new();
162        for block in response.message.content {
163            match &block {
164                crate::types::ContentBlock::Text(text) => {
165                    events.push(Ok(StreamEvent::TextDelta(text.clone())));
166                }
167                crate::types::ContentBlock::ToolUse(tool_use) => {
168                    events.push(Ok(StreamEvent::ToolUse(tool_use.clone())));
169                }
170                _ => {}
171            }
172            events.push(Ok(StreamEvent::ContentBlock(block)));
173        }
174        events.push(Ok(StreamEvent::Stop {
175            stop_reason: response.stop_reason,
176            usage: response.usage,
177        }));
178
179        Ok(Box::pin(futures::stream::iter(events)))
180    }
181}
182
183// Implement ModelProvider for Arc<dyn ModelProvider> to support dynamic dispatch
184#[async_trait::async_trait]
185impl ModelProvider for std::sync::Arc<dyn ModelProvider> {
186    fn name(&self) -> &str {
187        (**self).name()
188    }
189
190    fn max_context_tokens(&self) -> usize {
191        (**self).max_context_tokens()
192    }
193
194    fn max_output_tokens(&self) -> usize {
195        (**self).max_output_tokens()
196    }
197
198    fn estimate_token_count(&self, text: &str) -> usize {
199        (**self).estimate_token_count(text)
200    }
201
202    fn estimate_message_tokens(&self, messages: &[Message]) -> usize {
203        (**self).estimate_message_tokens(messages)
204    }
205
206    async fn generate(
207        &self,
208        messages: Vec<Message>,
209        tools: Vec<ToolDefinition>,
210        system_prompt: Option<String>,
211    ) -> Result<ModelResponse, ProviderError> {
212        (**self).generate(messages, tools, system_prompt).await
213    }
214
215    async fn generate_stream(
216        &self,
217        messages: Vec<Message>,
218        tools: Vec<ToolDefinition>,
219        system_prompt: Option<String>,
220    ) -> Result<BoxStream<'static, Result<StreamEvent, ProviderError>>, ProviderError> {
221        (**self)
222            .generate_stream(messages, tools, system_prompt)
223            .await
224    }
225}