1use mesh_llm_plugin::{
2 HostInferenceRequest, MeshVisibility, PluginContext, PluginMetadata, PluginRuntime,
3 SimplePlugin, VirtualModelInvocation, VirtualModelResponse, VirtualModelRouter,
4 operation_with_schema, plugin_server_info, structured_tool_result, virtual_model,
5};
6use mesh_mixture_of_agents as moa;
7use serde_json::{Value, json};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::Duration;
11use tokio::time::Instant;
12
13pub const PLUGIN_ID: &str = "mesh-moa";
14pub const HANDLER: &str = "chat";
15
16const SMALL_TIER_MAX_B: f64 = moa::SMALL_TIER_MAX_B;
20
21const COMMITTEE_CAP: usize = 4;
24
25pub const PROGRESS_LINES: [&str; 6] = [
29 "Routing through mesh…",
30 "Querying peer models…",
31 "Comparing responses…",
32 "Waiting on a slow peer…",
33 "Still gathering responses…",
34 "Hold on, this one's taking a moment…",
35];
36
37pub async fn run(stream: mesh_llm_plugin::LocalStream) -> anyhow::Result<()> {
38 PluginRuntime::run_with_stream(plugin(), stream).await
39}
40
41struct PatienceProfile {
43 first_answer_grace: Duration,
44 strong_patience: Duration,
45}
46
47fn patience_profile(public_mesh: bool) -> PatienceProfile {
59 if public_mesh {
60 PatienceProfile {
61 first_answer_grace: Duration::from_millis(1500),
63 strong_patience: Duration::from_secs(8),
66 }
67 } else {
68 PatienceProfile {
69 first_answer_grace: Duration::from_secs(10),
70 strong_patience: Duration::from_secs(20),
71 }
72 }
73}
74
75pub fn plugin() -> SimplePlugin {
76 let public_mesh = Arc::new(AtomicBool::new(false));
77 let manifest = mesh_llm_plugin::plugin_manifest![
78 virtual_model(moa::VIRTUAL_MODEL_NAME, HANDLER)
79 .supports_tools(true)
80 .supports_streaming(true)
81 .requires_candidates(true)
82 .input_modalities(["text", "image", "audio"])
83 .progress_lines(PROGRESS_LINES)
89 ];
90 let mut router = VirtualModelRouter::new();
91 let turn_public_mesh = Arc::clone(&public_mesh);
92 router.add_raw(
93 operation_with_schema(HANDLER, "Run a Mesh MoA turn", serde_json::Map::new()),
94 move |request, context| {
95 let context = context.owned();
96 let public_mesh = Arc::clone(&turn_public_mesh);
97 Box::pin(async move {
98 let invocation: VirtualModelInvocation = request.arguments()?;
99 let response =
100 handle(invocation, context, public_mesh.load(Ordering::Relaxed)).await;
101 structured_tool_result(response)
102 })
103 },
104 );
105 SimplePlugin::new(PluginMetadata::new(
106 PLUGIN_ID,
107 env!("CARGO_PKG_VERSION"),
108 plugin_server_info(
109 PLUGIN_ID,
110 env!("CARGO_PKG_VERSION"),
111 "Mesh MoA",
112 "Built-in mixture-of-agents virtual model",
113 None::<String>,
114 ),
115 ))
116 .with_manifest(manifest)
117 .with_virtual_model_router(router)
118 .on_initialize(move |request, _context| {
119 public_mesh.store(
120 request.mesh_visibility == MeshVisibility::Public,
121 Ordering::Relaxed,
122 );
123 Box::pin(async { Ok(()) })
124 })
125}
126
127async fn handle(
128 mut invocation: VirtualModelInvocation,
129 context: PluginContext<'static>,
130 public_mesh: bool,
131) -> VirtualModelResponse {
132 if let Some(message) = chat_request_rejection(&invocation.request) {
139 return error_response(400, message);
140 }
141 if invocation.candidates.is_empty() {
142 return error_response(503, "no concrete models available in the mesh");
143 }
144 let requested_stream = invocation
145 .request
146 .get("stream")
147 .and_then(Value::as_bool)
148 .unwrap_or(false);
149 if let Some(object) = invocation.request.as_object_mut() {
150 object.remove("stream");
151 }
152
153 let (needs_vision, needs_audio) = match select_request_candidates(&mut invocation) {
154 Ok(media) => media,
155 Err(response) => return response,
156 };
157 let all_small = if needs_vision || needs_audio {
160 invocation.candidates.sort_by(candidate_rank);
161 false
162 } else {
163 apply_pool_policy(&mut invocation.candidates)
164 };
165 if all_small || invocation.candidates.len() == 1 || needs_vision || needs_audio {
172 return direct_capability_response(invocation, context, requested_stream).await;
173 }
174
175 let mut backends: Vec<Arc<dyn moa::ModelBackend>> = Vec::new();
176 let mut models = Vec::new();
177 for candidate in invocation.candidates {
178 let index = backends.len();
179 backends.push(Arc::new(HostBackend {
180 context: context.clone(),
181 target_node_id: candidate.target_node_id.clone(),
182 }));
183 models.push(
184 moa::ModelEntry::new(candidate.model_id, index)
185 .with_parameter_count_b(candidate.parameter_count_b),
186 );
187 }
188 let actor_candidates = actor_candidates(&models);
189 let patience = patience_profile(public_mesh);
190 let config = moa::GatewayConfig {
191 backends,
192 models,
193 worker_timeout: Duration::from_secs(60),
194 reducer_timeout: Duration::from_secs(60),
195 hedge_delay: Duration::from_secs(5),
196 first_answer_grace: patience.first_answer_grace,
197 strong_patience: patience.strong_patience,
198 enable_thinking: Some(false),
199 actor_candidates,
200 reference_policy: moa::ReferencePolicy::Auto,
201 refinement_policy: moa::RefinementPolicy::Auto,
202 };
203 let mut result = moa::handle_turn(&config, &invocation.request).await;
204 strip_response_thinking(&mut result.response_body);
205 let workers_ok = result
206 .worker_summaries
207 .iter()
208 .filter(|w| w.succeeded)
209 .count();
210 let headers = vec![
211 ("x-moa-elapsed-ms".into(), result.elapsed_ms.to_string()),
212 ("x-moa-turn".into(), result.turn_kind.label().to_string()),
213 (
214 "x-moa-workers".into(),
215 result.worker_summaries.len().to_string(),
216 ),
217 ("x-moa-workers-ok".into(), workers_ok.to_string()),
218 ("x-moa-reducer".into(), result.reducer_used.to_string()),
219 (
220 "x-moa-reducer-attempts".into(),
221 result.reducer_attempts.to_string(),
222 ),
223 ];
224 VirtualModelResponse {
225 status_code: if is_failure_body(&result.response_body) {
226 502
227 } else {
228 200
229 },
230 body: result.response_body,
231 headers,
232 event_stream: requested_stream,
233 }
234}
235
236fn select_request_candidates(
240 invocation: &mut VirtualModelInvocation,
241) -> Result<(bool, bool), VirtualModelResponse> {
242 let needs_vision = contains_any_key(&invocation.request, &["image_url", "input_image"]);
243 let needs_audio = contains_any_key(
244 &invocation.request,
245 &["audio_url", "input_audio", "input_audio_buffer"],
246 );
247 let needs_tools = invocation.request.get("tools").is_some_and(|tools| {
248 !tools.is_null() && tools.as_array().is_none_or(|items| !items.is_empty())
249 });
250 invocation.candidates.retain(|candidate| {
251 (!needs_vision || candidate.supports_vision) && (!needs_audio || candidate.supports_audio)
252 });
253 if invocation.candidates.is_empty() {
254 return Err(error_response(
255 422,
256 "no concrete model satisfies the request capabilities",
257 ));
258 }
259 if needs_tools
260 && invocation
261 .candidates
262 .iter()
263 .any(|candidate| candidate.supports_tools)
264 {
265 invocation
266 .candidates
267 .retain(|candidate| candidate.supports_tools);
268 }
269 Ok((needs_vision, needs_audio))
270}
271
272async fn direct_capability_response(
273 invocation: VirtualModelInvocation,
274 context: PluginContext<'static>,
275 requested_stream: bool,
276) -> VirtualModelResponse {
277 direct_with_fallback(
278 invocation,
279 requested_stream,
280 Duration::from_secs(60),
281 |request| context.infer(request),
282 )
283 .await
284}
285
286async fn direct_with_fallback<F, Fut>(
287 mut invocation: VirtualModelInvocation,
288 requested_stream: bool,
289 budget: Duration,
290 mut infer: F,
291) -> VirtualModelResponse
292where
293 F: FnMut(HostInferenceRequest) -> Fut,
294 Fut: std::future::Future<Output = anyhow::Result<mesh_llm_plugin::HostInferenceResponse>>,
295{
296 invocation.candidates.sort_by(candidate_rank);
297 let deadline = Instant::now() + budget;
298 let mut last_failure = None;
299 for candidate in &invocation.candidates {
300 let remaining = deadline.saturating_duration_since(Instant::now());
301 if remaining.is_zero() {
302 break;
303 }
304 let request = HostInferenceRequest {
305 model_id: candidate.model_id.clone(),
306 target_node_id: candidate.target_node_id.clone(),
307 request: invocation.request.clone(),
308 timeout_ms: Some(remaining.as_millis().min(u64::MAX as u128) as u64),
309 };
310 let result = tokio::time::timeout(remaining, infer(request)).await;
311 let response = match result {
312 Ok(Ok(response)) => response,
313 Ok(Err(error)) => {
314 last_failure = Some(error_response(
315 502,
316 &format!("capability route failed: {error}"),
317 ));
318 continue;
319 }
320 Err(_) => {
321 last_failure = Some(error_response(504, "capability route timed out"));
322 break;
323 }
324 };
325 if matches!(response.status_code, 502..=504) {
326 last_failure = Some(direct_response(response, requested_stream));
327 continue;
328 }
329 return direct_response(response, requested_stream);
330 }
331 last_failure.unwrap_or_else(|| error_response(504, "capability route timed out"))
332}
333
334fn direct_response(
335 mut response: mesh_llm_plugin::HostInferenceResponse,
336 requested_stream: bool,
337) -> VirtualModelResponse {
338 normalize_direct_response_body(&mut response.body);
339 VirtualModelResponse {
340 status_code: response.status_code,
341 body: response.body,
342 headers: response
343 .served_by
344 .map(|served_by| vec![("x-mesh-served-by".into(), served_by)])
345 .unwrap_or_default(),
346 event_stream: requested_stream,
347 }
348}
349
350fn chat_request_rejection(request: &Value) -> Option<&'static str> {
353 match request.get("messages") {
354 Some(Value::Array(messages)) if !messages.is_empty() => None,
355 _ => Some("MoA requires a non-empty `messages` array"),
356 }
357}
358
359fn contains_any_key(value: &Value, keys: &[&str]) -> bool {
360 match value {
361 Value::Object(object) => object
362 .iter()
363 .any(|(key, value)| keys.contains(&key.as_str()) || contains_any_key(value, keys)),
364 Value::Array(values) => values.iter().any(|value| contains_any_key(value, keys)),
365 _ => false,
366 }
367}
368
369fn is_failure_body(body: &Value) -> bool {
370 body.get("error").is_some()
371 || body
372 .pointer("/choices/0/finish_reason")
373 .and_then(Value::as_str)
374 == Some("error")
375}
376
377fn strip_response_thinking(body: &mut Value) {
378 let Some(content) = body
379 .pointer_mut("/choices/0/message/content")
380 .and_then(|value| value.as_str())
381 .map(moa::strip_thinking)
382 else {
383 return;
384 };
385 body["choices"][0]["message"]["content"] = Value::String(content);
386}
387
388fn normalize_direct_response_body(body: &mut Value) {
389 strip_response_thinking(body);
390 if body.is_object() {
391 body["model"] = Value::String(moa::VIRTUAL_MODEL_NAME.into());
392 }
393}
394
395fn actor_candidates(models: &[moa::ModelEntry]) -> Vec<usize> {
396 let mut indices = (0..models.len()).collect::<Vec<_>>();
397 indices.sort_by(|left, right| {
398 models[*right]
399 .parameter_count_b
400 .partial_cmp(&models[*left].parameter_count_b)
401 .unwrap_or(std::cmp::Ordering::Equal)
402 });
403 indices
404}
405
406fn apply_pool_policy(candidates: &mut Vec<mesh_llm_plugin::VirtualModelCandidate>) -> bool {
410 candidates.sort_by(candidate_rank);
411 let all_small = candidates.iter().all(|candidate| {
412 candidate
413 .parameter_count_b
414 .is_none_or(|size| size < SMALL_TIER_MAX_B)
415 });
416 if all_small {
417 } else {
420 candidates.truncate(COMMITTEE_CAP);
421 }
422 all_small
423}
424
425fn candidate_rank(
426 left: &mesh_llm_plugin::VirtualModelCandidate,
427 right: &mesh_llm_plugin::VirtualModelCandidate,
428) -> std::cmp::Ordering {
429 left.deprioritized
430 .cmp(&right.deprioritized)
431 .then_with(|| {
432 right
433 .parameter_count_b
434 .partial_cmp(&left.parameter_count_b)
435 .unwrap_or(std::cmp::Ordering::Equal)
436 })
437 .then_with(|| left.model_id.cmp(&right.model_id))
438 .then_with(|| left.target_node_id.cmp(&right.target_node_id))
439}
440
441fn error_response(status_code: u16, message: &str) -> VirtualModelResponse {
442 VirtualModelResponse {
443 status_code,
444 body: json!({"error": {"message": message, "type": "virtual_model_error"}}),
445 headers: Vec::new(),
446 event_stream: false,
447 }
448}
449
450struct HostBackend {
451 context: PluginContext<'static>,
452 target_node_id: Option<String>,
453}
454
455#[async_trait::async_trait]
456impl moa::ModelBackend for HostBackend {
457 async fn chat_completion(
458 &self,
459 model: &str,
460 messages: &[Value],
461 tools: Option<&Value>,
462 max_tokens: u32,
463 timeout: Duration,
464 sampling: moa::SamplingParams,
465 ) -> Result<Value, String> {
466 let mut request = json!({
467 "model": model,
468 "messages": messages,
469 "max_tokens": max_tokens,
470 "temperature": sampling.temperature,
471 "top_p": sampling.top_p,
472 "stream": false,
473 });
474 if let Some(tools) = tools {
475 request["tools"] = tools.clone();
476 }
477 moa::apply_enable_thinking(&mut request, sampling.enable_thinking);
478 let response = self
479 .context
480 .infer(HostInferenceRequest {
481 model_id: model.to_string(),
482 target_node_id: self.target_node_id.clone(),
483 request,
484 timeout_ms: Some(timeout.as_millis().min(u64::MAX as u128) as u64),
485 })
486 .await
487 .map_err(|error| error.to_string())?;
488 if !(200..300).contains(&response.status_code) {
489 return Err(format!("HTTP {}: {}", response.status_code, response.body));
490 }
491 Ok(response.body)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use mesh_llm_plugin::Plugin;
499
500 fn candidate(
501 model_id: &str,
502 parameter_count_b: Option<f64>,
503 target_node_id: &str,
504 ) -> mesh_llm_plugin::VirtualModelCandidate {
505 mesh_llm_plugin::VirtualModelCandidate {
506 model_id: model_id.into(),
507 target_node_id: Some(target_node_id.into()),
508 parameter_count_b,
509 context_length: Some(32_768),
510 deprioritized: false,
511 supports_tools: true,
512 supports_vision: false,
513 supports_audio: false,
514 }
515 }
516
517 #[test]
518 fn plugin_declares_mesh_virtual_model() {
519 let manifest = plugin().manifest().expect("manifest");
520 assert_eq!(manifest.virtual_models.len(), 1);
521 assert_eq!(manifest.virtual_models[0].model_id, "mesh");
522 assert_eq!(manifest.virtual_models[0].handler, HANDLER);
523 assert_eq!(
524 manifest.virtual_models[0].input_modalities,
525 ["text", "image", "audio"]
526 );
527 assert!(manifest.virtual_models[0].requires_candidates);
528 }
529
530 #[test]
531 fn rejects_malformed_messages_before_dispatch() {
532 for request in [
533 json!({}),
534 json!({"messages": []}),
535 json!({"messages": "hi"}),
536 ] {
537 assert_eq!(
538 chat_request_rejection(&request),
539 Some("MoA requires a non-empty `messages` array"),
540 "unexpected contract decision for {request}"
541 );
542 }
543 assert_eq!(
544 chat_request_rejection(&json!({
545 "messages": [{"role": "user", "content": "hi"}]
546 })),
547 None
548 );
549 }
550
551 #[test]
552 fn public_mesh_waits_less_for_a_better_answer() {
553 let public = patience_profile(true);
554 let private = patience_profile(false);
555 assert!(
556 public.first_answer_grace < private.first_answer_grace,
557 "a public mesh must not hold a usable answer for the trusted-mesh grace"
558 );
559 assert!(
560 public.strong_patience < private.strong_patience,
561 "a public mesh must not wait out a long tail for a strong peer"
562 );
563 assert_eq!(public.first_answer_grace, Duration::from_millis(1500));
564 assert_eq!(public.strong_patience, Duration::from_secs(8));
565 assert_eq!(private.first_answer_grace, Duration::from_secs(10));
566 assert_eq!(private.strong_patience, Duration::from_secs(20));
567 }
568
569 #[test]
570 fn detects_nested_media_inputs() {
571 let request = json!({
572 "messages": [{
573 "role": "user",
574 "content": [{"type": "image_url", "image_url": {"url": "data:image/png;base64,x"}}]
575 }]
576 });
577 assert!(contains_any_key(&request, &["image_url", "input_image"]));
578 assert!(!contains_any_key(&request, &["audio_url", "input_audio"]));
579 }
580
581 #[test]
582 fn strips_thinking_and_detects_error_shapes() {
583 let mut response = json!({
584 "choices": [{
585 "finish_reason": "stop",
586 "message": {"content": "<think>private</think>answer"}
587 }]
588 });
589 strip_response_thinking(&mut response);
590 assert_eq!(response["choices"][0]["message"]["content"], "answer");
591 assert!(!is_failure_body(&response));
592
593 response["choices"][0]["finish_reason"] = Value::String("error".into());
594 assert!(is_failure_body(&response));
595 }
596
597 #[test]
598 fn direct_response_uses_virtual_identity_after_thinking_is_removed() {
599 let mut response = json!({
600 "model": "worker-a",
601 "choices": [{
602 "finish_reason": "stop",
603 "message": {"content": "<think>private</think>answer"}
604 }]
605 });
606
607 normalize_direct_response_body(&mut response);
608
609 assert_eq!(response["model"], moa::VIRTUAL_MODEL_NAME);
610 assert_eq!(response["choices"][0]["message"]["content"], "answer");
611 }
612
613 #[test]
614 fn all_small_pool_routes_to_its_best_member() {
615 let mut candidates = vec![
616 candidate("small-a", Some(8.0), "a"),
617 candidate("small-b", Some(9.0), "b"),
618 candidate("unknown", None, "c"),
619 ];
620
621 assert!(apply_pool_policy(&mut candidates));
622
623 assert_eq!(
624 candidates.len(),
625 3,
626 "physical standbys must survive selection"
627 );
628 assert_eq!(candidates[0].model_id, "small-b");
629 }
630
631 fn invocation(
632 candidates: Vec<mesh_llm_plugin::VirtualModelCandidate>,
633 ) -> VirtualModelInvocation {
634 VirtualModelInvocation {
635 request: json!({"messages": [{"role": "user", "content": "hi"}]}),
636 candidates,
637 response_adapter: String::new(),
638 }
639 }
640
641 fn tools_invocation(
642 candidates: Vec<mesh_llm_plugin::VirtualModelCandidate>,
643 ) -> VirtualModelInvocation {
644 let mut invocation = invocation(candidates);
645 invocation.request["tools"] = json!([{
646 "type": "function",
647 "function": {"name": "report_status", "parameters": {"type": "object"}}
648 }]);
649 invocation
650 }
651
652 #[tokio::test]
653 async fn tools_without_advertised_support_reach_inference() {
654 let mut worker = candidate("qwen", Some(27.0), "local");
655 worker.supports_tools = false;
656 let mut invocation = tools_invocation(vec![worker]);
657 assert_eq!(
658 select_request_candidates(&mut invocation).unwrap(),
659 (false, false)
660 );
661 let original_request = invocation.request.clone();
662 let response =
663 direct_with_fallback(invocation, false, Duration::from_secs(1), move |request| {
664 assert_eq!(request.model_id, "qwen");
665 assert_eq!(request.request, original_request);
666 async {
667 Ok(mesh_llm_plugin::HostInferenceResponse {
668 status_code: 200,
669 body: json!({"choices": [{"message": {"tool_calls": [{
670 "type": "function",
671 "function": {"name": "report_status", "arguments": "{}"}
672 }]}}]}),
673 served_by: Some("local".into()),
674 })
675 }
676 })
677 .await;
678 assert_eq!(response.status_code, 200);
679 assert_eq!(
680 response.body["choices"][0]["message"]["tool_calls"][0]["function"]["name"],
681 "report_status"
682 );
683 }
684
685 #[test]
686 fn tools_prefer_advertised_support_over_larger_unknown_models() {
687 let mut unknown = candidate("unknown", Some(70.0), "a");
688 unknown.supports_tools = false;
689 let mut invocation = tools_invocation(vec![unknown, candidate("tools", Some(8.0), "b")]);
690 select_request_candidates(&mut invocation).unwrap();
691 apply_pool_policy(&mut invocation.candidates);
692 assert_eq!(invocation.candidates.len(), 1);
693 assert_eq!(invocation.candidates[0].model_id, "tools");
694 }
695
696 #[test]
697 fn absent_null_or_empty_tools_do_not_filter_candidates() {
698 for tools in [None, Some(Value::Null), Some(json!([]))] {
699 let mut unknown = candidate("unknown", Some(70.0), "a");
700 unknown.supports_tools = false;
701 let mut invocation = invocation(vec![unknown, candidate("tools", Some(8.0), "b")]);
702 if let Some(tools) = tools {
703 invocation.request["tools"] = tools;
704 }
705 select_request_candidates(&mut invocation).unwrap();
706 assert_eq!(invocation.candidates.len(), 2);
707 }
708 }
709
710 #[test]
711 fn tools_fallback_never_relaxes_media_requirements() {
712 for key in ["image_url", "input_audio"] {
713 let mut worker = candidate("text", Some(27.0), "a");
714 worker.supports_tools = false;
715 let mut invocation = tools_invocation(vec![worker]);
716 invocation.request["messages"][0]["content"] = json!([{key: {"url": "fixture"}}]);
717 let response = select_request_candidates(&mut invocation).unwrap_err();
718 assert_eq!(response.status_code, 422);
719 }
720 }
721
722 #[test]
723 fn tools_preference_is_computed_after_media_filtering() {
724 for key in ["image_url", "input_audio"] {
725 let text = candidate("text-tools", Some(70.0), "a");
726 let mut media = candidate("media", Some(27.0), "b");
727 media.supports_tools = false;
728 media.supports_vision = true;
729 media.supports_audio = true;
730 let mut invocation = tools_invocation(vec![text, media]);
731 invocation.request["messages"][0]["content"] = json!([{key: {"url": "fixture"}}]);
732 select_request_candidates(&mut invocation).unwrap();
733 assert_eq!(invocation.candidates.len(), 1);
734 assert_eq!(invocation.candidates[0].model_id, "media");
735 }
736 }
737
738 #[tokio::test]
739 async fn direct_route_retries_a_failed_or_disappeared_replica() {
740 for first_fails_with_response in [true, false] {
741 let attempts = Arc::new(std::sync::Mutex::new(Vec::new()));
742 let seen = Arc::clone(&attempts);
743 let response = direct_with_fallback(
744 invocation(vec![
745 candidate("small", Some(8.0), "a"),
746 candidate("small", Some(8.0), "b"),
747 ]),
748 false,
749 Duration::from_secs(1),
750 move |request| {
751 let seen = Arc::clone(&seen);
752 async move {
753 seen.lock().unwrap().push(request.target_node_id.clone());
754 assert!(request.timeout_ms.is_some_and(|ms| ms <= 1000));
755 if request.target_node_id.as_deref() == Some("a") {
756 if first_fails_with_response {
757 return Ok(mesh_llm_plugin::HostInferenceResponse {
758 status_code: 503,
759 body: json!({"error": "unavailable"}),
760 served_by: None,
761 });
762 }
763 anyhow::bail!("target disappeared after discovery");
764 }
765 Ok(mesh_llm_plugin::HostInferenceResponse {
766 status_code: 200,
767 body: json!({"model": "small", "choices": [{"message": {"content": "healthy"}}]}),
768 served_by: Some("b".into()),
769 })
770 }
771 },
772 )
773 .await;
774 assert_eq!(response.status_code, 200);
775 assert_eq!(response.body["choices"][0]["message"]["content"], "healthy");
776 assert_eq!(response.headers, [("x-mesh-served-by".into(), "b".into())]);
777 assert_eq!(
778 *attempts.lock().unwrap(),
779 [Some("a".into()), Some("b".into())]
780 );
781 }
782 }
783
784 #[tokio::test]
785 async fn direct_route_does_not_retry_a_non_retryable_response() {
786 let attempts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
787 let seen = Arc::clone(&attempts);
788 let response = direct_with_fallback(
789 invocation(vec![
790 candidate("small", Some(8.0), "a"),
791 candidate("small", Some(8.0), "b"),
792 ]),
793 false,
794 Duration::from_secs(1),
795 move |_| {
796 let seen = Arc::clone(&seen);
797 async move {
798 seen.fetch_add(1, Ordering::Relaxed);
799 Ok(mesh_llm_plugin::HostInferenceResponse {
800 status_code: 402,
801 body: json!({"error": "payment required"}),
802 served_by: None,
803 })
804 }
805 },
806 )
807 .await;
808 assert_eq!(response.status_code, 402);
809 assert_eq!(attempts.load(Ordering::Relaxed), 1);
810 }
811
812 #[tokio::test]
813 async fn direct_route_has_one_shared_deadline() {
814 let attempts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
815 let seen = Arc::clone(&attempts);
816 let response = direct_with_fallback(
817 invocation(vec![
818 candidate("small", Some(8.0), "a"),
819 candidate("small", Some(8.0), "b"),
820 ]),
821 false,
822 Duration::from_millis(20),
823 move |_| {
824 let seen = Arc::clone(&seen);
825 async move {
826 seen.fetch_add(1, Ordering::Relaxed);
827 tokio::time::sleep(Duration::from_millis(100)).await;
828 anyhow::bail!("late")
829 }
830 },
831 )
832 .await;
833 assert_eq!(response.status_code, 504);
834 assert_eq!(attempts.load(Ordering::Relaxed), 1);
835 }
836
837 #[test]
838 fn capable_committee_is_capped_and_keeps_same_model_replicas() {
839 let mut candidates = vec![
840 candidate("big", Some(70.0), "node-a"),
841 candidate("big", Some(70.0), "node-b"),
842 candidate("medium-a", Some(32.0), "node-c"),
843 candidate("medium-b", Some(24.0), "node-d"),
844 candidate("small", Some(8.0), "node-e"),
845 ];
846
847 apply_pool_policy(&mut candidates);
848
849 assert_eq!(candidates.len(), COMMITTEE_CAP);
850 assert_eq!(candidates[0].model_id, "big");
851 assert_eq!(candidates[1].model_id, "big");
852 assert_ne!(
853 candidates[0].target_node_id, candidates[1].target_node_id,
854 "physical replicas must remain distinct committee members"
855 );
856 assert!(
857 candidates
858 .iter()
859 .all(|candidate| candidate.model_id != "small")
860 );
861 }
862
863 #[test]
864 fn committee_cap_keeps_ready_replica_ahead_of_deprioritized_large_ones() {
865 let mut candidates = (0..COMMITTEE_CAP)
866 .map(|index| candidate("big", Some(70.0), &format!("deprioritized-{index}")))
867 .collect::<Vec<_>>();
868 for candidate in &mut candidates {
869 candidate.deprioritized = true;
870 }
871 candidates.push(candidate("ready", Some(32.0), "ready"));
872 assert!(!apply_pool_policy(&mut candidates));
873 assert_eq!(candidates.len(), COMMITTEE_CAP);
874 assert_eq!(candidates[0].model_id, "ready");
875 }
876}