use std::time::Duration;
use allama::mock::{MockProvider, MockStreamingProvider};
use allama::wrappers::{
FallbackProvider, RetryPolicy, RetryingProvider, TimeoutPolicy, TimeoutProvider,
TracingProvider,
};
use allama::{ChatProvider, ChatStreamExt, Error};
use opentelemetry::Value;
use opentelemetry::trace::Status;
use super::otel::export;
use super::request;
pub(super) fn recommended_stack<P, F>(
primary: P,
fallback: F,
first_output: Duration,
) -> impl ChatProvider
where
P: ChatProvider + Send + Sync + 'static,
P::Stream: Send + 'static,
F: ChatProvider + Send + Sync + 'static,
F::Stream: Send + 'static,
{
TracingProvider::new(TimeoutProvider::new(
FallbackProvider::new(
RetryingProvider::new(
TimeoutProvider::new(
primary,
TimeoutPolicy::new()
.stream_setup(Duration::from_secs(60))
.stream_idle(Duration::from_secs(60)),
),
RetryPolicy::new().max_retry_after(Duration::from_secs(5)),
),
fallback,
),
TimeoutPolicy::new().stream_setup(first_output),
))
}
fn rate_limited(hint: Duration) -> Error {
Error::rate_limited("HTTP 429")
.with_status(429)
.with_retry_after(hint)
}
#[tokio::test]
async fn a_long_rate_limit_fails_over_at_once() {
let provider = recommended_stack(
MockStreamingProvider::build(|b| {
b.setup_error(rate_limited(Duration::from_secs(30)))
.setup_error(rate_limited(Duration::from_secs(30)))
})
.with_provider_name("anthropic"),
MockProvider::build(|b| b.text("unary backup").text("streamed backup"))
.with_provider_name("openai"),
Duration::from_secs(60),
);
let started = std::time::Instant::now();
let ((unary, streamed), exported) = export(async {
let unary = provider.chat(&request()).await;
let streamed = provider
.chat_stream(&request())
.await
.unwrap()
.collect_response()
.await;
(unary, streamed)
})
.await;
assert!(started.elapsed() < Duration::from_secs(1));
assert_eq!(unary.unwrap().text().as_deref(), Some("unary backup"));
assert_eq!(streamed.unwrap().text().as_deref(), Some("streamed backup"));
for chat in exported.spans_for("chat") {
assert_eq!(
chat.value("allama.retry.give_up_reason"),
&Value::from("hint_over_ceiling")
);
assert_eq!(chat.value("allama.fallback.used"), &Value::Bool(true));
assert_eq!(
chat.value("allama.response.provider"),
&Value::from("openai")
);
}
}
#[tokio::test]
async fn the_outer_deadline_expiring_during_backoff_is_a_timeout() {
let deadline = Duration::from_millis(100);
let provider = recommended_stack(
MockStreamingProvider::build(|b| {
b.setup_error(rate_limited(Duration::from_millis(400)))
.text("too late")
}),
MockProvider::with_text("never asked"),
deadline,
);
let started = std::time::Instant::now();
let (result, exported) = export(async { provider.chat_stream(&request()).await }).await;
let elapsed = started.elapsed();
let Err(error) = result else {
panic!("the deadline should end the call before its first output");
};
assert!(matches!(error, Error::Timeout { .. }), "{error}");
assert!(
elapsed >= deadline && elapsed < Duration::from_millis(400),
"{elapsed:?}"
);
let chat = exported.span("chat");
assert_eq!(chat.value("error.type"), &Value::from("timeout"));
assert!(
matches!(chat.status(), Status::Error { .. }),
"{:?}",
chat.status()
);
assert!(!chat.has("allama.response.cancelled"));
assert_eq!(chat.value("allama.attempts"), &Value::I64(1));
}
#[tokio::test]
async fn a_redaction_transform_goes_outside_tracing() {
use allama::ChatRequest;
use allama::wrappers::{MapRequestProvider, TracingContentConfig};
fn redact(request: &mut ChatRequest) {
for message in &mut request.messages {
if let allama::Message::User(user) = message {
user.content = "[email]".into();
}
}
}
let request = ChatRequest::new("m").user("mail jane@example.com");
let inner_content = |key: &str, exported: &super::otel::Exported| {
exported.span("chat").value(key).as_str().into_owned()
};
let outside = MapRequestProvider::new(
TracingProvider::new(MockProvider::with_text("ok"))
.with_content_capture(TracingContentConfig::all()),
redact,
);
let (_, exported) = export(async { outside.chat(&request).await }).await;
let recorded = inner_content("gen_ai.input.messages", &exported);
assert!(recorded.contains("[email]"), "{recorded}");
assert!(!recorded.contains("jane@example.com"), "{recorded}");
let inside = TracingProvider::new(MapRequestProvider::new(
MockProvider::with_text("ok"),
redact,
))
.with_content_capture(TracingContentConfig::all());
let (_, exported) = export(async { inside.chat(&request).await }).await;
let recorded = inner_content("gen_ai.input.messages", &exported);
assert!(recorded.contains("jane@example.com"), "{recorded}");
}