1pub mod anthropic;
4pub mod circuit;
5
6const ERROR_BODY_MAX_BYTES: usize = 8 << 10; pub(crate) const STREAM_MAX_TEXT_BYTES: usize = 16 << 20; pub(crate) const STREAM_MAX_TOOL_ARGS_BYTES: usize = 1 << 20; pub(crate) const STREAM_MAX_TOOL_CALLS: usize = 256;
27
28pub(crate) async fn api_error_from_response(response: reqwest::Response) -> Error {
36 use futures::TryStreamExt;
37 let status = response.status().as_u16();
38 let message = if status == 401 || status == 403 {
39 format!("authentication failed (HTTP {status})")
40 } else {
41 let mut buf: Vec<u8> = Vec::with_capacity(2048);
42 let mut stream = response.bytes_stream();
43 let mut overflowed = false;
44 loop {
45 match stream.try_next().await {
46 Ok(Some(chunk)) => {
47 let remaining = ERROR_BODY_MAX_BYTES.saturating_sub(buf.len());
48 if remaining == 0 {
49 overflowed = true;
50 break;
51 }
52 let take = chunk.len().min(remaining);
53 buf.extend_from_slice(&chunk[..take]);
54 if take < chunk.len() {
55 overflowed = true;
56 break;
57 }
58 }
59 Ok(None) => break,
60 Err(e) => {
61 return Error::Api {
62 status,
63 message: format!("<body read error: {e}>"),
64 };
65 }
66 }
67 }
68 let mut text = String::from_utf8_lossy(&buf).to_string();
69 text.retain(|c| c == '\t' || (!c.is_control() && c != '\u{1b}'));
71 if overflowed {
72 text.push_str("…[truncated]");
73 }
74 text
75 };
76 Error::Api { status, message }
77}
78pub mod cascade;
79pub mod error_class;
80pub mod gemini;
81pub mod openai_compat;
82pub mod openrouter;
83pub mod pricing;
84pub mod registry;
85pub mod retry;
86pub mod types;
87
88use std::future::Future;
89use std::pin::Pin;
90use std::sync::Arc;
91
92use crate::error::Error;
93use crate::llm::types::{CompletionRequest, CompletionResponse};
94
95pub type OnText = dyn Fn(&str) + Send + Sync;
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum ApprovalDecision {
105 Allow,
107 Deny,
109 AlwaysAllow,
111 AlwaysDeny,
113}
114
115impl ApprovalDecision {
116 pub fn is_allowed(self) -> bool {
118 matches!(self, Self::Allow | Self::AlwaysAllow)
119 }
120
121 pub fn is_persistent(self) -> bool {
123 matches!(self, Self::AlwaysAllow | Self::AlwaysDeny)
124 }
125}
126
127impl From<bool> for ApprovalDecision {
128 fn from(allowed: bool) -> Self {
129 if allowed { Self::Allow } else { Self::Deny }
130 }
131}
132
133pub type OnApproval = dyn Fn(&[crate::llm::types::ToolCall]) -> ApprovalDecision + Send + Sync;
140
141pub trait LlmProvider: Send + Sync {
150 fn complete(
152 &self,
153 request: CompletionRequest,
154 ) -> impl Future<Output = Result<CompletionResponse, Error>> + Send;
155
156 fn stream_complete(
163 &self,
164 request: CompletionRequest,
165 on_text: &OnText,
166 ) -> impl Future<Output = Result<CompletionResponse, Error>> + Send {
167 let _ = on_text;
168 self.complete(request)
169 }
170
171 fn model_name(&self) -> Option<&str> {
175 None
176 }
177}
178
179pub trait DynLlmProvider: Send + Sync {
193 fn complete<'a>(
195 &'a self,
196 request: CompletionRequest,
197 ) -> Pin<Box<dyn Future<Output = Result<CompletionResponse, Error>> + Send + 'a>>;
198
199 fn stream_complete<'a>(
201 &'a self,
202 request: CompletionRequest,
203 on_text: &'a OnText,
204 ) -> Pin<Box<dyn Future<Output = Result<CompletionResponse, Error>> + Send + 'a>>;
205
206 fn model_name(&self) -> Option<&str>;
208}
209
210impl<P: LlmProvider> DynLlmProvider for P {
211 fn complete<'a>(
212 &'a self,
213 request: CompletionRequest,
214 ) -> Pin<Box<dyn Future<Output = Result<CompletionResponse, Error>> + Send + 'a>> {
215 Box::pin(LlmProvider::complete(self, request))
216 }
217
218 fn stream_complete<'a>(
219 &'a self,
220 request: CompletionRequest,
221 on_text: &'a OnText,
222 ) -> Pin<Box<dyn Future<Output = Result<CompletionResponse, Error>> + Send + 'a>> {
223 Box::pin(LlmProvider::stream_complete(self, request, on_text))
224 }
225
226 fn model_name(&self) -> Option<&str> {
227 LlmProvider::model_name(self)
228 }
229}
230
231pub struct BoxedProvider(Box<dyn DynLlmProvider>);
251
252impl BoxedProvider {
253 pub fn new<P: LlmProvider + 'static>(provider: P) -> Self {
255 Self(Box::new(provider))
256 }
257
258 pub fn from_arc<P: LlmProvider + 'static>(provider: Arc<P>) -> Self {
264 struct ArcAdapter<P>(Arc<P>);
266
267 impl<P: LlmProvider> LlmProvider for ArcAdapter<P> {
268 async fn complete(
269 &self,
270 request: CompletionRequest,
271 ) -> Result<CompletionResponse, Error> {
272 self.0.complete(request).await
273 }
274
275 async fn stream_complete(
276 &self,
277 request: CompletionRequest,
278 on_text: &OnText,
279 ) -> Result<CompletionResponse, Error> {
280 self.0.stream_complete(request, on_text).await
281 }
282
283 fn model_name(&self) -> Option<&str> {
284 self.0.model_name()
285 }
286 }
287
288 Self(Box::new(ArcAdapter(provider)))
289 }
290}
291
292impl LlmProvider for BoxedProvider {
293 async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, Error> {
294 self.0.complete(request).await
295 }
296
297 async fn stream_complete(
298 &self,
299 request: CompletionRequest,
300 on_text: &OnText,
301 ) -> Result<CompletionResponse, Error> {
302 self.0.stream_complete(request, on_text).await
303 }
304
305 fn model_name(&self) -> Option<&str> {
306 self.0.model_name()
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::llm::types::{ContentBlock, Message, StopReason, TokenUsage};
314 use std::sync::{Arc, Mutex};
315
316 struct FakeProvider;
317
318 impl LlmProvider for FakeProvider {
319 async fn complete(&self, _request: CompletionRequest) -> Result<CompletionResponse, Error> {
320 Ok(CompletionResponse {
321 content: vec![ContentBlock::Text {
322 text: "fake".into(),
323 }],
324 stop_reason: StopReason::EndTurn,
325 usage: TokenUsage::default(),
326 model: None,
327 })
328 }
329 }
330
331 struct StreamingFakeProvider;
332
333 impl LlmProvider for StreamingFakeProvider {
334 async fn complete(&self, _request: CompletionRequest) -> Result<CompletionResponse, Error> {
335 panic!("should call stream_complete, not complete");
336 }
337
338 async fn stream_complete(
339 &self,
340 _request: CompletionRequest,
341 on_text: &OnText,
342 ) -> Result<CompletionResponse, Error> {
343 on_text("hello");
344 on_text(" world");
345 Ok(CompletionResponse {
346 content: vec![ContentBlock::Text {
347 text: "hello world".into(),
348 }],
349 stop_reason: StopReason::EndTurn,
350 usage: TokenUsage::default(),
351 model: None,
352 })
353 }
354 }
355
356 fn test_request() -> CompletionRequest {
357 CompletionRequest {
358 system: String::new(),
359 messages: vec![Message::user("test")],
360 tools: vec![],
361 max_tokens: 100,
362 tool_choice: None,
363 reasoning_effort: None,
364 }
365 }
366
367 #[test]
368 fn dyn_llm_provider_wraps_provider() {
369 let provider = FakeProvider;
370 let dyn_provider: &dyn DynLlmProvider = &provider;
371 let _ = dyn_provider;
372 }
373
374 #[tokio::test]
375 async fn boxed_provider_delegates_complete() {
376 let provider = BoxedProvider::new(FakeProvider);
377 let response = LlmProvider::complete(&provider, test_request())
379 .await
380 .unwrap();
381 assert_eq!(response.text(), "fake");
382 }
383
384 #[tokio::test]
385 async fn boxed_provider_delegates_stream_complete() {
386 let provider = BoxedProvider::new(StreamingFakeProvider);
387 let received = Arc::new(Mutex::new(Vec::<String>::new()));
388 let received_clone = received.clone();
389 let on_text: &OnText = &move |text: &str| {
390 received_clone
391 .lock()
392 .expect("test lock")
393 .push(text.to_string());
394 };
395
396 let response = LlmProvider::stream_complete(&provider, test_request(), on_text)
397 .await
398 .unwrap();
399 assert_eq!(response.text(), "hello world");
400
401 let texts = received.lock().expect("test lock");
402 assert_eq!(*texts, vec!["hello", " world"]);
403 }
404
405 #[test]
406 fn boxed_provider_is_send_sync() {
407 fn assert_send_sync<T: Send + Sync>() {}
408 assert_send_sync::<BoxedProvider>();
409 }
410
411 #[tokio::test]
412 async fn boxed_provider_default_stream_falls_back_to_complete() {
413 let provider = BoxedProvider::new(FakeProvider);
415 let on_text: &OnText = &|_| {};
416 let response = LlmProvider::stream_complete(&provider, test_request(), on_text)
417 .await
418 .unwrap();
419 assert_eq!(response.text(), "fake");
420 }
421
422 #[tokio::test]
423 async fn boxed_provider_from_arc_delegates_complete() {
424 let provider = Arc::new(FakeProvider);
425 let boxed = BoxedProvider::from_arc(provider);
426 let response = LlmProvider::complete(&boxed, test_request()).await.unwrap();
427 assert_eq!(response.text(), "fake");
428 }
429
430 #[tokio::test]
431 async fn boxed_provider_from_arc_delegates_stream_complete() {
432 let provider = Arc::new(StreamingFakeProvider);
433 let boxed = BoxedProvider::from_arc(provider);
434 let received = Arc::new(Mutex::new(Vec::<String>::new()));
435 let received_clone = received.clone();
436 let on_text: &OnText = &move |text: &str| {
437 received_clone
438 .lock()
439 .expect("test lock")
440 .push(text.to_string());
441 };
442 let response = LlmProvider::stream_complete(&boxed, test_request(), on_text)
443 .await
444 .unwrap();
445 assert_eq!(response.text(), "hello world");
446 let texts = received.lock().expect("test lock");
447 assert_eq!(*texts, vec!["hello", " world"]);
448 }
449
450 #[test]
451 fn model_name_default_is_none() {
452 let provider = FakeProvider;
453 assert!(LlmProvider::model_name(&provider).is_none());
454 }
455
456 #[test]
457 fn boxed_provider_preserves_model_name() {
458 struct NamedProvider;
459 impl LlmProvider for NamedProvider {
460 async fn complete(
461 &self,
462 _request: CompletionRequest,
463 ) -> Result<CompletionResponse, Error> {
464 unimplemented!()
465 }
466 fn model_name(&self) -> Option<&str> {
467 Some("test-model")
468 }
469 }
470 let boxed = BoxedProvider::new(NamedProvider);
471 assert_eq!(LlmProvider::model_name(&boxed), Some("test-model"));
472 }
473
474 #[test]
475 fn boxed_provider_from_arc_preserves_model_name() {
476 struct NamedProvider;
477 impl LlmProvider for NamedProvider {
478 async fn complete(
479 &self,
480 _request: CompletionRequest,
481 ) -> Result<CompletionResponse, Error> {
482 unimplemented!()
483 }
484 fn model_name(&self) -> Option<&str> {
485 Some("arc-model")
486 }
487 }
488 let boxed = BoxedProvider::from_arc(Arc::new(NamedProvider));
489 assert_eq!(LlmProvider::model_name(&boxed), Some("arc-model"));
490 }
491
492 #[tokio::test]
493 async fn boxed_provider_from_arc_shares_underlying_provider() {
494 let call_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
496 struct CountingProvider(Arc<std::sync::atomic::AtomicUsize>);
497 impl LlmProvider for CountingProvider {
498 async fn complete(
499 &self,
500 _request: CompletionRequest,
501 ) -> Result<CompletionResponse, crate::error::Error> {
502 self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
503 Ok(CompletionResponse {
504 content: vec![ContentBlock::Text {
505 text: "counted".into(),
506 }],
507 stop_reason: StopReason::EndTurn,
508 usage: TokenUsage::default(),
509 model: None,
510 })
511 }
512 }
513
514 let inner = Arc::new(CountingProvider(call_count.clone()));
515 let boxed1 = BoxedProvider::from_arc(inner.clone());
516 let boxed2 = BoxedProvider::from_arc(inner);
517
518 LlmProvider::complete(&boxed1, test_request())
519 .await
520 .unwrap();
521 LlmProvider::complete(&boxed2, test_request())
522 .await
523 .unwrap();
524
525 assert_eq!(
526 call_count.load(std::sync::atomic::Ordering::Relaxed),
527 2,
528 "both boxed providers should share the same underlying provider"
529 );
530 }
531
532 #[test]
535 fn approval_decision_from_true() {
536 let decision = ApprovalDecision::from(true);
537 assert_eq!(decision, ApprovalDecision::Allow);
538 assert!(decision.is_allowed());
539 assert!(!decision.is_persistent());
540 }
541
542 #[test]
543 fn approval_decision_from_false() {
544 let decision = ApprovalDecision::from(false);
545 assert_eq!(decision, ApprovalDecision::Deny);
546 assert!(!decision.is_allowed());
547 assert!(!decision.is_persistent());
548 }
549
550 #[test]
551 fn approval_decision_always_allow() {
552 let decision = ApprovalDecision::AlwaysAllow;
553 assert!(decision.is_allowed());
554 assert!(decision.is_persistent());
555 }
556
557 #[test]
558 fn approval_decision_always_deny() {
559 let decision = ApprovalDecision::AlwaysDeny;
560 assert!(!decision.is_allowed());
561 assert!(decision.is_persistent());
562 }
563}