1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use futures::stream::{self, BoxStream};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::message::{Content, Message};
10use crate::pricing::Pricing;
11use crate::tool::ToolSpec;
12
13#[derive(Debug, Clone, Default)]
14pub struct ModelRequest {
15 pub system: Option<String>,
16 pub messages: Vec<Message>,
17 pub tools: Vec<ToolSpec>,
18 pub max_tokens: u32,
19 pub temperature: Option<f32>,
20 pub provider_options: BTreeMap<String, Value>,
25}
26
27#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum StopReason {
30 #[default]
31 EndTurn,
32 ToolUse,
33 MaxTokens,
34 Other(String),
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Usage {
39 pub input_tokens: u64,
40 pub output_tokens: u64,
41 #[serde(default)]
42 pub cache_read_tokens: u64,
43 #[serde(default)]
44 pub cache_write_tokens: u64,
45}
46
47impl Usage {
48 pub fn total(&self) -> u64 {
49 self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
50 }
51
52 pub fn add(&mut self, other: &Usage) {
53 self.input_tokens += other.input_tokens;
54 self.output_tokens += other.output_tokens;
55 self.cache_read_tokens += other.cache_read_tokens;
56 self.cache_write_tokens += other.cache_write_tokens;
57 }
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
61pub struct ModelResponse {
62 pub model: String,
64 pub content: Vec<Content>,
65 pub stop_reason: StopReason,
66 pub usage: Usage,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub served_by: Option<String>,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub rejected: Vec<RejectedAttempt>,
75}
76
77#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
79pub struct RejectedAttempt {
80 pub model: String,
81 pub reason: String,
82 pub usage: Usage,
83 pub cost_usd: Option<f64>,
84}
85
86impl ModelResponse {
87 pub fn total_usage(&self) -> Usage {
89 let mut u = self.usage;
90 for r in &self.rejected {
91 u.add(&r.usage);
92 }
93 u
94 }
95}
96
97#[derive(Debug, Clone, thiserror::Error)]
98pub enum ModelError {
99 #[error("transport error: {0}")]
100 Transport(String),
101 #[error("provider returned {status}: {body}")]
102 Api { status: u16, body: String },
103 #[error("could not decode provider response: {0}")]
104 Decode(String),
105 #[error("no usable response: {reason}")]
107 Unusable { reason: String, attempts: Vec<RejectedAttempt> },
108 #[error("{0}")]
109 Other(String),
110}
111
112impl ModelError {
113 pub fn retryable(&self) -> bool {
115 match self {
116 ModelError::Transport(_) => true,
117 ModelError::Api { status, .. } => *status == 408 || *status == 429 || *status >= 500,
118 _ => false,
119 }
120 }
121
122 pub fn spent(&self) -> (Usage, Option<f64>) {
125 match self {
126 ModelError::Unusable { attempts, .. } => {
127 let mut usage = Usage::default();
128 let mut cost = Some(0.0);
129 for a in attempts {
130 usage.add(&a.usage);
131 cost = cost.zip(a.cost_usd).map(|(x, y)| x + y);
132 }
133 (usage, cost)
134 }
135 _ => (Usage::default(), Some(0.0)),
136 }
137 }
138}
139
140#[async_trait]
141pub trait Model: Send + Sync {
142 fn id(&self) -> String;
144
145 async fn complete(&self, request: &ModelRequest) -> Result<ModelResponse, ModelError>;
146
147 async fn stream(&self, request: &ModelRequest) -> Result<ModelStream, ModelError> {
151 let response = self.complete(request).await?;
152 Ok(Box::pin(stream::iter(synthesize_stream(response).into_iter().map(Ok))))
153 }
154
155 fn pricing(&self) -> Option<Pricing> {
157 None
158 }
159
160 fn cost_usd(&self, response: &ModelResponse) -> Option<f64> {
162 self.pricing().map(|p| p.cost(&response.usage))
163 }
164
165 fn max_call_cost(&self, input_tokens: u64, output_tokens: u64) -> Option<f64> {
169 self.pricing()
170 .map(|p| (input_tokens as f64 * p.input.max(p.cache_write) + output_tokens as f64 * p.output) / 1_000_000.0)
171 }
172
173 fn unpriced_models(&self) -> Vec<String> {
176 if self.pricing().is_none() { vec![self.id()] } else { Vec::new() }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq)]
182pub enum StreamEvent {
183 TextDelta(String),
184 ThinkingDelta(String),
185 ThinkingSignature(String),
187 ToolCallStart {
189 index: usize,
190 id: String,
191 name: String,
192 },
193 ToolInputDelta {
194 index: usize,
195 partial_json: String,
196 },
197 Completed(ModelResponse),
199}
200
201pub type ModelStream = BoxStream<'static, Result<StreamEvent, ModelError>>;
202
203pub fn synthesize_stream(response: ModelResponse) -> Vec<StreamEvent> {
205 let mut events = Vec::new();
206 let mut tool_index = 0;
207 for c in &response.content {
208 match c {
209 Content::Text { text } => events.push(StreamEvent::TextDelta(text.clone())),
210 Content::Thinking { text, provider_data } => {
211 events.push(StreamEvent::ThinkingDelta(text.clone()));
212 if let Some(sig) = provider_data.as_ref().and_then(|d| d["signature"].as_str()) {
213 events.push(StreamEvent::ThinkingSignature(sig.to_owned()));
214 }
215 }
216 Content::ToolUse { id, name, input } => {
217 events.push(StreamEvent::ToolCallStart { index: tool_index, id: id.clone(), name: name.clone() });
218 events.push(StreamEvent::ToolInputDelta { index: tool_index, partial_json: input.to_string() });
219 tool_index += 1;
220 }
221 Content::ToolResult { .. } => {}
222 }
223 }
224 events.push(StreamEvent::Completed(response));
225 events
226}
227
228type UsableFn = Arc<dyn Fn(&ModelResponse) -> Result<(), String> + Send + Sync>;
229
230pub struct FallbackModel {
244 models: Vec<Arc<dyn Model>>,
245 allow_truncated: bool,
246 allow_empty: bool,
247 json_fields: Option<Vec<String>>,
248 custom: Option<UsableFn>,
249 keep_last: bool,
250}
251
252impl FallbackModel {
253 pub fn new(models: Vec<Arc<dyn Model>>) -> Self {
254 assert!(!models.is_empty(), "FallbackModel needs at least one model");
255 Self { models, allow_truncated: false, allow_empty: false, json_fields: None, custom: None, keep_last: false }
256 }
257
258 pub fn keep_last_answer(mut self) -> Self {
266 self.keep_last = true;
267 self
268 }
269
270 pub fn accept_truncated(mut self) -> Self {
272 self.allow_truncated = true;
273 self
274 }
275
276 pub fn accept_empty(mut self) -> Self {
278 self.allow_empty = true;
279 self
280 }
281
282 pub fn require_json_fields(mut self, fields: &[&str]) -> Self {
285 self.json_fields = Some(fields.iter().map(|f| (*f).to_owned()).collect());
286 self
287 }
288
289 pub fn require(mut self, check: impl Fn(&ModelResponse) -> Result<(), String> + Send + Sync + 'static) -> Self {
291 self.custom = Some(Arc::new(check));
292 self
293 }
294
295 fn has_checks(&self) -> bool {
296 !self.allow_truncated || !self.allow_empty || self.json_fields.is_some() || self.custom.is_some()
297 }
298
299 pub fn check(&self, r: &ModelResponse) -> Result<(), String> {
301 if !self.allow_truncated && r.stop_reason == StopReason::MaxTokens {
302 return Err("reply was cut off at the token limit".into());
303 }
304 let has_tools = r.content.iter().any(|c| matches!(c, Content::ToolUse { .. }));
305 let text = crate::message::content_text(&r.content);
306 if !self.allow_empty && !has_tools && text.trim().is_empty() {
307 return Err("reply is empty".into());
308 }
309 if let (Some(fields), false) = (&self.json_fields, has_tools) {
310 let Some(serde_json::Value::Object(obj)) = crate::output::extract_json(&text) else {
311 return Err("reply is not a JSON object".into());
312 };
313 let missing: Vec<&str> = fields.iter().filter(|f| !obj.contains_key(*f)).map(String::as_str).collect();
314 if !missing.is_empty() {
315 return Err(format!("JSON is missing required field(s): {}", missing.join(", ")));
316 }
317 }
318 if let Some(check) = &self.custom {
319 check(r)?;
320 }
321 Ok(())
322 }
323}
324
325#[async_trait]
326impl Model for FallbackModel {
327 fn id(&self) -> String {
328 let ids: Vec<_> = self.models.iter().map(|m| m.id()).collect();
329 format!("fallback({})", ids.join(","))
330 }
331
332 async fn complete(&self, request: &ModelRequest) -> Result<ModelResponse, ModelError> {
333 let mut last_err = None;
334 let mut rejected: Vec<RejectedAttempt> = Vec::new();
335 let mut last_unusable: Option<ModelResponse> = None;
337 for model in &self.models {
338 match model.complete(request).await {
339 Ok(mut resp) => {
340 resp.served_by.get_or_insert_with(|| model.id());
341 match self.check(&resp) {
342 Ok(()) => {
343 rejected.append(&mut resp.rejected);
344 resp.rejected = rejected;
345 return Ok(resp);
346 }
347 Err(reason) => {
348 tracing::warn!(model = %model.id(), %reason, "unusable answer, falling back");
349 rejected.append(&mut resp.rejected);
350 if self.keep_last {
351 last_unusable = Some(resp.clone());
352 last_err = None;
353 }
354 rejected.push(RejectedAttempt {
355 model: model.id(),
356 reason,
357 usage: resp.usage,
358 cost_usd: model.cost_usd(&resp),
359 });
360 }
361 }
362 }
363 Err(e) if e.retryable() => {
364 tracing::warn!(model = %model.id(), error = %e, "model failed, falling back");
365 last_err = Some(e);
366 }
367 Err(ModelError::Unusable { reason, attempts }) => {
368 rejected.extend(attempts);
369 last_err = Some(ModelError::Other(reason));
370 }
371 Err(e) if rejected.is_empty() => return Err(e),
372 Err(e) => {
373 last_err = Some(e);
374 break;
375 }
376 }
377 }
378 if rejected.is_empty() {
379 return Err(last_err.expect("at least one model"));
380 }
381 if let (Some(mut resp), None) = (last_unusable, &last_err) {
385 rejected.pop();
386 resp.rejected = rejected;
387 tracing::warn!(model = resp.served_by.as_deref().unwrap_or(""), "no usable answer; keeping the last one");
388 return Ok(resp);
389 }
390 let mut reason = rejected.iter().map(|r| format!("{}: {}", r.model, r.reason)).collect::<Vec<_>>().join("; ");
392 if let Some(e) = last_err {
393 reason = format!("{reason}; last error: {e}");
394 }
395 Err(ModelError::Unusable { reason, attempts: rejected })
396 }
397
398 async fn stream(&self, request: &ModelRequest) -> Result<ModelStream, ModelError> {
401 if self.has_checks() {
402 let response = self.complete(request).await?;
404 return Ok(Box::pin(stream::iter(synthesize_stream(response).into_iter().map(Ok))));
405 }
406 let mut last_err = None;
407 for model in &self.models {
408 match model.stream(request).await {
409 Ok(s) => {
410 let id = model.id();
411 return Ok(Box::pin(futures::StreamExt::map(s, move |ev| match ev {
412 Ok(StreamEvent::Completed(mut resp)) => {
413 resp.served_by.get_or_insert_with(|| id.clone());
414 Ok(StreamEvent::Completed(resp))
415 }
416 other => other,
417 })));
418 }
419 Err(e) if e.retryable() => {
420 tracing::warn!(model = %model.id(), error = %e, "model stream failed, falling back");
421 last_err = Some(e);
422 }
423 Err(e) => return Err(e),
424 }
425 }
426 Err(last_err.expect("at least one model"))
427 }
428
429 fn pricing(&self) -> Option<Pricing> {
432 self.models.iter().map(|m| m.pricing()).try_fold(Pricing::default(), |acc, p| p.map(|p| acc.max(p)))
433 }
434
435 fn cost_usd(&self, response: &ModelResponse) -> Option<f64> {
440 let served = response.served_by.as_deref();
441 let answer = match self.models.iter().find(|m| Some(m.id().as_str()) == served) {
442 Some(m) => m.cost_usd(&ModelResponse { rejected: Vec::new(), ..response.clone() }),
443 None => self.pricing().map(|p| p.cost(&response.usage)),
444 }?;
445 let conservative = self.pricing();
446 response
447 .rejected
448 .iter()
449 .try_fold(answer, |acc, r| r.cost_usd.or_else(|| conservative.map(|p| p.cost(&r.usage))).map(|c| acc + c))
450 }
451
452 fn unpriced_models(&self) -> Vec<String> {
453 self.models.iter().flat_map(|m| m.unpriced_models()).collect()
454 }
455
456 fn max_call_cost(&self, input_tokens: u64, output_tokens: u64) -> Option<f64> {
458 self.models.iter().map(|m| m.max_call_cost(input_tokens, output_tokens)).sum()
459 }
460}
461
462#[async_trait]
463impl<M: Model + ?Sized> Model for Arc<M> {
464 fn id(&self) -> String {
465 (**self).id()
466 }
467
468 async fn complete(&self, request: &ModelRequest) -> Result<ModelResponse, ModelError> {
469 (**self).complete(request).await
470 }
471
472 async fn stream(&self, request: &ModelRequest) -> Result<ModelStream, ModelError> {
473 (**self).stream(request).await
474 }
475
476 fn pricing(&self) -> Option<Pricing> {
477 (**self).pricing()
478 }
479
480 fn cost_usd(&self, response: &ModelResponse) -> Option<f64> {
481 (**self).cost_usd(response)
482 }
483
484 fn unpriced_models(&self) -> Vec<String> {
485 (**self).unpriced_models()
486 }
487
488 fn max_call_cost(&self, input_tokens: u64, output_tokens: u64) -> Option<f64> {
489 (**self).max_call_cost(input_tokens, output_tokens)
490 }
491}