bamboo-llm 2026.10.21

LLM provider layer (providers, protocol adapters, streaming, routing, model catalog) for the Bamboo agent framework
Documentation
use super::*;
use bamboo_domain::{AdmissionCommit, LiveBootstrapProof};
use std::sync::atomic::{AtomicUsize, Ordering};
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, Respond, ResponseTemplate};

struct FailThenOk {
    hits: Arc<AtomicUsize>,
    delay_first: bool,
    times: Arc<Mutex<Vec<Instant>>>,
}

impl Respond for FailThenOk {
    fn respond(&self, _: &wiremock::Request) -> ResponseTemplate {
        self.times.lock().unwrap().push(Instant::now());
        if self.hits.fetch_add(1, Ordering::SeqCst) == 0 {
            if self.delay_first {
                ResponseTemplate::new(200).set_delay(Duration::from_millis(250))
            } else {
                ResponseTemplate::new(503).insert_header("Retry-After", "1")
            }
        } else {
            ResponseTemplate::new(200).set_body_string("ok")
        }
    }
}

async fn run_real_managed_request(delay_first: bool) {
    let server = MockServer::start().await;
    let hits = Arc::new(AtomicUsize::new(0));
    let times = Arc::new(Mutex::new(Vec::new()));
    Mock::given(method("POST"))
        .respond_with(FailThenOk {
            hits: hits.clone(),
            delay_first,
            times: times.clone(),
        })
        .mount(&server)
        .await;
    let (observer, offer) = InitialHttpRetryObserver::with_live_bootstrap();
    let observed = observer.clone();
    let url = server.uri();
    let task = tokio::spawn(async move {
        let config = RetryConfig {
            max_attempts: 1,
            base_delay: Duration::from_millis(1),
            max_delay: Duration::from_millis(5),
        };
        let mut budget = InitialRequestBudget::new(&config);
        budget.attempt_timeout = Duration::from_millis(30);
        budget.deadline = Instant::now() + Duration::from_secs(3);
        let client = reqwest::Client::builder().no_proxy().build().unwrap();
        observe_initial_http_retries(observed, async {
            send_anthropic_with_retry(&config, &budget, || {
                client
                    .post(&url)
                    .json(&serde_json::json!({"synthetic":"request"}))
            })
            .await
        })
        .await
    });
    let offer = tokio::time::timeout(Duration::from_secs(2), offer)
        .await
        .unwrap()
        .expect("actual managed request must offer continuation");
    assert_eq!(hits.load(Ordering::SeqCst), 1);
    assert!(
        observer.terminal_diagnostic().is_none(),
        "waiting is not terminal"
    );
    let gate = offer.gate();
    let proof = LiveBootstrapProof::from_storage((), gate.clone());
    assert!(offer.matches_proof(&proof));
    assert!(matches!(
        gate.commit(|| offer.approve(proof)),
        Ok(AdmissionCommit::Committed(()))
    ));
    let result = tokio::time::timeout(Duration::from_secs(4), task)
        .await
        .unwrap()
        .unwrap()
        .unwrap();
    assert_eq!(result.status(), StatusCode::OK);
    assert_eq!(hits.load(Ordering::SeqCst), 2);
    if !delay_first {
        let times = times.lock().unwrap();
        assert!(
            times[1].duration_since(times[0]) >= Duration::from_secs(1),
            "Retry-After was shortened"
        );
    }
}

#[tokio::test]
async fn actual_anthropic_response_headers_timeout_can_continue_same_request() {
    run_real_managed_request(true).await;
}

#[tokio::test]
async fn actual_anthropic_retry_after_waits_for_actor_gate_and_server_hint() {
    run_real_managed_request(false).await;
}