use std::sync::Arc;
use super::*;
#[allow(clippy::too_many_arguments)]
pub(super) async fn run_race(
dispatch: &str,
policy: &RoutingPolicyConfig,
attempts_used: usize,
link: &ChainLink,
link_label: &str,
opts: &LlmCallOptions,
bridge: Option<&Arc<crate::bridge::HostBridge>>,
race_after_ms: u64,
primary_timeout_ms: u64,
backup_label: String,
backup_opts: LlmCallOptions,
) -> (
Result<crate::llm::api::LlmResult, VmError>,
Vec<RoutingAttempt>,
TerminalRoute,
) {
fn finalize_terminal(
record: &mut RoutingAttempt,
res: &Result<crate::llm::api::LlmResult, VmError>,
) {
match res {
Ok(v) => {
record.status = AttemptStatus::Succeeded;
record.cost_usd = Some(project_link_cost_usd(v));
record.input_tokens = Some(v.input_tokens);
record.output_tokens = Some(v.output_tokens);
}
Err(_) => {
record.status = AttemptStatus::Failed;
}
}
}
let primary_start = std::time::Instant::now();
let primary_attempt_no = attempts_used + 1;
let backup_attempt_no = attempts_used + 2;
let primary_link = link.clone();
let primary_label = link_label.to_string();
let primary_opts = opts.clone();
let mut primary_future = Box::pin(async move {
let (res, _) = execute_link(&primary_opts, bridge, None).await;
(res, primary_start.elapsed())
});
tokio::select! {
biased;
primary = &mut primary_future => {
let (res, elapsed) = primary;
let mut record = pending_attempt_record(
primary_attempt_no,
&primary_link,
&primary_label,
elapsed,
);
finalize_terminal(&mut record, &res);
(res, vec![record], TerminalRoute::Attempt(primary_attempt_no))
}
_ = crate::clock_mock::sleep(Duration::from_millis(race_after_ms)) => {
let mut race_meta = serde_json::Map::new();
race_meta.insert("policy".to_string(), json!(policy.label.clone()));
race_meta.insert("race_after_ms".to_string(), json!(race_after_ms));
race_meta.insert("primary_label".to_string(), json!(primary_label.clone()));
race_meta.insert("backup_label".to_string(), json!(backup_label.clone()));
emit_routing_event(dispatch, "race_started", race_meta);
let backup_start = std::time::Instant::now();
let backup_link_clone = ChainLink {
provider: backup_opts.provider.clone(),
model: backup_opts.model.clone(),
timeout_ms: link.timeout_ms,
label: Some(backup_label.clone()),
region: backup_opts.region.clone(),
overrides: None,
};
let mut backup_future = Box::pin({
let backup_opts = backup_opts.clone();
async move {
let (res, _) = execute_link(&backup_opts, bridge, None).await;
(res, backup_start.elapsed())
}
});
let primary_deadline = primary_timeout_ms.saturating_add(race_after_ms);
tokio::select! {
biased;
primary = &mut primary_future => {
let (res, elapsed) = primary;
let mut primary_record = pending_attempt_record(
primary_attempt_no,
&primary_link,
&primary_label,
elapsed,
);
finalize_terminal(&mut primary_record, &res);
let mut backup_record = pending_attempt_record(
backup_attempt_no,
&backup_link_clone,
&backup_label,
backup_start.elapsed(),
);
backup_record.status = AttemptStatus::RaceLost;
let mut meta = serde_json::Map::new();
meta.insert("policy".to_string(), json!(policy.label.clone()));
meta.insert("winner".to_string(), json!(primary_label));
meta.insert("loser".to_string(), json!(backup_label));
emit_routing_event(dispatch, "race_won", meta.clone());
let mut lost_meta = meta;
lost_meta.insert("reason".to_string(), json!("primary_finished_first"));
emit_routing_event(dispatch, "race_lost", lost_meta);
(
res,
vec![primary_record, backup_record],
TerminalRoute::Attempt(primary_attempt_no),
)
}
backup = &mut backup_future => {
let (res, elapsed) = backup;
let mut backup_record = pending_attempt_record(
backup_attempt_no,
&backup_link_clone,
&backup_label,
elapsed,
);
finalize_terminal(&mut backup_record, &res);
let mut primary_record = pending_attempt_record(
primary_attempt_no,
&primary_link,
&primary_label,
primary_start.elapsed(),
);
primary_record.status = AttemptStatus::RaceLost;
let mut meta = serde_json::Map::new();
meta.insert("policy".to_string(), json!(policy.label.clone()));
meta.insert("winner".to_string(), json!(backup_label));
meta.insert("loser".to_string(), json!(primary_label));
emit_routing_event(dispatch, "race_won", meta.clone());
let mut lost_meta = meta;
lost_meta.insert("reason".to_string(), json!("backup_finished_first"));
emit_routing_event(dispatch, "race_lost", lost_meta);
(
res,
vec![primary_record, backup_record],
TerminalRoute::Attempt(backup_attempt_no),
)
}
_ = crate::clock_mock::sleep(Duration::from_millis(primary_deadline)) => {
let mut primary_record = pending_attempt_record(
primary_attempt_no,
&primary_link,
&primary_label,
Duration::from_millis(primary_deadline),
);
primary_record.status = AttemptStatus::Failed;
let mut backup_record = pending_attempt_record(
backup_attempt_no,
&backup_link_clone,
&backup_label,
Duration::from_millis(primary_deadline),
);
backup_record.status = AttemptStatus::Failed;
(
Err(runtime_error(
"routing_policy: race exhausted both primary and backup attempts".to_string(),
)),
vec![primary_record, backup_record],
TerminalRoute::Composite,
)
}
}
}
}
}
#[cfg(test)]
mod race_terminal_tests {
use super::*;
use crate::llm::api::options::base_opts;
use crate::llm::fake::{install_fake_llm_script, FakeLlmScript, FakeLlmTurn};
use crate::value::ErrorCategory;
use std::time::Duration;
fn link(model: &str) -> ChainLink {
ChainLink {
provider: "fake".to_string(),
model: model.to_string(),
timeout_ms: None,
label: Some(model.to_string()),
region: None,
overrides: None,
}
}
fn opts(model: &str) -> LlmCallOptions {
let mut opts = base_opts("fake");
opts.model = model.to_string();
opts
}
fn policy() -> std::sync::Arc<RoutingPolicyConfig> {
crate::llm::routing::linear_failover_policy(
"test".to_string(),
vec![link("fake-primary"), link("fake-backup")],
false,
)
}
fn paused_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.start_paused(true)
.build()
.expect("paused runtime")
}
async fn drive(
race_after_ms: u64,
primary_timeout_ms: u64,
) -> (
Result<crate::llm::api::LlmResult, VmError>,
Vec<RoutingAttempt>,
TerminalRoute,
) {
let policy = policy();
let primary_link = link("fake-primary");
run_race(
"test",
&policy,
0,
&primary_link,
"fake-primary",
&opts("fake-primary"),
None,
race_after_ms,
primary_timeout_ms,
"fake-backup".to_string(),
opts("fake-backup"),
)
.await
}
#[test]
fn backup_wins_race_marks_terminal_as_backup() {
let runtime = paused_runtime();
let _guard = install_fake_llm_script(
FakeLlmScript::new()
.push(FakeLlmTurn::Stalled(Duration::from_mins(1)))
.push(FakeLlmTurn::error(
ErrorCategory::CircuitOpen,
"backup failed first",
)),
);
let (result, records, terminal) = runtime.block_on(drive(10, 50));
assert!(result.is_err(), "backup errored, so the race result is Err");
assert_eq!(
terminal,
TerminalRoute::Attempt(2),
"terminal route names the backup attempt"
);
assert_eq!(records.len(), 2);
assert_eq!(records[0].model, "fake-primary");
assert!(matches!(records[0].status, AttemptStatus::RaceLost));
assert_eq!(records[1].model, "fake-backup");
assert!(matches!(records[1].status, AttemptStatus::Failed));
}
#[test]
fn dual_deadline_marks_composite() {
let runtime = paused_runtime();
let _guard = install_fake_llm_script(
FakeLlmScript::new()
.push(FakeLlmTurn::Stalled(Duration::from_mins(1)))
.push(FakeLlmTurn::Stalled(Duration::from_mins(1))),
);
let (result, records, terminal) = runtime.block_on(drive(10, 20));
assert!(result.is_err(), "an exhausted race returns Err");
assert_eq!(terminal, TerminalRoute::Composite);
assert_eq!(records.len(), 2);
assert!(matches!(records[0].status, AttemptStatus::Failed));
assert!(matches!(records[1].status, AttemptStatus::Failed));
}
#[test]
fn primary_terminal_marks_attempt_one() {
let runtime = paused_runtime();
let _guard = install_fake_llm_script(FakeLlmScript::new().push(FakeLlmTurn::error(
ErrorCategory::CircuitOpen,
"primary failed",
)));
let (result, records, terminal) = runtime.block_on(drive(10, 50));
assert!(result.is_err());
assert_eq!(terminal, TerminalRoute::Attempt(1));
assert_eq!(records.len(), 1);
assert_eq!(records[0].model, "fake-primary");
assert!(matches!(records[0].status, AttemptStatus::Failed));
}
}