use super::*;
use crate::frontend::generation_gate::{GenerationGate, register};
use std::sync::atomic::AtomicBool;
struct TestGate {
prefilled: tokio::sync::Notify,
released: AtomicBool,
expired: AtomicBool,
input: AtomicUsize,
output: AtomicU64,
}
impl GenerationGate for TestGate {
fn after_prefill(&self, input: usize, _: u32) -> OpenAiResult<()> {
self.input.store(input, Ordering::Release);
self.prefilled.notify_one();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while !self.released.load(Ordering::Acquire) {
if std::time::Instant::now() > deadline {
return Err(OpenAiError::backend("test approval timed out"));
}
std::thread::sleep(Duration::from_millis(10));
}
if self.expired.load(Ordering::Acquire) {
return Err(OpenAiError::backend("test invoice expired"));
}
Ok(())
}
fn committed_token(&self) -> OpenAiResult<()> {
self.output.fetch_add(1, Ordering::AcqRel);
Ok(())
}
fn committed_tokens(&self) -> u64 {
self.output.load(Ordering::Acquire)
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "requires MESH_PAYMENT_TEST_MODEL and a CPU native runtime bundle"]
async fn payments_real_model_prefills_before_gate_and_streams_usage_after_release() -> Result<()> {
#[cfg(feature = "dynamic-native-runtime")]
{
let directory = PathBuf::from(std::env::var("MESH_PAYMENT_TEST_RUNTIME")?);
let manifest: Value = serde_json::from_slice(&fs::read(directory.join("manifest.json"))?)?;
let libraries = manifest["runtime"]["libraries"]
.as_array()
.context("runtime library list")?
.iter()
.map(|path| {
path.as_str()
.map(|p| directory.join(p))
.context("runtime library path")
})
.collect::<Result<Vec<_>>>()?;
unsafe {
skippy_runtime::load_native_runtime_libraries(libraries)?;
}
}
let path = std::env::var("MESH_PAYMENT_TEST_MODEL")?;
let config = StageConfig {
model_id: "payment-smoke".into(),
model_path: Some(path),
layer_start: 0,
layer_end: 30,
ctx_size: 512,
lane_count: 1,
n_gpu_layers: 0,
load_mode: LoadMode::RuntimeSlice,
..StageConfig::default()
};
let backend = Arc::new(support::local_openai_backend(config, "mm-smoke")?);
let gate = Arc::new(TestGate {
prefilled: tokio::sync::Notify::new(),
released: AtomicBool::new(false),
expired: AtomicBool::new(false),
input: AtomicUsize::new(0),
output: AtomicU64::new(0),
});
let id = [47; 16];
let _registration = register(id, gate.clone())?;
let generating_backend = backend.clone();
let generation =
tokio::spawn(async move { collect_completion(generating_backend.as_ref(), id).await });
tokio::time::timeout(Duration::from_secs(30), gate.prefilled.notified()).await?;
assert!(gate.input.load(Ordering::Acquire) > 0);
assert_eq!(gate.output.load(Ordering::Acquire), 0);
assert!(!generation.is_finished());
gate.released.store(true, Ordering::Release);
let (text, tokens) = tokio::time::timeout(Duration::from_secs(30), generation).await???;
assert!(!text.is_empty());
assert!(tokens > 0 && tokens <= 8);
assert_eq!(u64::from(tokens), gate.output.load(Ordering::Acquire));
let expired = Arc::new(TestGate {
prefilled: tokio::sync::Notify::new(),
released: AtomicBool::new(false),
expired: AtomicBool::new(true),
input: AtomicUsize::new(0),
output: AtomicU64::new(0),
});
let expired_id = [48; 16];
let _expired_registration = register(expired_id, expired.clone())?;
let expiring_backend = backend.clone();
let generation =
tokio::spawn(
async move { collect_completion(expiring_backend.as_ref(), expired_id).await },
);
tokio::time::timeout(Duration::from_secs(30), expired.prefilled.notified()).await?;
assert_eq!(expired.output.load(Ordering::Acquire), 0);
expired.released.store(true, Ordering::Release);
assert!(
tokio::time::timeout(Duration::from_secs(30), generation)
.await??
.is_err()
);
assert_eq!(expired.output.load(Ordering::Acquire), 0);
let (text, tokens) = tokio::time::timeout(
Duration::from_secs(30),
collect_completion(backend.as_ref(), [49; 16]),
)
.await??;
assert!(!text.is_empty());
assert!(tokens > 0 && tokens <= 8);
Ok(())
}
async fn collect_completion(backend: &impl OpenAiBackend, id: [u8; 16]) -> Result<(String, u32)> {
use futures_util::StreamExt;
let request: CompletionRequest = serde_json::from_value(
json!({"model":"mm-smoke","prompt":"The sun is","max_tokens":8,"stream":true,"stream_options":{"include_usage":true},"temperature":0}),
)?;
let context = OpenAiRequestContext::with_request_id(uuid::Uuid::from_bytes(id).into());
let mut stream = backend.completion_stream(request, context).await?;
let mut tokens = 0;
let mut text = String::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
for choice in chunk.choices {
text.push_str(&choice.text);
}
if let Some(usage) = chunk.usage {
tokens = usage.completion_tokens;
}
}
Ok((text, tokens))
}