Skip to content
Merged
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ mesh-llm serve --auto --headless
|---|---|---|
| Try the public mesh | `mesh-llm serve --auto` | [docs/MESHES.md](docs/MESHES.md) |
| Start a private mesh | `mesh-llm serve --model Qwen3-8B-Q4_K_M` | [docs/MESHES.md](docs/MESHES.md) |
| Serve one model without mesh networking | `mesh-llm serve --local-model-only --model /models/model.gguf` | OpenAI API defaults to `127.0.0.1:9337` (`--port` and `--listen-all` change it) |
| Publish your own mesh | `mesh-llm serve --model Qwen3-8B-Q4_K_M --publish` | [docs/MESHES.md](docs/MESHES.md) |
| Join by invite token | `mesh-llm serve --join <token>` | [docs/MESHES.md](docs/MESHES.md) |
| Run an API-only client | `mesh-llm client --auto` | [docs/MESHES.md](docs/MESHES.md) |
Expand Down Expand Up @@ -115,6 +116,28 @@ mesh-llm serve --auto --headless
For a deeper operator guide, see [docs/USAGE.md](docs/USAGE.md). For every CLI
command and switch, see [docs/CLI.md](docs/CLI.md).

### Local model-only serving

Use the direct topology when a process should expose one complete local model
through the OpenAI API without becoming a mesh node:

```bash
mesh-llm serve \
--local-model-only \
--model /models/model.gguf \
--port 9337
```

This mode starts the OpenAI frontend and one local Skippy model runtime. It does
not start QUIC, discovery, peer maintenance, split planning, plugins, release
lookup, the web console, or the management API. Add `--listen-all` only when the
OpenAI endpoint must bind beyond loopback. Startup fails if the complete model
does not fit within detected local capacity (or `--max-vram`); it never falls
back to distributed serving.
Comment thread
i386 marked this conversation as resolved.

For `--local-model-only`, `--model`, `--gguf`, and `--mmproj` values must be
absolute paths and must not be symlinks.

## Mixture-of-Agents (`model: "mesh"`) — experimental

> ⚠️ **Experimental.** The MoA gateway is new in this release. Behavior,
Expand Down
4 changes: 4 additions & 0 deletions crates/mesh-llm-cli/src/parser/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ pub struct Cli {
#[arg(long, default_value = "9337")]
pub port: u16,

/// Serve one model directly through the local OpenAI API without starting a mesh node.
#[arg(long)]
pub local_model_only: bool,

/// Run as a client — no GPU, no model needed.
#[arg(long)]
pub client: bool,
Expand Down
19 changes: 18 additions & 1 deletion crates/mesh-llm-cli/src/parser/normalization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ where
// Recognized value-taking flags: --log-format, --mesh-discovery-mode, --max-vram,
// --llama-flavor, --device, --tensor-split, --bind-port, --bind-ip, --max-clients,
// --port, --console, --swarm-capture, --draft-max, --ctx-size.
// Boolean flags: --help-advanced, --auto, --client, --headless, --publish,
// Boolean flags: --help-advanced, --auto, --client, --local-model-only, --headless, --publish,
// --plugin, --auto-update, --no-draft, --split, --no-enumerate-host, --listen-all,
// --no-console, --owner-required.
let value_taking_flags = [
Expand Down Expand Up @@ -442,6 +442,23 @@ mod tests {
);
}

#[test]
fn local_model_only_is_an_explicit_serve_topology() {
let args = vec![
"mesh-llm",
"serve",
"--local-model-only",
"--model",
"/models/model.gguf",
];
let normalized = normalize_runtime_surface_args(args);
let cli = Cli::try_parse_from(&normalized.normalized).unwrap();

assert_eq!(normalized.explicit_surface, Some(RuntimeSurface::Serve));
assert!(cli.local_model_only);
assert!(!cli.client);
}

#[test]
fn unknown_top_level_command_is_captured_for_plugin_dispatch() {
let normalized = normalize_runtime_surface_args([
Expand Down
3 changes: 3 additions & 0 deletions crates/mesh-llm-cli/src/parser/runtime_surface_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ pub fn runtime_surface_help(surface: RuntimeSurface) -> String {
" --auto Auto-join the best discovered mesh\n",
" --join <JOIN> Join a mesh via invite token\n",
" --publish Publish this mesh for discovery\n",
" --local-model-only Serve one local model without mesh networking or management APIs\n",
" --port <PORT> OpenAI-compatible API port [default: 9337]\n",
" --console <CONSOLE> Management console/API port [default: 3131]\n",
" --log-format <FORMAT> Terminal output format [default: pretty]\n\n",
"Bare `mesh-llm serve` loads startup models from ~/.mesh-llm/config.toml.\n",
"Add [[models]] there or pass --model / --gguf explicitly.\n",
"With --local-model-only, --model, --gguf, and --mmproj must be absolute, non-symlink paths.\n",
"Run `mesh-llm --help-advanced` for the full runtime option surface.\n"
)
.to_string(),
Expand Down Expand Up @@ -47,6 +49,7 @@ mod tests {
assert!(help.contains("Usage: mesh-llm serve"));
assert!(help.contains("--model"));
assert!(help.contains("--gguf"));
assert!(help.contains("--local-model-only"));
assert!(help.contains("startup models"));
assert!(!help.contains("Pool GPUs over the internet for LLM inference\n\nUsage: mesh-llm"));
}
Expand Down
43 changes: 38 additions & 5 deletions crates/mesh-llm-guardrails/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,42 @@ fn strip_tag_pairs(content: &str, start_tag: &str, end_tag: &str) -> String {
let mut result = String::new();
while let Some(start_index) = remainder.find(start_tag) {
result.push_str(&remainder[..start_index]);
let after_start = &remainder[start_index + start_tag.len()..];
if let Some(end_index) = after_start.find(end_tag) {
remainder = &after_start[end_index + end_tag.len()..];
} else {
let Some(end_index) = matching_tag_end(&remainder[start_index..], start_tag, end_tag)
else {
return result;
}
};
remainder = &remainder[start_index + end_index..];
}
result.push_str(remainder);
Comment thread
i386 marked this conversation as resolved.
result
}

fn matching_tag_end(content: &str, start_tag: &str, end_tag: &str) -> Option<usize> {
let mut offset = 0;
let mut depth = 0usize;
while offset < content.len() {
let next_start = content[offset..]
.find(start_tag)
.map(|index| offset + index);
let next_end = content[offset..].find(end_tag).map(|index| offset + index);
match (next_start, next_end) {
(Some(start), Some(end)) if start < end => {
depth += 1;
offset = start + start_tag.len();
}
(_, Some(end)) if depth > 0 => {
depth -= 1;
offset = end + end_tag.len();
if depth == 0 {
return Some(offset);
}
}
_ => return None,
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -44,4 +69,12 @@ mod tests {
fn drops_unterminated_thinking_blocks_without_duplicating_prefix() {
assert_eq!(strip_thinking_blocks("hello <think>truncated"), "hello");
}

#[test]
fn strips_nested_thinking_blocks_completely() {
assert_eq!(
strip_thinking_blocks("before<think>outer<think>inner</think>tail</think>after"),
"beforeafter"
);
}
}
13 changes: 12 additions & 1 deletion crates/mesh-llm-host-runtime/src/inference/skippy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ impl SkippyHttpHandle {
self.port
}

pub(crate) fn status(&self) -> skippy_server::EmbeddedServerStatus {
self.server.status()
}

pub(crate) async fn shutdown(self) -> Result<()> {
self.server.shutdown().await
}
Expand Down Expand Up @@ -902,7 +906,14 @@ impl SkippyModelHandle {
}

pub(crate) fn start_http(&self, port: u16) -> Result<SkippyHttpHandle> {
let bind_addr = ([127, 0, 0, 1], port).into();
self.start_http_on(([127, 0, 0, 1], port).into())
}

pub(crate) fn start_http_on(
&self,
bind_addr: std::net::SocketAddr,
) -> Result<SkippyHttpHandle> {
let port = bind_addr.port();
let tokenizer = self
.runtime
.tokenizer_capability()
Expand Down
Loading
Loading