Skip to main content

rullama_core/
provider.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use futures::stream::BoxStream;
4use serde::{Deserialize, Serialize};
5
6use crate::message::{ChatResponse, Message, StreamChunk};
7use crate::tool::Tool;
8
9/// Base provider trait for AI providers
10#[async_trait]
11pub trait Provider: Send + Sync {
12    /// Get the provider name
13    fn name(&self) -> &str;
14
15    /// Get the model's maximum output tokens (for setting appropriate limits)
16    /// Returns None if the model doesn't have a specific limit
17    fn max_output_tokens(&self) -> Option<u32> {
18        None // Default implementation - providers can override
19    }
20
21    /// Chat completion (non-streaming)
22    async fn chat(
23        &self,
24        messages: &[Message],
25        tools: Option<&[Tool]>,
26        options: &ChatOptions,
27    ) -> Result<ChatResponse>;
28
29    /// Chat completion (streaming)
30    fn stream_chat<'a>(
31        &'a self,
32        messages: &'a [Message],
33        tools: Option<&'a [Tool]>,
34        options: &'a ChatOptions,
35    ) -> BoxStream<'a, Result<StreamChunk>>;
36}
37
38/// Prompt-cache strategy for providers that support explicit caching
39/// (Anthropic Messages API today; a no-op elsewhere).
40///
41/// Controls which parts of a request receive `cache_control` breakpoints.
42/// Caching reuses cached prompt bytes across turns for a 50–90% input-token
43/// discount on subsequent calls, at the cost of a one-time "creation" charge
44/// on first population.
45#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum CacheStrategy {
48    /// No cache breakpoints. Fresh compute on every call.
49    Off,
50    /// Cache only the system prompt.
51    SystemOnly,
52    /// Cache the system prompt and tool definitions (the default).
53    #[default]
54    SystemAndTools,
55    /// Cache system + tools + the tail of the conversation once the message
56    /// history reaches the given approximate token threshold.
57    SystemAndTailTurn {
58        /// Minimum conversation size (approximate tokens) before the tail
59        /// breakpoint is emitted. Avoids wasting a cache slot on short chats.
60        threshold_tokens: u32,
61    },
62}
63
64/// Chat completion options
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ChatOptions {
67    /// Temperature (0.0 - 1.0)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub temperature: Option<f32>,
70    /// Maximum tokens to generate
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub max_tokens: Option<u32>,
73    /// Top-p sampling
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub top_p: Option<f32>,
76    /// Stop sequences
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub stop: Option<Vec<String>>,
79    /// System prompt
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub system: Option<String>,
82    /// Per-request model override.
83    ///
84    /// When `Some`, providers MUST use this model name instead of their default.
85    /// This enables per-session model switching without replacing the provider.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub model: Option<String>,
88    /// Prompt-cache strategy. Ignored by providers without prompt caching.
89    #[serde(default)]
90    pub cache_strategy: CacheStrategy,
91    /// Caller-supplied identifier used to attribute the resulting cost,
92    /// tokens, and duration in telemetry. `ChatAgent` generates a fresh
93    /// id per turn; multi-tenant deployments can stamp their own
94    /// (e.g. `"<tenant>-<msg_id>"`) so the analytics query
95    /// `cost_by_request(id)` can pull every event for one request.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub request_id: Option<String>,
98    /// Cooperative cancellation handle for `stream_chat`. When the token
99    /// cancels, the provider stops emitting chunks at the next
100    /// poll-point and drops the underlying HTTP connection. Has no
101    /// effect on the non-streaming `chat` call (use `tokio::select!`
102    /// against the future instead).
103    ///
104    /// Not serialised — only meaningful inside one process.
105    #[serde(skip)]
106    pub cancel: Option<tokio_util::sync::CancellationToken>,
107}
108
109impl Default for ChatOptions {
110    fn default() -> Self {
111        Self {
112            temperature: Some(0.7),
113            max_tokens: Some(4096),
114            top_p: None,
115            stop: None,
116            system: None,
117            model: None,
118            cache_strategy: CacheStrategy::default(),
119            request_id: None,
120            cancel: None,
121        }
122    }
123}
124
125impl ChatOptions {
126    /// Create new chat options with defaults
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Set temperature
132    pub fn temperature(mut self, temperature: f32) -> Self {
133        self.temperature = Some(temperature);
134        self
135    }
136
137    /// Set max tokens
138    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
139        self.max_tokens = Some(max_tokens);
140        self
141    }
142
143    /// Set system prompt
144    pub fn system<S: Into<String>>(mut self, system: S) -> Self {
145        self.system = Some(system.into());
146        self
147    }
148
149    /// Set top-p sampling
150    pub fn top_p(mut self, top_p: f32) -> Self {
151        self.top_p = Some(top_p);
152        self
153    }
154
155    /// Override the model for this request.
156    pub fn model<S: Into<String>>(mut self, model: S) -> Self {
157        self.model = Some(model.into());
158        self
159    }
160
161    /// Set the prompt-cache strategy.
162    pub fn cache_strategy(mut self, strategy: CacheStrategy) -> Self {
163        self.cache_strategy = strategy;
164        self
165    }
166
167    /// Stamp a caller-supplied `request_id` so telemetry can attribute
168    /// the resulting tokens / cost to this specific call.
169    pub fn request_id<S: Into<String>>(mut self, request_id: S) -> Self {
170        self.request_id = Some(request_id.into());
171        self
172    }
173
174    /// Attach a cancellation token to `stream_chat`. Cancelling stops
175    /// chunk delivery at the next poll-point and drops the upstream HTTP
176    /// connection.
177    pub fn cancel_with(mut self, token: tokio_util::sync::CancellationToken) -> Self {
178        self.cancel = Some(token);
179        self
180    }
181
182    /// Deterministic classification/routing (temp=0, few tokens)
183    pub fn deterministic(max_tokens: u32) -> Self {
184        Self {
185            temperature: Some(0.0),
186            max_tokens: Some(max_tokens),
187            ..Default::default()
188        }
189    }
190
191    /// Low-temperature factual generation
192    pub fn factual(max_tokens: u32) -> Self {
193        Self {
194            temperature: Some(0.1),
195            max_tokens: Some(max_tokens),
196            top_p: Some(0.9),
197            ..Default::default()
198        }
199    }
200
201    /// Creative generation with moderate temperature
202    pub fn creative(max_tokens: u32) -> Self {
203        Self {
204            temperature: Some(0.3),
205            max_tokens: Some(max_tokens),
206            ..Default::default()
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_chat_options_default() {
217        let opts = ChatOptions::default();
218        assert_eq!(opts.temperature, Some(0.7));
219        assert_eq!(opts.max_tokens, Some(4096));
220    }
221
222    #[test]
223    fn test_chat_options_builder() {
224        let opts = ChatOptions::new()
225            .temperature(0.5)
226            .max_tokens(2048)
227            .system("Test");
228        assert_eq!(opts.temperature, Some(0.5));
229        assert_eq!(opts.max_tokens, Some(2048));
230        assert_eq!(opts.system, Some("Test".to_string()));
231    }
232
233    #[test]
234    fn test_chat_options_deterministic() {
235        let opts = ChatOptions::deterministic(50);
236        assert_eq!(opts.temperature, Some(0.0));
237        assert_eq!(opts.max_tokens, Some(50));
238    }
239
240    #[test]
241    fn test_chat_options_factual() {
242        let opts = ChatOptions::factual(200);
243        assert_eq!(opts.temperature, Some(0.1));
244        assert_eq!(opts.max_tokens, Some(200));
245        assert_eq!(opts.top_p, Some(0.9));
246    }
247
248    #[test]
249    fn test_chat_options_creative() {
250        let opts = ChatOptions::creative(400);
251        assert_eq!(opts.temperature, Some(0.3));
252        assert_eq!(opts.max_tokens, Some(400));
253    }
254}