//! Multi-tier orchestration oracle.
//!
//! Everything else proves a *tier* works. This proves the **cascade**
//! works: a `jouleclaw_cascade::Runtime` with an auto-prepended L0 cache
//! and a registered `PrismTier(Bonsai)` routes a query end-to-end,
//! and a *repeat* of the same query is served from L0 at
//! picojoule cost instead of re-running the 1.8B model.
//!
//! This is the thesis in one test: the runtime is the AI, the model
//! is a peripheral the cascade reaches for only when the cheaper
//! tiers can't answer — and never twice for the same question.
//!
//! Gate:
//! * 1st query: tier_used = L3 (Bonsai), measured joules > 1 mJ,
//! answer contains "Paris".
//! * 2nd identical query: tier_used = L0, joules ≪ 1st (≥1000×
//! cheaper), identical answer text.
//!
//! `#[ignore]` — needs the model on disk.
use jouleclaw_cascade::{
AnswerOutput, Cascade, ContextRef, JouleBudget, QualityFloor,
Query, QueryInput, Runtime, TierId,
};
use jouleclaw_prism::PrismTier;
use std::time::Instant;
fn model_path() -> String {
std::env::var("JOULE_BONSAI")
.unwrap_or_else(|_| "../../models/ternary-bonsai-1.7b-q2_0.gguf".to_string())
}
fn query(text: &str) -> Query {
Query {
input: QueryInput::Text(text.to_string()),
// `expensive()` (100 J) instead of `standard()` (1 J) because
// Bonsai-1.7B in thinking mode genuinely sits on the L3/L4
// boundary: prompt prefill costs ~430 mJ alone, and a natural-
// length answer with `` markers adds another
// ~700 mJ to reach "Paris" — total ~1.1 J. The Runtime's
// budget guard would reject this against the 1 J limit even
// though the answer is correct, so we route through a budget
// category that fits. This is data-driven: it's what the
// calibration ledger learns after one dispatch.
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
#[test]
#[ignore]
fn cascade_routes_l3_then_l0_on_repeat() {
let path = model_path();
eprintln!("loading {}", path);
let t0 = Instant::now();
// 16 tokens: Bonsai is a thinking-mode reasoner; it emits
// ... markers before the answer. 6 tokens (prior
// cap) leaves no room for "Paris". 16 keeps the *measured* cost
// (~1.25× the static estimate at 43 mJ/tok) under
// `JouleBudget::standard()`'s 1 J ceiling, so the Runtime's
// budget guard accepts the answer.
let tier = PrismTier::from_gguf(7, &path, 128)
.expect("PrismTier::from_gguf")
.with_max_new_tokens(16);
eprintln!(" loaded in {:?}", t0.elapsed());
// Cascade with just the one L3 tier; Runtime::new prepends an L0.
let mut cascade = Cascade::new();
cascade.register(Box::new(tier));
let mut runtime = Runtime::new(cascade);
let q = query("The capital of France is");
// ---- 1st dispatch: L0 miss → L3 (Bonsai) ----
let t1 = Instant::now();
let a1 = runtime.answer(q.clone()).expect("answer #1");
let wall1 = t1.elapsed();
let txt1 = match &a1.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text, got {:?}", other),
};
eprintln!(" #1 tier={:?} joules={:.3} mJ wall={:?} {:?}",
a1.tier_used, a1.joules_spent * 1e3, wall1, txt1);
assert_eq!(a1.tier_used, TierId::L3(jouleclaw_cascade::L3ModelId(7)),
"first dispatch must reach the Bonsai L3 tier");
assert!(txt1.to_lowercase().contains("paris"),
"L3 answer must contain 'Paris', got {:?}", txt1);
assert!(a1.joules_spent > 1e-3,
"L3 measured joules should be model-scale (>1 mJ), got {} J",
a1.joules_spent);
// ---- 2nd dispatch: identical query → L0 cache hit ----
let t2 = Instant::now();
let a2 = runtime.answer(q.clone()).expect("answer #2");
let wall2 = t2.elapsed();
let txt2 = match &a2.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text on cache hit, got {:?}", other),
};
eprintln!(" #2 tier={:?} joules={:.3e} J wall={:?} {:?}",
a2.tier_used, a2.joules_spent, wall2, txt2);
assert_eq!(a2.tier_used, TierId::L0,
"second identical query must be served from the L0 cache");
assert_eq!(txt2, txt1, "cache must return the exact same answer text");
assert!(a2.joules_spent > 0.0 && a2.joules_spent < a1.joules_spent / 1000.0,
"L0 hit must be ≥1000× cheaper than the L3 run: L0={} J vs L3={} J",
a2.joules_spent, a1.joules_spent);
// The cache hit should also be dramatically faster in wall-clock.
assert!(wall2 * 50 < wall1,
"L0 wall {:?} should be ≫ faster than L3 wall {:?}", wall2, wall1);
let energy_ratio = a1.joules_spent / a2.joules_spent;
eprintln!("VERDICT: cascade routed L3 then L0 on repeat. \
Energy: L3 {:.1} mJ → L0 {:.3e} J ({:.0}× cheaper). \
Wall: {:?} → {:?}.",
a1.joules_spent * 1e3, a2.joules_spent, energy_ratio, wall1, wall2);
}