machi_llm/
breaker_sampler.rs1use std::sync::Arc;
6
7use async_trait::async_trait;
8use machi_types::{ErrorCode, MachiError};
9
10use crate::breaker::{Admission, BreakerOutcome, CircuitBreaker};
11use crate::sample::{SampleRequest, SampleResponse};
12use crate::sampler::LlmSampler;
13use crate::stream::SampleStream;
14
15#[derive(Debug, Clone)]
17pub struct BreakerSampler<S> {
18 inner: Arc<S>,
19 breaker: Arc<CircuitBreaker>,
20 endpoint: String,
22}
23
24impl<S> BreakerSampler<S> {
25 #[must_use]
27 pub fn new(inner: Arc<S>, breaker: Arc<CircuitBreaker>, endpoint: impl Into<String>) -> Self {
28 Self {
29 inner,
30 breaker,
31 endpoint: endpoint.into(),
32 }
33 }
34
35 #[must_use]
37 pub fn endpoint(&self) -> &str {
38 &self.endpoint
39 }
40
41 #[must_use]
43 pub fn breaker(&self) -> &Arc<CircuitBreaker> {
44 &self.breaker
45 }
46}
47
48#[async_trait]
49impl<S: LlmSampler + 'static> LlmSampler for BreakerSampler<S> {
50 async fn sample(&self, request: SampleRequest) -> Result<SampleResponse, MachiError> {
51 self.admit()?;
52 match self.inner.sample(request).await {
53 Ok(r) => {
54 self.breaker.record(BreakerOutcome::Success);
55 Ok(r)
56 }
57 Err(e) => {
58 self.breaker.record(BreakerOutcome::Failure);
59 Err(e)
60 }
61 }
62 }
63
64 async fn sample_stream(&self, request: SampleRequest) -> Result<SampleStream, MachiError> {
65 self.admit()?;
66 match self.inner.sample_stream(request).await {
67 Ok(s) => {
68 self.breaker.record(BreakerOutcome::Success);
69 Ok(s)
70 }
71 Err(e) => {
72 self.breaker.record(BreakerOutcome::Failure);
73 Err(e)
74 }
75 }
76 }
77}
78
79impl<S> BreakerSampler<S> {
80 fn admit(&self) -> Result<(), MachiError> {
81 match self.breaker.check() {
82 Admission::Allow => Ok(()),
83 Admission::Reject { retry_after } => Err(MachiError::new(
84 ErrorCode::LlmProvider,
85 format!(
86 "circuit breaker open for endpoint '{}'; retry after {}ms",
87 self.endpoint,
88 retry_after.as_millis()
89 ),
90 )
91 .with_retry(machi_types::RetryClass::Backoff)),
92 }
93 }
94}