Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ pub(crate) async fn model_has_eligible_remote_host(
model_has_eligible_target(node, model, required_tokens, &targets, affinity).await
}

/// Whether this node itself serves `model`, independent of the target table.
///
/// A freshly started serve node loads its model and records it in
/// `serving_models` / `hosted_models` before the election target table and peer
/// gossip catch up. During that window the model has no routable target and no
/// remote host, so a strict readiness check would exclude the node's own model
/// from auto routing. This keeps it eligible.
pub(crate) async fn model_is_locally_served(node: &mesh::Node, model: &str) -> bool {
node.hosted_models().await.iter().any(|m| m == model)
|| node.serving_models().await.iter().any(|m| m == model)
}

pub(crate) fn pool_for_ready_models<'a>(
available: &[router::RoutingCandidate<'a>],
ready_models: &[&str],
Expand Down Expand Up @@ -132,6 +144,29 @@ mod tests {
assert_eq!(pool[0].name, "ready-model");
}

#[tokio::test]
async fn model_served_by_nobody_is_not_locally_served() {
let node = mesh::Node::new_for_tests(crate::mesh::NodeRole::Worker)
.await
.expect("test node");

assert!(!model_is_locally_served(&node, "phantom/model:Q4_K_M").await);
}

#[tokio::test]
async fn freshly_loaded_local_model_stays_eligible_before_targets_populate() {
let node = mesh::Node::new_for_tests(crate::mesh::NodeRole::Worker)
.await
.expect("test node");
// A serve node records its model here before the election target table
// and peer gossip catch up.
node.set_hosted_models(vec!["local/fresh-model:Q4_K_M".to_string()])
.await;

assert!(model_is_locally_served(&node, "local/fresh-model:Q4_K_M").await);
assert!(!model_is_locally_served(&node, "some/other-model:Q4_K_M").await);
}

#[test]
fn auto_route_pool_preserves_availability_when_none_ready() {
let caps = crate::models::ModelCapabilities::default();
Expand Down
65 changes: 64 additions & 1 deletion crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,15 @@ async fn auto_route_model_has_ready_ingress_target(
.await;
}

true
// No routable local target and no peer advertises this model. Fail closed:
// such a model is a phantom (stale gossip, or a peer that unloaded it), and
// letting it stay in the pool means `auto` can pick a model that then 404s
// on a model the user never named. Explicit requests still 404 honestly.
//
// The one exception is a freshly started serve node: its own model is
// loaded and in `serving_models` before the target table and gossip catch
// up, so keep it eligible rather than excluding this node's own model.
auto_route::model_is_locally_served(node, model).await
Comment on lines +355 to +363

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent the all-candidates fallback from undoing fail-closed readiness.

If every candidate reaches this branch, ready_models is empty. auto_route_pool_for_ready_models then calls auto_route::pool_for_ready_models, which returns all candidates when the ready list is empty in crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs, Lines 107-109. auto can therefore still select a phantom model.

Make this caller return an empty pool when no candidate is ready, or add an explicit fail-closed mode to the helper. Extend the regression test to assert the final pool or resolved model, not only the predicate. Preserve the existing helper contract tested in crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs, Lines 170-183.

Also applies to: 1057-1082

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mesh-llm-host-runtime/src/network/openai/ingress.rs` around lines 355
- 363, Ensure auto_route_pool_for_ready_models returns an empty pool when no
candidates pass the readiness predicate, preventing pool_for_ready_models from
restoring all candidates as a fallback. Preserve pool_for_ready_models’ existing
empty-ready-list contract, and extend the regression test to verify the final
pool or resolved model cannot select a phantom candidate.

}

fn maybe_enable_auto_route_hooks(
Expand Down Expand Up @@ -1046,6 +1054,61 @@ mod tests {
}
}

/// A model nobody serves must not stay in the auto pool. Before this,
/// the readiness check fell through to `true`, so `auto` could select a
/// phantom (stale gossip, or a peer that unloaded) and 404 the caller on
/// a model they never named.
#[tokio::test]
async fn phantom_model_is_not_auto_route_eligible() {
let node = mesh::Node::new_for_tests(crate::mesh::NodeRole::Worker)
.await
.expect("test node");
let targets = election::ModelTargets::default();
let affinity = affinity::AffinityRouter::new();

let eligible = auto_route_model_has_ready_ingress_target(
&node,
&targets,
"phantom/model:Q4_K_M",
None,
&affinity,
)
.await;

assert!(
!eligible,
"a model with no local target and no remote host must not be auto-route eligible"
);
}

/// A freshly started serve node records its model before the target table
/// and gossip catch up. Failing closed must not exclude this node's own
/// model during that window.
#[tokio::test]
async fn freshly_served_local_model_is_auto_route_eligible() {
let node = mesh::Node::new_for_tests(crate::mesh::NodeRole::Worker)
.await
.expect("test node");
node.set_hosted_models(vec!["local/fresh-model:Q4_K_M".to_string()])
.await;
let targets = election::ModelTargets::default();
let affinity = affinity::AffinityRouter::new();

let eligible = auto_route_model_has_ready_ingress_target(
&node,
&targets,
"local/fresh-model:Q4_K_M",
None,
&affinity,
)
.await;

assert!(
eligible,
"a locally served model must stay eligible before targets populate"
);
}

#[test]
fn parse_model_with_profile_with_named_profile() {
let (model_ref, profile) = parse_model_with_profile("Qwen3-8B#low-ctx");
Expand Down
Loading