use super::*;
impl RetryMiddleware {
pub fn new(policy: RetryPolicy) -> Self {
Self {
label: "retry",
policy,
}
}
pub fn with_default_policy() -> Self {
Self::new(RetryPolicy::default())
}
pub fn backoff_for_attempt(&self, attempt: usize) -> Duration {
self.policy.backoff_for_attempt(attempt)
}
}
#[async_trait]
impl<State: Send + Sync, Ctx: Send + Sync> ModelMiddleware<State, Ctx> for RetryMiddleware {
fn name(&self) -> &str {
self.label
}
async fn wrap_model(
&self,
ctx: &mut RunContext<Ctx>,
state: &State,
request: ModelRequest,
next: ModelHandler<'_, State, Ctx>,
) -> Result<MiddlewareModelOutcome> {
let mut attempt = 0usize;
loop {
match next.run(ctx, state, request.clone()).await {
Ok(outcome) => return Ok(outcome),
Err(error) => {
if self.policy.should_retry_error(attempt, &error) {
let backoff_attempt = attempt;
attempt += 1;
let call_id = CallId::new(format!("{}-model", ctx.run_id()));
ctx.emit(AgentEvent::RetryScheduled { call_id, attempt });
self.policy.sleep_backoff(backoff_attempt).await;
continue;
}
return Err(error);
}
}
}
}
}
impl TimeoutMiddleware {
pub fn new(timeout: Duration) -> Self {
Self {
label: "timeout",
timeout,
}
}
pub fn from_millis(ms: u64) -> Self {
Self::new(Duration::from_millis(ms))
}
}
#[async_trait]
impl<State: Send + Sync, Ctx: Send + Sync> ModelMiddleware<State, Ctx> for TimeoutMiddleware {
fn name(&self) -> &str {
self.label
}
async fn wrap_model(
&self,
ctx: &mut RunContext<Ctx>,
state: &State,
request: ModelRequest,
next: ModelHandler<'_, State, Ctx>,
) -> Result<MiddlewareModelOutcome> {
let run_id = ctx.run_id().as_str().to_string();
let fut = next.run(ctx, state, request);
match tokio::time::timeout(self.timeout, fut).await {
Ok(result) => result,
Err(_) => Err(TinyAgentsError::Timeout(format!(
"model call for run `{run_id}` exceeded the {} ms middleware timeout",
self.timeout.as_millis()
))),
}
}
}
impl ModelFallbackMiddleware {
pub fn new(fallbacks: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
label: "model_fallback",
fallbacks: fallbacks.into_iter().map(Into::into).collect(),
}
}
}
#[async_trait]
impl<State: Send + Sync, Ctx: Send + Sync> ModelMiddleware<State, Ctx> for ModelFallbackMiddleware {
fn name(&self) -> &str {
self.label
}
async fn wrap_model(
&self,
ctx: &mut RunContext<Ctx>,
state: &State,
request: ModelRequest,
next: ModelHandler<'_, State, Ctx>,
) -> Result<MiddlewareModelOutcome> {
match next.run(ctx, state, request.clone()).await {
Ok(outcome) => Ok(outcome),
Err(mut last_error) => {
let mut current = request.model.clone().unwrap_or_default();
for fallback in &self.fallbacks {
if !is_retryable(&last_error) {
break;
}
ctx.emit(AgentEvent::FallbackSelected {
from: current.clone(),
to: fallback.clone(),
});
let mut req = request.clone();
req.model = Some(fallback.clone());
match next.run(ctx, state, req).await {
Ok(outcome) => return Ok(outcome),
Err(error) => {
last_error = error;
current = fallback.clone();
}
}
}
Err(last_error)
}
}
}
}
impl RateLimitMiddleware {
pub fn new(limiter: Arc<RateLimiter>) -> Self {
Self {
label: "rate_limit",
limiter,
tokens: 1,
behavior: RateLimitBehavior::Error,
poll_interval: Duration::from_millis(50),
now: Arc::new(Instant::now),
}
}
pub fn with_tokens(mut self, tokens: u64) -> Self {
self.tokens = tokens;
self
}
pub fn with_behavior(mut self, behavior: RateLimitBehavior) -> Self {
self.behavior = behavior;
self
}
pub fn waiting(mut self, poll_interval: Duration) -> Self {
self.behavior = RateLimitBehavior::Wait;
self.poll_interval = poll_interval;
self
}
pub fn with_clock(mut self, now: NowFn) -> Self {
self.now = now;
self
}
}
#[async_trait]
impl<State: Send + Sync, Ctx: Send + Sync> ModelMiddleware<State, Ctx> for RateLimitMiddleware {
fn name(&self) -> &str {
self.label
}
async fn wrap_model(
&self,
ctx: &mut RunContext<Ctx>,
state: &State,
request: ModelRequest,
next: ModelHandler<'_, State, Ctx>,
) -> Result<MiddlewareModelOutcome> {
let mut wait_start: Option<Instant> = None;
loop {
let now = (self.now)();
if self.limiter.try_acquire(self.tokens, now) {
if let Some(start) = wait_start {
ctx.emit(AgentEvent::RateLimitWaited {
waited_ms: now.saturating_duration_since(start).as_millis() as u64,
});
}
break;
}
match self.behavior {
RateLimitBehavior::Error => {
return Err(TinyAgentsError::LimitExceeded(format!(
"rate limit: could not acquire {} token(s)",
self.tokens
)));
}
RateLimitBehavior::Wait => {
if !self.limiter.can_ever_acquire(self.tokens) {
return Err(TinyAgentsError::LimitExceeded(format!(
"rate limit: waiting for {} token(s) can never succeed \
(bucket capacity {}, refill {}/s)",
self.tokens,
self.limiter.capacity(),
self.limiter.refill_per_sec()
)));
}
wait_start.get_or_insert(now);
tokio::time::sleep(self.poll_interval).await;
}
}
}
next.run(ctx, state, request).await
}
}