Skip to content

fix: stop auto from picking models nobody serves - #1221

Merged
michaelneale merged 1 commit into
mainfrom
fix/auto-route-phantom-models
Aug 10, 2026
Merged

fix: stop auto from picking models nobody serves#1221
michaelneale merged 1 commit into
mainfrom
fix/auto-route-phantom-models

Conversation

@michaelneale

@michaelneale michaelneale commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

auto no longer picks a model that nobody in the mesh is actually serving.

Before this, a request with model: "auto" could land on a "phantom" model — one still advertised in gossip but served by no peer (stale gossip, or a peer that unloaded it). The request then failed with:

model 'unsloth/Qwen3-8B-GGUF@main:Q4_K_M' not found (no local or remote host serving this model)

which is a confusing thing to receive when you asked for auto and never named that model.

Explicit model requests are unchanged — asking for a model nobody serves still returns an honest 404.

Architecture

The auto readiness filter checked local targets, then remote hosts, then fell through to true. A model with neither was therefore treated as "ready" and stayed in the auto candidate pool, where the weighted draw could select it.

It now fails closed: no routable local target and no remote host means not auto-route eligible.

The one case that must not regress is a freshly started serve node — it records its model in hosted_models/serving_models before the election target table and peer gossip catch up, so during that window its own model has no target and no remote host. A naive fail-closed would drop the node's own model from auto. model_is_locally_served keeps it eligible, and there is a test for exactly that window.

Regression origin

Two commits combined to produce this:

Validation

  • cargo test -p mesh-llm-host-runtime --lib — 1930 passed, 0 failed
  • cargo clippy -p mesh-llm-host-runtime --all-targets -- -D warnings — clean
  • cargo clippy -p mesh-llm --all-targets -- -D warnings — clean
  • cargo fmt --all --check — clean

Tests fail without the fix. I verified this rather than assuming: temporarily reverting the fail-closed line back to true makes phantom_model_is_not_auto_route_eligible FAIL while freshly_served_local_model_is_auto_route_eligible still passes, then restoring it makes both pass. So the new coverage genuinely pins the regression.

Public mesh

Ran mesh-llm client --auto against the public mesh (5 peers), which had a real phantom present (unsloth/Qwen3-8B-GGUF:Q4_K_M — advertised in /v1/models, served by no peer):

  • 55 model=auto requests across two runs: no phantom ever selected, no model_not_found.
  • Explicit call to the phantom still returns the honest 404.

One caveat worth stating plainly: on this particular mesh the phantom was also being masked by the big/small tier partition (Qwen3-8B sorts small, and big-tier models were available), so the public-mesh run alone does not prove the fix — I confirmed that by A/B-ing against an unfixed binary and getting identical results. The unit tests above are the real evidence; the mesh run is a no-regression check.

Unrelated to this change, one request in 55 returned a 503 (all 1 target(s) for model 'local-gguf/...' failed) — a peer whose target died mid-request. That model is served by a peer, so it is peer churn on a path this PR does not touch.

Summary by CodeRabbit

  • Bug Fixes
    • Improved automatic model routing to recognize models served locally, including during startup and routing-data updates.
    • Prevented requests from being routed to models that have no available local or remote serving targets.
    • Added coverage for unavailable models and newly loaded local models.

Auto model selection could choose a "phantom" model — one advertised in
gossip but served by no peer (stale gossip, or a peer that unloaded it)
— and the request then failed with a 404 naming a model the user never
asked for.

The readiness filter checked local targets, then remote hosts, then fell
through to `true`. A model with neither was therefore treated as ready
and stayed in the auto candidate pool.

It now fails closed: no routable local target and no remote host means
not auto-route eligible. A freshly started serve node is kept eligible
via its own hosted/serving model list, because that populates before the
election target table and peer gossip catch up.

Explicit model requests are unchanged and still return an honest 404.

Regression introduced by the combination of #734 (added the readiness
filter with the fail-open default) and #1082 (replaced the old
route-to-first-available fallback with a hard 404, which made selecting
a phantom user-visible).
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Auto-routing now rejects models without local or remote routing targets. Models currently served by the node remain eligible while routing targets and peer gossip state are not yet populated. Tests cover both exclusion and local-serving eligibility.

Changes

Local model auto-routing eligibility

Layer / File(s) Summary
Local serving detection
crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs
Adds model_is_locally_served to check hosted and serving model lists. Tests cover served and unserved models.
Ingress eligibility filtering
crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
Excludes models without local or remote targets while retaining locally served models. Tests cover phantom models and empty target tables.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ivgolovach, i386

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing auto-routing from selecting models that no local target or remote peer serves.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/auto-route-phantom-models

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs (1)

156-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the serving_models branch.

This test populates only hosted_models. Add a case where the model appears only in serving_models and assert that model_is_locally_served returns true.

🤖 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/auto_route.rs` around lines
156 - 168, Extend
freshly_loaded_local_model_stays_eligible_before_targets_populate to populate
serving_models with a model absent from hosted_models, then assert
model_is_locally_served returns true for that model while retaining the existing
hosted_models and unrelated-model assertions.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/mesh-llm-host-runtime/src/network/openai/ingress.rs`:
- Around line 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.

---

Nitpick comments:
In `@crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs`:
- Around line 156-168: Extend
freshly_loaded_local_model_stays_eligible_before_targets_populate to populate
serving_models with a model absent from hosted_models, then assert
model_is_locally_served returns true for that model while retaining the existing
hosted_models and unrelated-model assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e70c0b1e-be1d-4f78-b037-7fb19f872bf3

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5e2bf and b318d19.

📒 Files selected for processing (2)
  • crates/mesh-llm-host-runtime/src/network/openai/auto_route.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs

Comment on lines +355 to +363
// 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

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.

@ndizazzo ndizazzo changed the title Stop auto from picking models nobody serves fix: stop auto from picking models nobody serves Aug 10, 2026
@michaelneale
michaelneale merged commit 1a2fef9 into main Aug 10, 2026
46 checks passed
@michaelneale
michaelneale deleted the fix/auto-route-phantom-models branch August 10, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants