1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use sha2::{Digest, Sha256};
12
13use crate::schema::ResponseShape;
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum ModelSelection {
18 Exact(String),
20 Capabilities {
22 capabilities: Vec<String>,
23 min_context_tokens: Option<i64>,
24 },
25 Default,
27}
28
29#[derive(Debug, Clone)]
31pub struct CompletionRequest {
32 pub node: String,
34 pub model: ModelSelection,
35 pub system: Option<String>,
36 pub prompt: String,
37 pub context: Vec<(String, Value)>,
39 pub response_type: String,
41 pub shape: ResponseShape,
42 pub max_tokens: u32,
44}
45
46impl CompletionRequest {
47 pub fn digest(&self) -> String {
52 let mut hasher = Sha256::new();
53 hasher.update(self.node.as_bytes());
54 hasher.update([0]);
55 hasher.update(self.system.as_deref().unwrap_or("").as_bytes());
56 hasher.update([0]);
57 hasher.update(self.prompt.as_bytes());
58 hasher.update([0]);
59 hasher.update(self.response_type.as_bytes());
60 for (name, value) in &self.context {
61 hasher.update([0]);
62 hasher.update(name.as_bytes());
63 hasher.update([0]);
64 hasher.update(value.to_string().as_bytes());
67 }
68 format!("{:x}", hasher.finalize())
69 }
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub struct CompletionResponse {
74 pub value: Value,
76 pub usage: Usage,
77 pub model: String,
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct Usage {
84 pub input_tokens: u64,
85 pub output_tokens: u64,
86 #[serde(default, skip_serializing_if = "is_zero")]
87 pub cache_read_tokens: u64,
88}
89
90fn is_zero(value: &u64) -> bool {
91 *value == 0
92}
93
94impl Usage {
95 pub fn total(&self) -> u64 {
96 self.input_tokens + self.output_tokens
97 }
98
99 pub fn add(&mut self, other: Usage) {
100 self.input_tokens += other.input_tokens;
101 self.output_tokens += other.output_tokens;
102 self.cache_read_tokens += other.cache_read_tokens;
103 }
104}
105
106#[derive(Debug)]
107pub enum ProviderError {
108 Transport(String),
110 Request { status: u16, message: String },
112 RateLimited { retry_after_seconds: Option<u64> },
114 Refused {
116 category: Option<String>,
117 explanation: Option<String>,
118 },
119 InvalidResponse(String),
121 Truncated { limit: u32 },
123 Configuration(String),
125 Cassette(String),
127}
128
129impl fmt::Display for ProviderError {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 ProviderError::Transport(message) => write!(f, "provider transport failed: {message}"),
133 ProviderError::Request { status, message } => {
134 write!(f, "provider rejected the request ({status}): {message}")
135 }
136 ProviderError::RateLimited {
137 retry_after_seconds: Some(seconds),
138 } => {
139 write!(f, "provider is rate limiting; retry after {seconds}s")
140 }
141 ProviderError::RateLimited { .. } => write!(f, "provider is rate limiting"),
142 ProviderError::Refused {
143 category,
144 explanation,
145 } => {
146 write!(f, "the provider declined to answer")?;
147 if let Some(category) = category {
148 write!(f, " ({category})")?;
149 }
150 if let Some(explanation) = explanation {
151 write!(f, ": {explanation}")?;
152 }
153 Ok(())
154 }
155 ProviderError::InvalidResponse(message) => {
156 write!(f, "the response did not match the declared type: {message}")
157 }
158 ProviderError::Truncated { limit } => {
159 write!(f, "the response was cut off at the {limit} token limit")
160 }
161 ProviderError::Configuration(message) => {
162 write!(f, "provider not configured: {message}")
163 }
164 ProviderError::Cassette(message) => write!(f, "cassette replay failed: {message}"),
165 }
166 }
167}
168
169impl std::error::Error for ProviderError {}
170
171pub type DeltaSink<'a> = &'a mut dyn FnMut(&str);
178
179pub trait ModelProvider {
181 fn name(&self) -> &str;
183
184 fn complete(
185 &mut self,
186 request: &CompletionRequest,
187 ) -> Result<CompletionResponse, ProviderError>;
188
189 fn streams(&self) -> bool {
198 false
199 }
200
201 fn complete_streaming(
213 &mut self,
214 request: &CompletionRequest,
215 on_delta: DeltaSink<'_>,
216 ) -> Result<CompletionResponse, ProviderError> {
217 let _ = on_delta;
218 self.complete(request)
219 }
220}
221
222impl<P: ModelProvider + ?Sized> ModelProvider for Box<P> {
226 fn name(&self) -> &str {
227 (**self).name()
228 }
229
230 fn complete(
231 &mut self,
232 request: &CompletionRequest,
233 ) -> Result<CompletionResponse, ProviderError> {
234 (**self).complete(request)
235 }
236
237 fn streams(&self) -> bool {
238 (**self).streams()
239 }
240
241 fn complete_streaming(
242 &mut self,
243 request: &CompletionRequest,
244 on_delta: DeltaSink<'_>,
245 ) -> Result<CompletionResponse, ProviderError> {
246 (**self).complete_streaming(request, on_delta)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use serde_json::json;
254
255 fn request() -> CompletionRequest {
256 CompletionRequest {
257 node: "n0".into(),
258 model: ModelSelection::Default,
259 system: None,
260 prompt: "Summarise this".into(),
261 context: vec![("document".into(), json!("hello"))],
262 response_type: "markdown".into(),
263 shape: ResponseShape::Prose,
264 max_tokens: 4096,
265 }
266 }
267
268 #[test]
269 fn the_digest_is_stable() {
270 assert_eq!(request().digest(), request().digest());
271 }
272
273 #[test]
274 fn the_digest_changes_with_the_prompt() {
275 let mut other = request();
276 other.prompt = "Summarise this document".into();
277 assert_ne!(request().digest(), other.digest());
278 }
279
280 #[test]
281 fn the_digest_changes_with_the_context() {
282 let mut other = request();
283 other.context = vec![("document".into(), json!("goodbye"))];
284 assert_ne!(request().digest(), other.digest());
285 }
286
287 #[test]
288 fn the_digest_changes_with_the_response_type() {
289 let mut other = request();
290 other.response_type = "text".into();
291 assert_ne!(request().digest(), other.digest());
292 }
293
294 #[test]
295 fn a_provider_that_cannot_stream_answers_at_once_and_shows_nothing() {
296 struct AtOnce;
297 impl ModelProvider for AtOnce {
298 fn name(&self) -> &str {
299 "at-once"
300 }
301 fn complete(
302 &mut self,
303 _request: &CompletionRequest,
304 ) -> Result<CompletionResponse, ProviderError> {
305 Ok(CompletionResponse {
306 value: json!("the whole answer"),
307 usage: Usage::default(),
308 model: "at-once".into(),
309 })
310 }
311 }
312
313 let mut seen = Vec::new();
314 let response = AtOnce
315 .complete_streaming(&request(), &mut |text| seen.push(text.to_string()))
316 .unwrap();
317 assert_eq!(response.value, json!("the whole answer"));
318 assert!(
319 seen.is_empty(),
320 "a provider with nothing live to show must not invent deltas: {seen:?}"
321 );
322 assert!(!AtOnce.streams());
323 }
324
325 #[test]
326 fn usage_accumulates() {
327 let mut total = Usage::default();
328 total.add(Usage {
329 input_tokens: 10,
330 output_tokens: 5,
331 cache_read_tokens: 0,
332 });
333 total.add(Usage {
334 input_tokens: 1,
335 output_tokens: 2,
336 cache_read_tokens: 3,
337 });
338 assert_eq!(total.input_tokens, 11);
339 assert_eq!(total.output_tokens, 7);
340 assert_eq!(total.cache_read_tokens, 3);
341 assert_eq!(total.total(), 18);
342 }
343}