use std::collections::VecDeque;
use std::time::{Duration, Instant};
use pagesift_api::types::{AdaptiveOptions, AgentBudget, AgentEffort, AgentRequest};
use pagesift_llm::ChatClient;
const DELTA: f64 = 0.05;
const EFFORT_STEP: usize = 4;
const DEFAULT_CONTEXT: usize = 8192;
const RATE_WINDOW: Duration = Duration::from_secs(60);
fn effort_pages(effort: Option<AgentEffort>) -> usize {
let low = AdaptiveOptions::default().patience.max(1);
match effort {
Some(AgentEffort::Low) => low,
Some(AgentEffort::Medium) | None => low * EFFORT_STEP,
Some(AgentEffort::High) => low * EFFORT_STEP * EFFORT_STEP,
}
}
pub(super) fn call_limits(
context: usize,
max_completion: Option<usize>,
per_minute: Option<usize>,
) -> (usize, usize) {
let usable = (per_minute.map_or(context, |t| t.min(context)) as f64 * (1.0 - DELTA)) as usize;
let half = usable / 2;
let output = max_completion.map_or(half, |o| o.min(half));
(usable - output, output)
}
#[derive(Debug, Clone)]
pub(super) struct Budget {
pub max_pages: u64,
pub max_calls: u64,
pub max_tokens: u64,
pub max_credits: Option<u64>,
pub input: usize,
pub output: usize,
pub per_minute: Option<usize>,
pub page_tokens: usize,
}
impl Budget {
pub async fn new(llm: &ChatClient, req: &AgentRequest) -> Self {
let (context, completion) = llm.model_limits().await.unwrap_or((DEFAULT_CONTEXT, None));
let per_minute = llm.token_rate_limit().await;
let (input, output) = call_limits(context, completion, per_minute);
Self {
per_minute,
..Self::from_limits(input, output, req)
}
}
pub fn from_limits(input: usize, output: usize, req: &AgentRequest) -> Self {
let pages = effort_pages(req.effort);
let calls = 2 * pages + 2;
Self {
max_pages: pages as u64,
max_calls: calls as u64,
max_tokens: (calls * (input + output)) as u64,
max_credits: req.max_credits,
input,
output,
per_minute: None,
page_tokens: (input / 4).max(1), }
}
pub fn report(&self, exhausted: Option<&str>) -> AgentBudget {
AgentBudget {
max_pages: self.max_pages,
max_llm_calls: self.max_calls,
max_tokens: self.max_tokens,
max_credits: self.max_credits,
input_tokens_per_call: self.input as u64,
exhausted: exhausted.map(str::to_string),
}
}
}
#[derive(Debug, Default)]
pub(super) struct Pacer {
limit: Option<usize>,
used: VecDeque<(Instant, usize)>,
}
impl Pacer {
pub fn new(limit: Option<usize>) -> Self {
Self {
limit,
used: VecDeque::new(),
}
}
pub fn wait_for(&mut self, tokens: usize, now: Instant) -> Duration {
let Some(limit) = self.limit else {
return Duration::ZERO;
};
while self
.used
.front()
.is_some_and(|(t, _)| now.duration_since(*t) >= RATE_WINDOW)
{
self.used.pop_front();
}
let mut in_window: usize = self.used.iter().map(|(_, n)| n).sum();
for (t, n) in &self.used {
if in_window + tokens <= limit {
return Duration::ZERO;
}
in_window -= n;
if in_window + tokens <= limit {
return RATE_WINDOW.saturating_sub(now.duration_since(*t));
}
}
Duration::ZERO
}
pub fn record(&mut self, tokens: usize, now: Instant) {
self.used.push_back((now, tokens));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn req(effort: Option<AgentEffort>) -> AgentRequest {
AgentRequest {
prompt: "p".into(),
urls: None,
schema: None,
effort,
strict_constrain_to_urls: false,
max_credits: None,
webhook: None,
allowed_hosts: None,
}
}
#[test]
fn effort_changes_only_the_caps() {
let low = Budget::from_limits(3800, 3800, &req(Some(AgentEffort::Low)));
let med = Budget::from_limits(3800, 3800, &req(None));
let high = Budget::from_limits(3800, 3800, &req(Some(AgentEffort::High)));
assert_eq!((low.max_pages, med.max_pages, high.max_pages), (5, 20, 80));
assert_eq!((low.max_calls, med.max_calls), (12, 42));
assert_eq!(low.max_tokens, 12 * 7600);
assert_eq!(low.page_tokens, high.page_tokens);
}
#[test]
fn limits_respect_rate_context_and_completion_cap() {
assert_eq!(call_limits(131_072, Some(65_536), Some(8000)), (3800, 3800));
assert_eq!(call_limits(100_000, Some(1024), None), (93_976, 1024));
assert_eq!(call_limits(8192, None, None), (3891, 3891));
}
#[test]
fn pacer_waits_for_the_oldest_to_expire() {
let t0 = Instant::now();
let mut p = Pacer::new(Some(1000));
assert_eq!(p.wait_for(600, t0), Duration::ZERO);
p.record(600, t0);
let t1 = t0 + Duration::from_secs(10);
assert_eq!(p.wait_for(300, t1), Duration::ZERO);
p.record(300, t1);
assert_eq!(
p.wait_for(300, t0 + Duration::from_secs(10)),
Duration::from_secs(50)
);
assert_eq!(
p.wait_for(300, t0 + Duration::from_secs(61)),
Duration::ZERO
);
assert_eq!(Pacer::new(None).wait_for(1 << 40, t0), Duration::ZERO);
}
}