feat: native rust SDK for rust consumers. (#736)

* a run at SDK, tested with a client

* Use in-process shutdown for embedded SDK

* Satisfy clippy for embedded shutdown plumbing

* Document embedded Rust SDK usage

* Add public Rust SDK crate

* Tighten embedded SDK lifecycle

* Address embedded SDK review feedback

* Document native runtime packaging direction

* Document runtime CLI namespace

* Document runtime CLI UX expectations

* Document runtime diagnostics under doctor

* Add Windows PowerShell installer

* Document recommended runtime install flow

* Add native runtime resolver foundation

* Wire native runtime release installs

* Document native runtime crate

* Load versioned native runtimes dynamically

* Fix embedded SDK output manager reset

* Expose SDK mesh admission controls

* Split SDK runtime mapping assertions

* Tighten SDK docs and config module

* Clarify native runtime SDK TODOs

* Stabilize native log note test

* Expose native runtime install SDK

* Re-export native runtime APIs from SDK crate

* Add embedded SDK knobs for Sprout relay mesh (#782)

Two opt-in seams the Sprout v1 mesh integration needs:

- disable_iroh_relays(bool): when true, embedded runtime selects an explicitly disabled relay policy, which uses RelayMode::Disabled, skips public relay URL fallback, skips raw STUN, and avoids the 5s endpoint.online() wait that cannot succeed without a home relay. Default false preserves existing behavior.

- EmbeddedNodeHandle::join_token(token): forwards an invite token over the runtime control channel to node.join_with_retry so an already-running embedded node can dial a new EndpointAddr without restart. Handled in both auto and passive/client runtime loops.

Purely additive; existing defaults and startup join_tokens behavior remain unchanged.

Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>

* Fix relay policy test visibility

* Make SDK publishable and align language bindings (#771)

* Split SDK native runtime publish surface

* Split CLI and TUI support crates

* Move CLI parser surface into mesh-llm-cli

* Extract shared mesh event surface

* Move standalone command handlers out of host runtime

* Move benchmark and plugin commands out of host runtime

* Finish plugin command extraction

* Extract auth identity ownership

* Move remaining standalone commands out of host runtime

* Move model store into model-hf

* Move CLI commands out of host runtime

* Decouple host runtime from CLI and TUI crates

* Expose embedded node SDK facade

* Keep client identity dependencies pure

* Simplify Rust SDK feature surface

* Keep SDK client feature runtime-free

* Expose SDK client API base override

* Use direct mesh SDK client transport

* Remove API base URL client builder shim

* Align language SDKs with Rust SDK facade

* Document SDK client and serving modes

* Fix SDK smoke runtime setup

* Package SDK console assets

* Fix dynamic runtime CI setup

* Restructure SDK docs by language

* Fix SDK smoke package loading

* Fix native runtime bundle resolution

* Add structured native runtime backend metadata

* Harden Kotlin native runtime smoke resolution

* Fix mesh-llm-sdk clippy imports

* Retry smoke model downloads

---------

Co-authored-by: James Dumay <jameswdumay@gmail.com>
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Michael Neale 2026-06-03 12:39:19 +10:00 committed by GitHub
parent ee67364163
commit 97c0cad991
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
248 changed files with 23415 additions and 9762 deletions

View file

@ -149,7 +149,7 @@ runs:
DIRECT_SDK_INPUTS=$(echo "$CHANGED_FILES" | grep -E '(^sdk/|^Package\.swift$|^scripts/ci-(native|kotlin|swift)-sdk-smoke\.sh$|^scripts/ci-sdk-fixture\.sh$|^\.github/workflows/sdk-smoke\.yml$)' || true)
if [[ -n "$DIRECT_SDK_INPUTS" ]]; then
SDK_SMOKE_REQUIRED="true"
elif echo "$AFFECTED_CRATES" | jq -e 'index("mesh-llm-client") or index("mesh-llm-api-client") or index("mesh-llm-api-server") or index("mesh-llm-config") or index("mesh-llm-console-server") or index("mesh-llm-ffi") or index("mesh-llm-protocol") or index("mesh-llm-routing") or index("mesh-llm-types")' >/dev/null; then
elif echo "$AFFECTED_CRATES" | jq -e 'index("mesh-llm-client") or index("mesh-llm-api-client") or index("mesh-llm-api-server") or index("mesh-llm-config") or index("mesh-llm-console-server") or index("mesh-llm-ffi") or index("mesh-llm-native-runtime") or index("mesh-llm-protocol") or index("mesh-llm-routing") or index("mesh-llm-types")' >/dev/null; then
SDK_SMOKE_REQUIRED="true"
fi
fi

View file

@ -67,7 +67,10 @@ runs:
run: |
set -euo pipefail
mkdir -p ~/.models
curl -fSL "${{ inputs.model_url }}" -o "$HOME/.models/${{ inputs.model_file }}"
curl --fail --location --show-error \
--retry 6 --retry-delay 10 --retry-all-errors \
"${{ inputs.model_url }}" \
-o "$HOME/.models/${{ inputs.model_file }}"
ls -lh "$HOME/.models/${{ inputs.model_file }}"
- name: Save integration model cache

View file

@ -72,17 +72,30 @@ jobs:
- 'crates/mesh-llm-api-client/**'
- 'crates/mesh-llm-api-server/**'
- 'crates/mesh-llm-config/**'
- 'crates/mesh-llm-commands/**'
- 'crates/mesh-llm-events/**'
- 'crates/mesh-llm-hardware-profile/**'
- 'crates/mesh-llm-runtime-install/**'
- 'crates/mesh-llm-sdk/**'
- 'crates/mesh-llm-cli/**'
- 'crates/mesh-llm-embedded-runtime/**'
- 'crates/mesh-llm-tui/**'
- 'crates/mesh-llm-console-server/**'
- 'crates/mesh-llm-ffi/**'
- 'crates/mesh-llm-nodejs/**'
- 'crates/mesh-client/**'
- 'crates/mesh-llm-identity/**'
- 'crates/mesh-llm-native-runtime/**'
- 'crates/mesh-llm-protocol/**'
- 'crates/mesh-llm-routing/**'
- 'crates/mesh-llm-types/**'
- 'sdk/**'
- 'Package.swift'
- 'scripts/ci-native-sdk-smoke.sh'
- 'scripts/ci-rust-sdk-smoke.sh'
- 'scripts/ci-prepare-native-runtime.sh'
- 'scripts/ci-install-native-runtime.sh'
- 'scripts/package-sdk-console-assets.sh'
- 'scripts/verify-sdk-console-assets.sh'
- 'scripts/ci-kotlin-sdk-smoke.sh'
- 'scripts/ci-swift-sdk-smoke.sh'
- 'scripts/ci-sdk-fixture.sh'
@ -247,7 +260,7 @@ jobs:
run: scripts/build-llama.sh
- name: SDK and API crate tests
if: ${{ needs.changes.outputs.all_rust == 'true' || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-config') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-console-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-ffi') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-nodejs') }}
if: ${{ needs.changes.outputs.all_rust == 'true' || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-config') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-commands') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-events') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-hardware-profile') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-runtime-install') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-sdk') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-cli') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-tui') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-embedded-runtime') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-console-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-ffi') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-nodejs') }}
env:
AFFECTED: ${{ needs.changes.outputs.affected_crates }}
ALL_RUST: ${{ needs.changes.outputs.all_rust }}
@ -257,7 +270,7 @@ jobs:
[ "$ALL_RUST" = "true" ] || jq -e --arg crate "$crate" 'index($crate) != null' <<<"$AFFECTED" >/dev/null
}
for c in mesh-llm-client mesh-llm-api-client mesh-llm-api-server mesh-llm-config mesh-llm-console-server mesh-llm-ffi mesh-llm-nodejs; do
for c in mesh-llm-client mesh-llm-api-client mesh-llm-api-server mesh-llm-config mesh-llm-commands mesh-llm-events mesh-llm-hardware-profile mesh-llm-runtime-install mesh-llm-cli mesh-llm-tui mesh-llm-embedded-runtime mesh-llm-sdk mesh-llm-console-server mesh-llm-ffi mesh-llm-nodejs; do
if should_test "$c"; then
cargo test -p "$c"
else
@ -427,12 +440,12 @@ jobs:
smoke_script: scripts/ci-two-node-client-serving-smoke.sh
timeout_minutes: 20
native_sdk_smoke:
rust_sdk_smoke:
needs: [changes, linux, inference_smoke_tests]
if: ${{ needs.inference_smoke_tests.result == 'success' && (github.event_name == 'workflow_dispatch' || needs.changes.outputs.sdk == 'true') && needs.changes.outputs.docs_only != 'true' }}
uses: ./.github/workflows/sdk-smoke.yml
with:
sdk_kind: native
sdk_kind: rust
artifact_name: ci-linux-inference-binaries
artifact_path: ci-artifacts/linux
staged_binary_path: target/debug/mesh-llm

View file

@ -63,13 +63,21 @@ jobs:
grep -qE '^\s*COPY\s+Cargo\.toml\s+Cargo\.lock\s+\./?$' "$file"
for path in \
crates/mesh-llm/ \
crates/mesh-llm-cli/ \
crates/mesh-llm-commands/ \
crates/mesh-llm-events/ \
crates/mesh-llm-hardware-profile/ \
crates/mesh-llm-identity/ \
crates/mesh-llm-native-runtime/ \
crates/mesh-llm-protocol/ \
crates/mesh-llm-routing/ \
crates/mesh-llm-runtime-install/ \
crates/mesh-llm-guardrails/ \
crates/mesh-llm-types/ \
crates/mesh-llm-config/ \
crates/mesh-llm-console-server/ \
crates/mesh-llm-embedded-runtime/ \
crates/mesh-llm-tui/ \
crates/mesh-llm-plugin/ \
crates/mesh-llm-skills/ \
crates/mesh-llm-plugin-manager/ \

View file

@ -76,17 +76,30 @@ jobs:
- 'crates/mesh-llm-api-client/**'
- 'crates/mesh-llm-api-server/**'
- 'crates/mesh-llm-config/**'
- 'crates/mesh-llm-commands/**'
- 'crates/mesh-llm-events/**'
- 'crates/mesh-llm-hardware-profile/**'
- 'crates/mesh-llm-runtime-install/**'
- 'crates/mesh-llm-sdk/**'
- 'crates/mesh-llm-cli/**'
- 'crates/mesh-llm-embedded-runtime/**'
- 'crates/mesh-llm-tui/**'
- 'crates/mesh-llm-console-server/**'
- 'crates/mesh-llm-ffi/**'
- 'crates/mesh-llm-nodejs/**'
- 'crates/mesh-client/**'
- 'crates/mesh-llm-identity/**'
- 'crates/mesh-llm-native-runtime/**'
- 'crates/mesh-llm-protocol/**'
- 'crates/mesh-llm-routing/**'
- 'crates/mesh-llm-types/**'
- 'sdk/**'
- 'Package.swift'
- 'scripts/ci-native-sdk-smoke.sh'
- 'scripts/ci-rust-sdk-smoke.sh'
- 'scripts/ci-prepare-native-runtime.sh'
- 'scripts/ci-install-native-runtime.sh'
- 'scripts/package-sdk-console-assets.sh'
- 'scripts/verify-sdk-console-assets.sh'
- 'scripts/ci-kotlin-sdk-smoke.sh'
- 'scripts/ci-swift-sdk-smoke.sh'
- 'scripts/ci-sdk-fixture.sh'
@ -496,14 +509,14 @@ jobs:
run: scripts/build-llama.sh
- name: SDK and API crate tests
if: ${{ matrix.group == 'sdk-api' && (needs.changes.outputs.all_rust == 'true' || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-config') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-console-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-ffi') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-nodejs')) }}
if: ${{ matrix.group == 'sdk-api' && (needs.changes.outputs.all_rust == 'true' || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-client') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-api-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-config') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-commands') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-events') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-hardware-profile') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-runtime-install') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-sdk') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-cli') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-tui') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-embedded-runtime') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-console-server') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-ffi') || contains(fromJson(needs.changes.outputs.affected_crates || '[]'), 'mesh-llm-nodejs')) }}
run: |
should_test() {
local crate="$1"
[ "$ALL_RUST" = "true" ] || jq -e --arg crate "$crate" 'index($crate) != null' <<<"$AFFECTED" >/dev/null
}
for c in mesh-llm-client mesh-llm-api-client mesh-llm-api-server mesh-llm-config mesh-llm-console-server mesh-llm-ffi mesh-llm-nodejs; do
for c in mesh-llm-client mesh-llm-api-client mesh-llm-api-server mesh-llm-config mesh-llm-commands mesh-llm-events mesh-llm-hardware-profile mesh-llm-runtime-install mesh-llm-cli mesh-llm-tui mesh-llm-embedded-runtime mesh-llm-sdk mesh-llm-console-server mesh-llm-ffi mesh-llm-nodejs; do
if should_test "$c"; then
cargo test -p "$c"
else
@ -682,12 +695,12 @@ jobs:
smoke_script: scripts/ci-two-node-split-smoke.sh
timeout_minutes: 25
native_sdk_smoke:
rust_sdk_smoke:
needs: [changes, linux_cpu_artifact]
if: ${{ needs.linux_cpu_artifact.result == 'success' && needs.changes.outputs.linux_inference_artifact_required == 'true' && needs.changes.outputs.sdk_smoke_required == 'true' && needs.changes.outputs.docs_only != 'true' }}
uses: ./.github/workflows/sdk-smoke.yml
with:
sdk_kind: native
sdk_kind: rust
artifact_name: ci-linux-inference-binaries
artifact_path: ci-artifacts/linux
staged_binary_path: target/debug/mesh-llm

View file

@ -242,6 +242,68 @@ jobs:
dist/native-sdk-crates/*/target/package/*.crate
if-no-files-found: error
build_native_runtime:
name: Build native runtime ${{ matrix.name }}
needs: metadata
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS aarch64 Metal
os: blacksmith-6vcpu-macos-15
backend: metal
target: aarch64-apple-darwin
artifact_suffix: darwin-aarch64-metal
- name: Linux x86_64 CPU
os: blacksmith-4vcpu-ubuntu-2404
backend: cpu
target: x86_64-unknown-linux-gnu
artifact_suffix: linux-x86_64-cpu
env:
LLAMA_STAGE_BACKEND: ${{ matrix.backend }}
MESH_NATIVE_RUNTIME_TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y build-essential cmake ninja-build pkg-config libssl-dev libdbus-1-dev curl lld
- name: Install macOS dependencies
if: runner.os == 'macOS'
run: brew install cmake ninja lld
- name: Prepare dispatched release version
if: github.event_name == 'workflow_dispatch'
env:
RELEASE_TAG: ${{ needs.metadata.outputs.tag }}
run: scripts/release-version.sh "$RELEASE_TAG"
- name: Package native runtime
run: |
scripts/package-native-runtime.sh \
--build \
--backend "${{ matrix.backend }}" \
--target "${{ matrix.target }}" \
--out dist/native-runtimes
- name: Verify native runtime artifact
run: scripts/verify-native-runtime-package.sh dist/native-runtimes/*.tar.gz
- name: Upload native runtime
uses: actions/upload-artifact@v6
with:
name: release-native-runtime-${{ matrix.artifact_suffix }}
path: |
dist/native-runtimes/*.tar.gz
dist/native-runtimes/*.sha256
if-no-files-found: error
build_swift_sdk_artifact:
name: Build Swift SDK XCFramework
needs: metadata
@ -251,6 +313,18 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v5
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
.github/cache-version.txt
crates/mesh-llm-ui/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
@ -264,6 +338,15 @@ jobs:
RELEASE_TAG: ${{ needs.metadata.outputs.tag }}
run: scripts/release-version.sh "$RELEASE_TAG"
- name: Prepare Swift console resources
run: |
scripts/package-sdk-console-assets.sh --sdk swift
scripts/verify-sdk-console-assets.sh --sdk swift
- name: Verify tagged Swift console resources
if: github.event_name != 'workflow_dispatch'
run: git ls-files --error-unmatch sdk/swift/Sources/MeshLLM/Resources/Console/index.html
- name: Build SwiftPM binary artifact
run: |
sdk/swift/scripts/build-xcframework.sh
@ -734,6 +817,7 @@ jobs:
- build
- inference_smoke_tests
- build_native_sdk_runtime
- build_native_runtime
- build_swift_sdk_artifact
- build_linux_arm64
- build_linux_aarch64_cuda
@ -742,14 +826,27 @@ jobs:
- build_linux_vulkan
- build_windows_cpu
- build_windows_gpu
if: ${{ always() && needs.metadata.result == 'success' && needs.metadata.outputs.canary != 'true' && needs.build.result == 'success' && needs.inference_smoke_tests.result == 'success' && needs.build_native_sdk_runtime.result == 'success' && needs.build_swift_sdk_artifact.result == 'success' && needs.build_linux_arm64.result == 'success' && (needs.build_linux_aarch64_cuda.result == 'success' || needs.build_linux_aarch64_cuda.result == 'skipped') && (needs.build_linux_cuda.result == 'success' || needs.build_linux_cuda.result == 'skipped') && (needs.build_linux_rocm.result == 'success' || needs.build_linux_rocm.result == 'skipped') && (needs.build_linux_vulkan.result == 'success' || needs.build_linux_vulkan.result == 'skipped') && needs.build_windows_cpu.result == 'success' && (needs.build_windows_gpu.result == 'success' || needs.build_windows_gpu.result == 'skipped') }}
if: ${{ always() && needs.metadata.result == 'success' && needs.metadata.outputs.canary != 'true' && needs.build.result == 'success' && needs.inference_smoke_tests.result == 'success' && needs.build_native_sdk_runtime.result == 'success' && needs.build_native_runtime.result == 'success' && needs.build_swift_sdk_artifact.result == 'success' && needs.build_linux_arm64.result == 'success' && (needs.build_linux_aarch64_cuda.result == 'success' || needs.build_linux_aarch64_cuda.result == 'skipped') && (needs.build_linux_cuda.result == 'success' || needs.build_linux_cuda.result == 'skipped') && (needs.build_linux_rocm.result == 'success' || needs.build_linux_rocm.result == 'skipped') && (needs.build_linux_vulkan.result == 'success' || needs.build_linux_vulkan.result == 'skipped') && needs.build_windows_cpu.result == 'success' && (needs.build_windows_gpu.result == 'success' || needs.build_windows_gpu.result == 'skipped') }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
if: github.event_name == 'workflow_dispatch'
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
if: github.event_name == 'workflow_dispatch'
with:
version: 10
- uses: actions/setup-node@v5
if: github.event_name == 'workflow_dispatch'
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
.github/cache-version.txt
crates/mesh-llm-ui/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
if: github.event_name == 'workflow_dispatch'
@ -763,6 +860,16 @@ jobs:
- name: Remove smoke-only binary
run: rm -f release-artifacts/mesh-llm
- name: Generate native runtime release manifest
env:
RELEASE_TAG: ${{ needs.metadata.outputs.tag }}
run: |
scripts/generate-native-runtime-release-manifest.sh \
--tag "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--out release-artifacts/native-runtimes.json \
release-artifacts/meshllm-native-runtime-*.tar.gz
- name: Download generated SwiftPM manifest
if: github.event_name == 'workflow_dispatch'
uses: actions/download-artifact@v7
@ -782,9 +889,12 @@ jobs:
fi
scripts/release-version.sh "$RELEASE_TAG"
cp generated-swift-manifest/Package.swift Package.swift
scripts/package-sdk-console-assets.sh --sdk all
scripts/verify-sdk-console-assets.sh --sdk all
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock crates/*/Cargo.toml tools/*/Cargo.toml sdk/kotlin/build.gradle.kts Package.swift
git add -f sdk/node/console sdk/swift/Sources/MeshLLM/Resources/Console sdk/kotlin/src/main/resources/mesh-llm/console
if git diff --cached --quiet; then
echo "Release source files already match $RELEASE_TAG"
else

View file

@ -55,7 +55,7 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
if: ${{ inputs.sdk_kind == 'native' }}
if: ${{ inputs.sdk_kind == 'rust' }}
with:
python-version: "3.12"
@ -65,6 +65,16 @@ jobs:
distribution: temurin
java-version: '21'
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: pnpm
cache-dependency-path: crates/mesh-llm-ui/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
- name: Install Linux SDK dependencies
@ -106,9 +116,9 @@ jobs:
cache_key_prefix: ${{ inputs.cache_key_prefix }}
save_model_cache: ${{ github.ref == 'refs/heads/main' }}
- name: Native SDK smoke test
if: ${{ inputs.sdk_kind == 'native' }}
run: scripts/ci-native-sdk-smoke.sh "${{ inputs.staged_binary_path }}" "${{ inputs.artifact_path }}" "$HOME/.models/${{ inputs.model_file }}"
- name: Rust SDK smoke test
if: ${{ inputs.sdk_kind == 'rust' }}
run: scripts/ci-rust-sdk-smoke.sh "${{ inputs.staged_binary_path }}" "${{ inputs.artifact_path }}" "$HOME/.models/${{ inputs.model_file }}"
- name: Kotlin SDK smoke test
if: ${{ inputs.sdk_kind == 'kotlin' }}

6
.gitignore vendored
View file

@ -26,6 +26,12 @@ dist/llama-stage-static/
sdk/kotlin/src/main/kotlin/uniffi/
sdk/kotlin/example/example-jvm/src/main/kotlin/uniffi/
sdk/node/native/
sdk/node/console/*
!sdk/node/console/.gitkeep
sdk/swift/Sources/MeshLLM/Resources/Console/*
!sdk/swift/Sources/MeshLLM/Resources/Console/.gitkeep
sdk/kotlin/src/main/resources/mesh-llm/console/*
!sdk/kotlin/src/main/resources/mesh-llm/console/.gitkeep
sdk/swift/Sources/MeshLLM/Generated/*
!sdk/swift/Sources/MeshLLM/Generated/mesh_ffi.swift
sdk/swift/Generated/FFI/

177
Cargo.lock generated
View file

@ -3759,15 +3759,27 @@ dependencies = [
name = "mesh-llm"
version = "0.68.0"
dependencies = [
"anyhow",
"axum",
"chrono",
"clap",
"hex",
"mesh-llm-cli",
"mesh-llm-client",
"mesh-llm-commands",
"mesh-llm-host-runtime",
"mesh-llm-plugin",
"mesh-llm-plugin-manager",
"mesh-llm-system",
"mesh-llm-tui",
"reqwest 0.12.28",
"serde",
"serde_json",
"serial_test",
"tabwriter",
"tempfile",
"tokio",
"urlencoding",
]
[[package]]
@ -3790,6 +3802,16 @@ dependencies = [
"tokio",
]
[[package]]
name = "mesh-llm-cli"
version = "0.68.0"
dependencies = [
"anyhow",
"clap",
"mesh-llm-events",
"serde",
]
[[package]]
name = "mesh-llm-client"
version = "0.68.0"
@ -3821,6 +3843,40 @@ dependencies = [
"uuid",
]
[[package]]
name = "mesh-llm-commands"
version = "0.68.0"
dependencies = [
"anyhow",
"chrono",
"dirs",
"hex",
"hf-hub",
"iroh",
"json5",
"mesh-llm-cli",
"mesh-llm-identity",
"mesh-llm-native-runtime",
"mesh-llm-plugin-manager",
"mesh-llm-runtime-install",
"mesh-llm-system",
"mesh-llm-tui",
"model-package",
"model-ref",
"rand 0.10.1",
"reqwest 0.12.28",
"rpassword",
"serde",
"serde_json",
"serde_yaml",
"serial_test",
"tempfile",
"tokio",
"tokio-stream",
"url",
"zeroize",
]
[[package]]
name = "mesh-llm-config"
version = "0.68.0"
@ -3845,14 +3901,30 @@ dependencies = [
"tokio",
]
[[package]]
name = "mesh-llm-embedded-runtime"
version = "0.68.0"
dependencies = [
"anyhow",
"mesh-llm-host-runtime",
"serde_json",
]
[[package]]
name = "mesh-llm-events"
version = "0.68.0"
dependencies = [
"anyhow",
"clap",
"serde_json",
]
[[package]]
name = "mesh-llm-ffi"
version = "0.68.0"
dependencies = [
"mesh-llm-api-server",
"mesh-llm-console-server",
"mesh-llm-host-runtime",
"mesh-llm-node",
"mesh-llm-sdk",
"thiserror 2.0.18",
"tokio",
"uniffi",
@ -3878,13 +3950,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "mesh-llm-hardware-profile"
version = "0.68.0"
dependencies = [
"mesh-llm-native-runtime",
]
[[package]]
name = "mesh-llm-host-runtime"
version = "0.68.0"
dependencies = [
"ansi-to-tui",
"anyhow",
"arboard",
"argon2",
"async-trait",
"axum",
@ -3897,6 +3974,7 @@ dependencies = [
"crypto_box",
"dirs",
"ed25519-dalek",
"flate2",
"futures-util",
"hex",
"hf-hub",
@ -3912,18 +3990,22 @@ dependencies = [
"mesh-llm-api-server",
"mesh-llm-client",
"mesh-llm-config",
"mesh-llm-events",
"mesh-llm-guardrails",
"mesh-llm-identity",
"mesh-llm-native-runtime",
"mesh-llm-node",
"mesh-llm-plugin",
"mesh-llm-plugin-manager",
"mesh-llm-protocol",
"mesh-llm-routing",
"mesh-llm-runtime-install",
"mesh-llm-system",
"mesh-llm-types",
"mesh-llm-ui",
"mesh-mixture-of-agents",
"model-artifact",
"model-hf",
"model-package",
"model-ref",
"model-resolver",
@ -3934,7 +4016,6 @@ dependencies = [
"opentelemetry_sdk",
"prost",
"rand 0.10.1",
"ratatui",
"regex-lite",
"reqwest 0.12.28",
"rmcp",
@ -3953,6 +4034,7 @@ dependencies = [
"skippy-server",
"skippy-topology",
"tabwriter",
"tar",
"tempfile",
"thiserror 2.0.18",
"tokio",
@ -3970,14 +4052,33 @@ dependencies = [
name = "mesh-llm-identity"
version = "0.68.0"
dependencies = [
"argon2",
"base64",
"chacha20poly1305",
"chrono",
"crypto_box",
"dirs",
"ed25519-dalek",
"hex",
"keyring",
"rand 0.10.1",
"serde",
"serde_json",
"serial_test",
"sha2 0.10.9",
"thiserror 2.0.18",
"zeroize",
]
[[package]]
name = "mesh-llm-native-runtime"
version = "0.68.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
]
[[package]]
@ -3998,9 +4099,7 @@ dependencies = [
name = "mesh-llm-nodejs"
version = "0.68.0"
dependencies = [
"mesh-llm-api-server",
"mesh-llm-console-server",
"mesh-llm-host-runtime",
"mesh-llm-sdk",
"napi",
"napi-build",
"napi-derive",
@ -4060,6 +4159,42 @@ dependencies = [
"iroh",
]
[[package]]
name = "mesh-llm-runtime-install"
version = "0.68.0"
dependencies = [
"anyhow",
"dirs",
"flate2",
"futures-util",
"hex",
"mesh-llm-hardware-profile",
"mesh-llm-native-runtime",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2 0.10.9",
"skippy-ffi",
"tar",
"tempfile",
"tokio",
]
[[package]]
name = "mesh-llm-sdk"
version = "0.68.0"
dependencies = [
"anyhow",
"mesh-llm-api-client",
"mesh-llm-api-server",
"mesh-llm-console-server",
"mesh-llm-embedded-runtime",
"mesh-llm-runtime-install",
"reqwest 0.12.28",
"serde",
"serde_json",
]
[[package]]
name = "mesh-llm-skills"
version = "0.68.0"
@ -4102,6 +4237,22 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "mesh-llm-tui"
version = "0.68.0"
dependencies = [
"ansi-to-tui",
"anyhow",
"arboard",
"chrono",
"crossterm 0.28.1",
"mesh-llm-events",
"ratatui",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "mesh-llm-types"
version = "0.68.0"
@ -4210,10 +4361,15 @@ version = "0.68.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"dirs",
"hf-hub",
"model-artifact",
"model-ref",
"serde",
"serde_json",
"serial_test",
"sha2 0.10.9",
"tempfile",
"tokio",
]
@ -7252,6 +7408,9 @@ dependencies = [
[[package]]
name = "skippy-ffi"
version = "0.68.0"
dependencies = [
"libloading",
]
[[package]]
name = "skippy-metrics"

View file

@ -1,16 +1,25 @@
[workspace]
members = [
"crates/mesh-llm",
"crates/mesh-llm-cli",
"crates/mesh-llm-commands",
"crates/mesh-llm-config",
"crates/mesh-llm-events",
"crates/mesh-llm-gpu-bench",
"crates/mesh-llm-host-runtime",
"crates/mesh-llm-hardware-profile",
"crates/mesh-llm-identity",
"crates/mesh-llm-native-runtime",
"crates/mesh-llm-protocol",
"crates/mesh-llm-routing",
"crates/mesh-llm-runtime-install",
"crates/mesh-llm-sdk",
"crates/mesh-llm-guardrails",
"crates/mesh-llm-system",
"crates/mesh-llm-tui",
"crates/mesh-llm-types",
"crates/mesh-llm-console-server",
"crates/mesh-llm-embedded-runtime",
"crates/mesh-llm-ui",
"crates/mesh-llm-plugin",
"crates/mesh-llm-skills",
@ -60,7 +69,7 @@ version = "0.68.0"
anyhow = "1"
blake3 = "1"
clap = { version = "4", features = ["derive"] }
mesh-llm-skills = { path = "crates/mesh-llm-skills" }
mesh-llm-skills = { path = "crates/mesh-llm-skills", version = "0.68.0" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"

View file

@ -55,6 +55,9 @@ let package = Package(
dependencies: meshLLMDependencies,
path: "sdk/swift/Sources/MeshLLM",
exclude: hasFFIBinaryTarget ? [] : ["Generated"],
resources: [
.copy("Resources/Console"),
],
linkerSettings: [
.linkedFramework("Accelerate"),
.linkedFramework("AppKit", .when(platforms: [.macOS])),

View file

@ -17,6 +17,12 @@ Install the latest release:
curl -fsSL https://raw.githubusercontent.com/Mesh-LLM/mesh-llm/main/install.sh | bash
```
On Windows, use PowerShell:
```powershell
irm https://raw.githubusercontent.com/Mesh-LLM/mesh-llm/main/install.ps1 | iex
```
Join the public mesh and start serving:
```bash

View file

@ -112,6 +112,30 @@ cargo run -p xtask -- repo-consistency publish-crates
scripts/publish-crates.sh --dry-run
```
SDK packages that expose the optional console must package the built web
console before publishing language SDK artifacts:
```bash
scripts/package-sdk-console-assets.sh --sdk all
scripts/verify-sdk-console-assets.sh --sdk all
```
The script builds `crates/mesh-llm-ui/dist` in release mode and copies it to
the canonical SDK resource locations: `sdk/node/console`,
`sdk/swift/Sources/MeshLLM/Resources/Console`, and
`sdk/kotlin/src/main/resources/mesh-llm/console`.
These generated directories are ignored during normal development. For a
manual tag push, force-add them into the release commit before tagging because
SwiftPM resolves package resources from the Git tag:
```bash
git add -f sdk/node/console sdk/swift/Sources/MeshLLM/Resources/Console sdk/kotlin/src/main/resources/mesh-llm/console
```
Workflow-dispatch releases generate and force-add these resources into the
release tag commit automatically.
The chain currently publishes:
1. `model-ref`

View file

@ -14,7 +14,7 @@ categories = ["network-programming", "api-bindings"]
name = "mesh_client"
[features]
host-io = []
host-io = ["mesh-llm-identity/host-io"]
[dependencies]
# Allowlist enforced by embedded-client-purity CI (Wave 1C)
@ -23,7 +23,7 @@ host-io = []
# tracing, sha2, ed25519-dalek, hex, uuid, url, http, base64, async-trait, httparse
# (httparse is a transitive dep of iroh; not in forbidden list)
iroh = "1.0.0-rc.0"
mesh-llm-identity = { path = "../mesh-llm-identity", version = "0.68.0" }
mesh-llm-identity = { path = "../mesh-llm-identity", version = "0.68.0", default-features = false }
mesh-llm-protocol = { path = "../mesh-llm-protocol", version = "0.68.0" }
mesh-llm-routing = { path = "../mesh-llm-routing", version = "0.68.0" }
mesh-llm-types = { path = "../mesh-llm-types", version = "0.68.0" }

View file

@ -1,5 +1,8 @@
use crate::crypto::keys::OwnerKeypair;
use crate::protocol::{ALPN_V1, STREAM_TUNNEL_HTTP};
use crate::runtime::CoreRuntime;
use base64::Engine;
use iroh::{Endpoint, EndpointAddr};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
@ -13,8 +16,7 @@ type CancelFlagMap =
Arc<Mutex<HashMap<String, (Arc<AtomicBool>, Arc<dyn crate::events::EventListener>)>>>;
pub const MAX_RECONNECT_ATTEMPTS: u32 = 10;
const MISSING_API_BASE_URL_ERROR: &str =
"MESH_CLIENT_API_BASE or ClientBuilder::with_api_base_url is required for inference";
const MAX_MESH_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
#[derive(Debug, Error)]
pub enum ClientError {
@ -46,12 +48,19 @@ impl std::str::FromStr for InviteToken {
}
}
#[derive(Clone, Debug)]
pub struct ClientConfig {
pub owner_keypair: OwnerKeypair,
pub invite_token: InviteToken,
pub user_agent: String,
pub connect_timeout: Duration,
pub api_base_url: Option<String>,
pub transport: ClientTransport,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClientTransport {
DirectMesh,
OpenAiHttp { api_base_url: String },
}
pub struct ClientBuilder {
@ -66,7 +75,7 @@ impl ClientBuilder {
invite_token,
user_agent: format!("mesh-client/{}", env!("CARGO_PKG_VERSION")),
connect_timeout: Duration::from_secs(30),
api_base_url: std::env::var("MESH_CLIENT_API_BASE").ok(),
transport: default_client_transport(),
},
}
}
@ -81,8 +90,19 @@ impl ClientBuilder {
self
}
pub fn with_api_base_url(mut self, api_base_url: String) -> Self {
self.config.api_base_url = Some(api_base_url);
pub fn with_transport(mut self, transport: ClientTransport) -> Self {
self.config.transport = transport;
self
}
pub fn with_direct_mesh_transport(self) -> Self {
self.with_transport(ClientTransport::DirectMesh)
}
pub fn with_openai_http_transport(mut self, api_base_url: impl Into<String>) -> Self {
self.config.transport = ClientTransport::OpenAiHttp {
api_base_url: api_base_url.into(),
};
self
}
@ -123,16 +143,9 @@ impl MeshClient {
/// List available models on the mesh.
pub async fn list_models(&self) -> Result<Vec<Model>, ClientError> {
let Some(base_url) = self.config.api_base_url.as_deref() else {
return Err(ClientError::Endpoint(
MISSING_API_BASE_URL_ERROR.to_string(),
));
};
let response =
http_get_json::<ModelsResponse>(base_url, "/v1/models", &self.config.user_agent)
.await
.map_err(ClientError::Endpoint)?;
let response = get_json::<ModelsResponse>(&self.config, "/v1/models")
.await
.map_err(ClientError::Endpoint)?;
Ok(response
.data
@ -158,57 +171,48 @@ impl MeshClient {
.unwrap()
.insert(id.0.clone(), (cancel_flag.clone(), listener.clone()));
let id_clone = id.0.clone();
let api_base_url = self.config.api_base_url.clone();
let user_agent = self.config.user_agent.clone();
let config = self.config.clone();
self.runtime.handle().spawn(async move {
if let Some(base_url) = api_base_url {
let body = serde_json::json!({
"model": request.model,
"messages": request.messages.iter().map(|m| serde_json::json!({
"role": m.role,
"content": m.content,
})).collect::<Vec<_>>(),
"max_tokens": 64,
"temperature": 0,
"stream": false,
});
match http_post_json::<ChatCompletionResponse>(
&base_url,
"/v1/chat/completions",
&user_agent,
body.to_string(),
)
.await
{
Ok(response) => {
if !cancel_flag.load(Ordering::Relaxed) {
if let Some(content) = response
.choices
.first()
.map(|choice| choice.message.content.clone())
{
listener.on_event(crate::events::Event::TokenDelta {
request_id: id_clone.clone(),
delta: content,
});
}
listener.on_event(crate::events::Event::Completed {
let body = serde_json::json!({
"model": request.model,
"messages": request.messages.iter().map(|m| serde_json::json!({
"role": m.role,
"content": m.content,
})).collect::<Vec<_>>(),
"max_tokens": 64,
"temperature": 0,
"stream": false,
});
match post_json::<ChatCompletionResponse>(
&config,
"/v1/chat/completions",
body.to_string(),
)
.await
{
Ok(response) => {
if !cancel_flag.load(Ordering::Relaxed) {
if let Some(content) = response
.choices
.first()
.map(|choice| choice.message.content.clone())
{
listener.on_event(crate::events::Event::TokenDelta {
request_id: id_clone.clone(),
delta: content,
});
}
}
Err(error) => {
listener.on_event(crate::events::Event::Failed {
listener.on_event(crate::events::Event::Completed {
request_id: id_clone.clone(),
error,
});
}
}
} else if !cancel_flag.load(Ordering::Relaxed) {
listener.on_event(crate::events::Event::Failed {
request_id: id_clone,
error: MISSING_API_BASE_URL_ERROR.to_string(),
});
Err(error) => {
listener.on_event(crate::events::Event::Failed {
request_id: id_clone,
error,
});
}
}
});
id
@ -227,57 +231,48 @@ impl MeshClient {
.unwrap()
.insert(id.0.clone(), (cancel_flag.clone(), listener.clone()));
let id_clone = id.0.clone();
let api_base_url = self.config.api_base_url.clone();
let user_agent = self.config.user_agent.clone();
let config = self.config.clone();
self.runtime.handle().spawn(async move {
if let Some(base_url) = api_base_url {
let body = serde_json::json!({
"model": request.model,
"messages": [{
"role": "user",
"content": request.input,
}],
"max_tokens": 64,
"temperature": 0,
"stream": false,
});
match http_post_json::<ChatCompletionResponse>(
&base_url,
"/v1/chat/completions",
&user_agent,
body.to_string(),
)
.await
{
Ok(response) => {
if !cancel_flag.load(Ordering::Relaxed) {
if let Some(content) = response
.choices
.first()
.map(|choice| choice.message.content.clone())
{
listener.on_event(crate::events::Event::TokenDelta {
request_id: id_clone.clone(),
delta: content,
});
}
listener.on_event(crate::events::Event::Completed {
let body = serde_json::json!({
"model": request.model,
"messages": [{
"role": "user",
"content": request.input,
}],
"max_tokens": 64,
"temperature": 0,
"stream": false,
});
match post_json::<ChatCompletionResponse>(
&config,
"/v1/chat/completions",
body.to_string(),
)
.await
{
Ok(response) => {
if !cancel_flag.load(Ordering::Relaxed) {
if let Some(content) = response
.choices
.first()
.map(|choice| choice.message.content.clone())
{
listener.on_event(crate::events::Event::TokenDelta {
request_id: id_clone.clone(),
delta: content,
});
}
}
Err(error) => {
listener.on_event(crate::events::Event::Failed {
listener.on_event(crate::events::Event::Completed {
request_id: id_clone.clone(),
error,
});
}
}
} else if !cancel_flag.load(Ordering::Relaxed) {
listener.on_event(crate::events::Event::Failed {
request_id: id_clone,
error: MISSING_API_BASE_URL_ERROR.to_string(),
});
Err(error) => {
listener.on_event(crate::events::Event::Failed {
request_id: id_clone,
error,
});
}
}
});
id
@ -389,6 +384,14 @@ impl Default for RequestId {
}
}
fn default_client_transport() -> ClientTransport {
std::env::var("MESH_CLIENT_API_BASE")
.ok()
.filter(|value| !value.trim().is_empty())
.map(|api_base_url| ClientTransport::OpenAiHttp { api_base_url })
.unwrap_or(ClientTransport::DirectMesh)
}
#[derive(Deserialize)]
struct ModelsResponse {
data: Vec<ModelEntry>,
@ -414,33 +417,162 @@ struct ChatMessageResponse {
content: String,
}
async fn http_get_json<T: for<'de> Deserialize<'de>>(
base_url: &str,
async fn get_json<T: for<'de> Deserialize<'de>>(
config: &ClientConfig,
path: &str,
user_agent: &str,
) -> Result<T, String> {
let request = format!(
"GET {path} HTTP/1.1\r\nHost: {}\r\nUser-Agent: {user_agent}\r\nConnection: close\r\n\r\n",
host_header(base_url)?
);
let response = http_request(base_url, request).await?;
let response = request_get_bytes(config, path).await?;
parse_json_response(&response)
}
async fn http_post_json<T: for<'de> Deserialize<'de>>(
base_url: &str,
async fn post_json<T: for<'de> Deserialize<'de>>(
config: &ClientConfig,
path: &str,
user_agent: &str,
body: String,
) -> Result<T, String> {
let request = format!(
"POST {path} HTTP/1.1\r\nHost: {}\r\nUser-Agent: {user_agent}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
host_header(base_url)?,
let response = request_post_bytes(config, path, body).await?;
parse_json_response(&response)
}
async fn request_get_bytes(config: &ClientConfig, path: &str) -> Result<Vec<u8>, String> {
match &config.transport {
ClientTransport::DirectMesh => {
let request = http_get_request(path, "mesh.local", &config.user_agent);
direct_mesh_request(&config.invite_token, config.connect_timeout, request).await
}
ClientTransport::OpenAiHttp { api_base_url } => {
let request = http_get_request(path, &host_header(api_base_url)?, &config.user_agent);
http_request(api_base_url, request).await
}
}
}
async fn request_post_bytes(
config: &ClientConfig,
path: &str,
body: String,
) -> Result<Vec<u8>, String> {
match &config.transport {
ClientTransport::DirectMesh => {
let request = http_post_request(path, "mesh.local", &config.user_agent, body);
direct_mesh_request(&config.invite_token, config.connect_timeout, request).await
}
ClientTransport::OpenAiHttp { api_base_url } => {
let request =
http_post_request(path, &host_header(api_base_url)?, &config.user_agent, body);
http_request(api_base_url, request).await
}
}
}
fn http_get_request(path: &str, host: &str, user_agent: &str) -> String {
format!(
"GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: {user_agent}\r\nConnection: close\r\n\r\n",
)
}
fn http_post_request(path: &str, host: &str, user_agent: &str, body: String) -> String {
format!(
"POST {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: {user_agent}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let response = http_request(base_url, request).await?;
parse_json_response(&response)
)
}
async fn direct_mesh_request(
invite_token: &InviteToken,
connect_timeout: Duration,
request: String,
) -> Result<Vec<u8>, String> {
let addr = decode_invite_endpoint_addr(invite_token.as_str())?;
let mut builder = Endpoint::builder(iroh::endpoint::presets::Minimal)
.secret_key(iroh::SecretKey::generate())
.alpns(vec![ALPN_V1.to_vec()])
.bind_addr(std::net::SocketAddr::from(([0, 0, 0, 0], 0)))
.map_err(|err| format!("build mesh endpoint: {err}"))?;
builder = builder.relay_mode(relay_mode_from_endpoint_addr(&addr));
let endpoint = builder
.bind()
.await
.map_err(|err| format!("bind mesh endpoint: {err}"))?;
let result = direct_mesh_request_with_endpoint(&endpoint, addr, connect_timeout, request).await;
endpoint.close().await;
result
}
async fn direct_mesh_request_with_endpoint(
endpoint: &Endpoint,
addr: EndpointAddr,
connect_timeout: Duration,
request: String,
) -> Result<Vec<u8>, String> {
if addr.relay_urls().next().is_some() {
let _ = tokio::time::timeout(connect_timeout, endpoint.online()).await;
}
let connection = tokio::time::timeout(connect_timeout, endpoint.connect(addr, ALPN_V1))
.await
.map_err(|_| "connect mesh endpoint: timed out".to_string())?
.map_err(|err| format!("connect mesh endpoint: {err}"))?;
let (mut send, mut recv) = connection
.open_bi()
.await
.map_err(|err| format!("open mesh request stream: {err}"))?;
send.write_all(&[STREAM_TUNNEL_HTTP])
.await
.map_err(|err| format!("write mesh request stream type: {err}"))?;
send.write_all(request.as_bytes())
.await
.map_err(|err| format!("write mesh request: {err}"))?;
send.finish()
.map_err(|err| format!("finish mesh request: {err}"))?;
let response = recv
.read_to_end(MAX_MESH_RESPONSE_BYTES)
.await
.map_err(|err| format!("read mesh response: {err}"))?;
connection.close(0u32.into(), b"mesh-client-request-complete");
Ok(response)
}
#[derive(Deserialize)]
struct SignedBootstrapTokenAddrs {
serialized_addrs: Vec<Vec<u8>>,
}
fn decode_invite_endpoint_addr(invite_token: &str) -> Result<EndpointAddr, String> {
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(invite_token)
.map_err(|err| format!("invalid invite token encoding: {err}"))?;
if let Ok(addr) = serde_json::from_slice::<EndpointAddr>(&payload) {
return Ok(addr);
}
let signed = serde_json::from_slice::<SignedBootstrapTokenAddrs>(&payload)
.map_err(|err| format!("invalid invite token payload: {err}"))?;
let addr = signed
.serialized_addrs
.first()
.ok_or_else(|| "signed invite token has no endpoint addresses".to_string())?;
serde_json::from_slice(addr).map_err(|err| format!("invalid signed invite endpoint: {err}"))
}
fn relay_mode_from_endpoint_addr(addr: &EndpointAddr) -> iroh::endpoint::RelayMode {
match relay_map_from_endpoint_addr(addr) {
Some(relay_map) => iroh::endpoint::RelayMode::Custom(relay_map),
None => iroh::endpoint::RelayMode::Disabled,
}
}
fn relay_map_from_endpoint_addr(addr: &EndpointAddr) -> Option<iroh::RelayMap> {
let configs: Vec<_> = addr
.relay_urls()
.cloned()
.map(|url| iroh::RelayConfig::new(url, None))
.collect();
if configs.is_empty() {
None
} else {
Some(iroh::RelayMap::from_iter(configs))
}
}
async fn http_request(base_url: &str, request: String) -> Result<Vec<u8>, String> {

View file

@ -1,8 +1,8 @@
pub mod builder;
pub mod control_plane;
pub use builder::{
ChatMessage, ChatRequest, ClientBuilder, ClientConfig, ClientError, InviteToken, MeshClient,
Model, RequestId, ResponsesRequest, Status,
ChatMessage, ChatRequest, ClientBuilder, ClientConfig, ClientError, ClientTransport,
InviteToken, MeshClient, Model, RequestId, ResponsesRequest, Status,
};
pub use control_plane::{
ConfigTransportSelection, ControlPlaneBootstrapOptions, ControlPlaneClientError,

View file

@ -13,10 +13,10 @@ pub mod events;
pub mod protocol;
pub use client::{
ChatMessage, ChatRequest, ClientBuilder, ClientError, ConfigTransportSelection,
ControlPlaneBootstrapOptions, ControlPlaneClientError, ControlPlaneConnection,
ControlPlaneNegotiationError, ControlPlaneRetryPolicy, InviteToken, MeshClient, Model,
OwnerControlClient, OwnerControlRemoteError, OwnerControlWatchEvent, OwnerControlWatchStream,
RequestId, ResponsesRequest, Status,
ChatMessage, ChatRequest, ClientBuilder, ClientError, ClientTransport,
ConfigTransportSelection, ControlPlaneBootstrapOptions, ControlPlaneClientError,
ControlPlaneConnection, ControlPlaneNegotiationError, ControlPlaneRetryPolicy, InviteToken,
MeshClient, Model, OwnerControlClient, OwnerControlRemoteError, OwnerControlWatchEvent,
OwnerControlWatchStream, RequestId, ResponsesRequest, Status,
};
pub use crypto::keys::OwnerKeypair;

View file

@ -67,13 +67,13 @@ async fn mesh_client_cancel_idempotent() {
}
#[tokio::test]
async fn mesh_client_list_models_requires_api_base_url() {
async fn mesh_client_list_models_defaults_to_direct_mesh_transport() {
let kp = OwnerKeypair::generate();
let token = InviteToken::from_str("test-token").unwrap();
let client = ClientBuilder::new(kp, token).build().unwrap();
let err = client.list_models().await.unwrap_err();
assert!(err.to_string().contains("MESH_CLIENT_API_BASE"));
assert!(err.to_string().contains("invalid invite token"));
}
#[tokio::test]

View file

@ -6,6 +6,7 @@ use std::time::Duration;
use thiserror::Error;
pub const MAX_RECONNECT_ATTEMPTS: u32 = mesh_client::client::builder::MAX_RECONNECT_ATTEMPTS;
pub type ClientTransport = mesh_client::ClientTransport;
#[derive(Debug, Error)]
pub enum MeshApiError {
@ -33,6 +34,7 @@ pub struct ClientConfig {
pub invite_token: InviteToken,
pub user_agent: String,
pub connect_timeout: Duration,
pub transport: ClientTransport,
}
pub struct ClientBuilder {
@ -47,6 +49,7 @@ impl ClientBuilder {
invite_token,
user_agent: format!("mesh-llm-api-client/{}", env!("CARGO_PKG_VERSION")),
connect_timeout: Duration::from_secs(30),
transport: ClientTransport::DirectMesh,
},
}
}
@ -61,14 +64,33 @@ impl ClientBuilder {
self
}
pub fn with_transport(mut self, transport: ClientTransport) -> Self {
self.config.transport = transport;
self
}
pub fn with_direct_mesh_transport(self) -> Self {
self.with_transport(ClientTransport::DirectMesh)
}
pub fn with_openai_http_transport(mut self, api_base_url: impl Into<String>) -> Self {
self.config.transport = ClientTransport::OpenAiHttp {
api_base_url: api_base_url.into(),
};
self
}
pub fn build(self) -> Result<MeshClient, MeshApiError> {
let inner = mesh_client::ClientBuilder::new(
let mut builder = mesh_client::ClientBuilder::new(
self.config.owner_keypair.into_inner(),
self.config.invite_token.into_inner(),
)
.with_user_agent(self.config.user_agent.clone())
.with_connect_timeout(self.config.connect_timeout)
.build()?;
.with_connect_timeout(self.config.connect_timeout);
builder = builder.with_transport(self.config.transport);
let inner = builder.build()?;
Ok(MeshClient { inner })
}
@ -252,3 +274,33 @@ impl mesh_client::events::EventListener for EventListenerAdapter {
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_accepts_explicit_openai_http_transport() {
let owner = OwnerKeypair::generate();
let invite = "mesh-test:token".parse::<InviteToken>().unwrap();
let builder = ClientBuilder::new(owner, invite)
.with_openai_http_transport("http://127.0.0.1:9337/v1");
assert_eq!(
builder.config.transport,
ClientTransport::OpenAiHttp {
api_base_url: "http://127.0.0.1:9337/v1".to_string()
}
);
}
#[test]
fn builder_defaults_to_direct_mesh_transport() {
let owner = OwnerKeypair::generate();
let invite = "mesh-test:token".parse::<InviteToken>().unwrap();
let builder = ClientBuilder::new(owner, invite);
assert_eq!(builder.config.transport, ClientTransport::DirectMesh);
}
}

View file

@ -7,8 +7,8 @@ mod identity;
mod token;
pub use client::{
ChatMessage, ChatRequest, ClientBuilder, ClientConfig, MAX_RECONNECT_ATTEMPTS, MeshApiError,
MeshClient, Model, RequestId, ResponsesRequest, Status,
ChatMessage, ChatRequest, ClientBuilder, ClientConfig, ClientTransport, MAX_RECONNECT_ATTEMPTS,
MeshApiError, MeshClient, Model, RequestId, ResponsesRequest, Status,
};
pub use discover::{
AutoConnectResult, PublicMesh, PublicMeshQuery, create_auto_client, discover_public_meshes,

View file

@ -0,0 +1,20 @@
[package]
name = "mesh-llm-cli"
edition.workspace = true
license.workspace = true
version.workspace = true
description = "Reusable CLI support surface for mesh-llm"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
keywords = ["llm", "mesh", "cli"]
categories = ["command-line-interface"]
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
mesh-llm-events = { path = "../mesh-llm-events", version = "0.68.0" }
serde.workspace = true

View file

@ -0,0 +1,9 @@
# mesh-llm-cli
`mesh-llm-cli` owns the command-line surface for the shipped `mesh-llm` binary:
Clap parser types, runtime surface normalization, terminal progress indicators,
pager behavior, shell quoting, and shared CLI-facing output format types.
The current host runtime still owns command dispatch while its handlers are
being untangled from runtime internals. New parser types and CLI-only helpers
should live here instead of in `mesh-llm-host-runtime`.

View file

@ -2,7 +2,7 @@ use clap::{Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub(crate) enum BenchmarkCommand {
pub enum BenchmarkCommand {
/// Import a prompt corpus from a supported online source into local JSONL.
#[command(name = "import-prompts")]
ImportPrompts {
@ -27,7 +27,7 @@ pub(crate) enum BenchmarkCommand {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum GpuBenchmarkBackend {
pub enum GpuBenchmarkBackend {
Metal,
Cuda,
Hip,
@ -35,7 +35,7 @@ pub(crate) enum GpuBenchmarkBackend {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum PromptImportSource {
pub enum PromptImportSource {
MtBench,
Gsm8k,
Humaneval,

View file

@ -0,0 +1,17 @@
#![forbid(unsafe_code)]
pub mod benchmark;
pub mod models;
pub mod pager;
pub mod parser;
pub mod runtime;
pub mod shell;
pub use mesh_llm_events::LogFormat;
pub use parser::{
AuthCommand, BinaryFlavor, Cli, Command, DiscoveryScope, DoctorCommand, GpuCommand,
MeshDiscoveryMode, MeshGuardrailCliMode, NormalizedRuntimeArgs, PluginCommand, RuntimeSurface,
SkillAgentArg, SkillCommand, TrustCommand, TrustPolicy, legacy_runtime_surface_warning,
normalize_runtime_surface_args, validate_discovery_mode_args,
};

View file

@ -6,7 +6,7 @@ use std::process::{Command, Stdio};
const DEFAULT_PAGER: &str = "less";
const DEFAULT_PAGER_ARGS: &[&str] = &["-F", "-R", "-X"];
pub(crate) fn print_or_page(output: &str) -> Result<()> {
pub fn print_or_page(output: &str) -> Result<()> {
if !should_use_pager(
std::io::stdin().is_terminal(),
std::io::stdout().is_terminal(),

View file

@ -3,10 +3,76 @@ use std::ffi::OsString;
use std::net::IpAddr;
use std::path::PathBuf;
use crate::cli::benchmark::BenchmarkCommand;
use crate::cli::runtime::RuntimeCommand;
use crate::crypto::TrustPolicy;
use crate::network::discovery::MeshDiscoveryMode;
use crate::benchmark::BenchmarkCommand;
use crate::models;
use crate::runtime::RuntimeCommand;
use mesh_llm_events::LogFormat;
use serde::Serialize;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum BinaryFlavor {
#[default]
Cpu,
Cuda,
Rocm,
Vulkan,
Metal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum TrustPolicy {
#[default]
Off,
PreferOwned,
RequireOwned,
Allowlist,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum MeshDiscoveryMode {
#[default]
Nostr,
Mdns,
}
impl MeshDiscoveryMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::Nostr => "nostr",
Self::Mdns => "mdns",
}
}
pub const fn source(self) -> &'static str {
match self {
Self::Nostr => "nostr-relay",
Self::Mdns => "mdns-sd",
}
}
pub const fn scope(self) -> DiscoveryScope {
match self {
Self::Nostr => DiscoveryScope::Public,
Self::Mdns => DiscoveryScope::Lan,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiscoveryScope {
Public,
Lan,
}
impl DiscoveryScope {
pub const fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::Lan => "lan",
}
}
}
/// Parse a `URL=TOKEN` pair for `--relay-auth`. Splits on the first `=` only,
/// so tokens may contain `=` (base64 padding, JWTs).
@ -100,7 +166,7 @@ mod relay_auth_parser_tests {
}
#[derive(Subcommand, Debug)]
pub(crate) enum TrustCommand {
pub enum TrustCommand {
/// Add an owner to the local trust store allowlist.
Add {
/// Owner ID to trust.
@ -129,7 +195,7 @@ pub(crate) enum TrustCommand {
}
#[derive(Subcommand, Debug)]
pub(crate) enum AuthCommand {
pub enum AuthCommand {
/// Generate a new owner keypair and save to keystore.
Init {
/// Path to the owner keystore.
@ -296,7 +362,7 @@ pub(crate) enum AuthCommand {
}
#[derive(Subcommand, Debug)]
pub(crate) enum GpuCommand {
pub enum GpuCommand {
/// Detect and benchmark local GPUs, rewriting the cached fingerprint.
Detect {
/// Print machine-readable JSON output.
@ -305,24 +371,8 @@ pub(crate) enum GpuCommand {
},
}
pub(crate) mod benchmark;
pub(crate) mod commands;
pub mod models;
pub mod output;
pub(crate) mod pager;
pub(crate) mod runtime;
pub(crate) mod shell;
pub(crate) mod terminal_progress;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum LogFormat {
#[default]
Pretty,
Json,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub(crate) enum MeshGuardrailCliMode {
pub enum MeshGuardrailCliMode {
#[default]
Disabled,
Metrics,
@ -330,200 +380,192 @@ pub(crate) enum MeshGuardrailCliMode {
}
impl MeshGuardrailCliMode {
pub(crate) const fn as_str(self) -> &'static str {
pub const fn as_str(self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::Metrics => "metrics",
Self::Enforce => "enforce",
}
}
pub(crate) const fn to_guardrail_mode(self) -> openai_frontend::GuardrailMode {
match self {
Self::Disabled => openai_frontend::GuardrailMode::Disabled,
Self::Metrics => openai_frontend::GuardrailMode::MetricsOnly,
Self::Enforce => openai_frontend::GuardrailMode::Enforce,
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "mesh-llm",
version = crate::VERSION,
version = env!("CARGO_PKG_VERSION"),
about = "Pool GPUs over the internet for LLM inference",
after_help = "Preferred runtime entrypoints:\n mesh-llm serve\n mesh-llm serve --model Qwen3-8B-Q4_K_M\n mesh-llm client --auto\n mesh-llm gpus\n\n`mesh-llm serve` loads startup models from ~/.mesh-llm/config.toml.\nRun with --help-advanced for all options.\n\nExternal backends (vLLM, TGI, Ollama):\n Install the plugin:\n mesh-llm plugins install openai-endpoint\n Add to ~/.mesh-llm/config.toml:\n [[plugin]]\n name = \"openai-endpoint\"\n url = \"http://gpu-box:8000/v1\"\n Then: mesh-llm serve (or: mesh-llm client for client-only mode)\n\nFlash-MoE SSD backend:\n Install the plugin:\n mesh-llm plugins install flash-moe\n Add [[plugin]] name = \"flash-moe\" with url or plugin-owned args.\n Then: mesh-llm serve (or: mesh-llm client for client-only mode)"
)]
pub(crate) struct Cli {
pub struct Cli {
#[command(subcommand)]
pub(crate) command: Option<Command>,
pub command: Option<Command>,
/// Terminal output format for app-owned runtime events.
#[arg(long, value_enum, default_value_t = LogFormat::Pretty)]
pub(crate) log_format: LogFormat,
pub log_format: LogFormat,
/// Enable mesh runtime debug output; set MESH_LLM_DEBUG_NATIVE_VERBOSE=1 for verbose llama.cpp native logs.
#[arg(long)]
pub(crate) debug: bool,
pub debug: bool,
/// OTLP/gRPC endpoint for embedded Skippy debug telemetry, for example http://127.0.0.1:14317.
#[arg(long, hide = true)]
pub(crate) skippy_metrics_otlp_grpc: Option<String>,
pub skippy_metrics_otlp_grpc: Option<String>,
/// Server-side mesh guardrail mode for hosted Skippy backends.
#[arg(long = "mesh-guardrails", value_enum, default_value_t = MeshGuardrailCliMode::Disabled)]
pub(crate) mesh_guardrails: MeshGuardrailCliMode,
pub mesh_guardrails: MeshGuardrailCliMode,
/// Show all options (including advanced/niche ones).
#[arg(long, hide = true)]
pub(crate) help_advanced: bool,
pub help_advanced: bool,
/// Join a mesh via invite token (can repeat).
#[arg(long, short)]
pub(crate) join: Vec<String>,
pub join: Vec<String>,
/// Discover a mesh and join it.
#[arg(long, default_missing_value = "", num_args = 0..=1)]
pub(crate) discover: Option<String>,
pub discover: Option<String>,
/// Auto-join the best mesh found via discovery.
#[arg(long)]
pub(crate) auto: bool,
pub auto: bool,
/// Discovery provider for --auto, --discover, --publish, and the discover command.
#[arg(long, value_enum, default_value_t = MeshDiscoveryMode::Nostr, global = true)]
pub(crate) mesh_discovery_mode: MeshDiscoveryMode,
pub mesh_discovery_mode: MeshDiscoveryMode,
/// Model to serve (path, remote catalog name, or Hugging Face ref).
#[arg(long)]
pub(crate) model: Vec<PathBuf>,
pub model: Vec<PathBuf>,
/// Raw local GGUF file to serve directly (repeatable).
#[arg(long)]
pub(crate) gguf: Vec<PathBuf>,
pub gguf: Vec<PathBuf>,
/// Explicit mmproj sidecar for the primary served model.
#[arg(long, hide = true)]
pub(crate) mmproj: Option<PathBuf>,
pub mmproj: Option<PathBuf>,
/// API port (default: 9337).
#[arg(long, default_value = "9337")]
pub(crate) port: u16,
pub port: u16,
/// Run as a client — no GPU, no model needed.
#[arg(long)]
pub(crate) client: bool,
pub client: bool,
/// Web console port (default: 3131).
#[arg(long, default_value = "3131")]
pub(crate) console: u16,
pub console: u16,
/// Disable the embedded web UI but keep the management API on the --console port.
#[arg(long)]
pub(crate) headless: bool,
pub headless: bool,
/// Write passive swarm debug capture JSONL to this local directory (opt-in, no telemetry egress).
#[arg(long)]
pub(crate) swarm_capture: Option<PathBuf>,
pub swarm_capture: Option<PathBuf>,
/// Publish this mesh for discovery by other nodes.
/// Without this flag, your mesh is private and only joinable via invite token.
#[arg(long)]
pub(crate) publish: bool,
pub publish: bool,
/// Human-readable name for this mesh (shown in discovery when combined with --publish).
/// Naming a mesh does NOT make it publicly discoverable — use --publish for that.
#[arg(long)]
pub(crate) mesh_name: Option<String>,
pub mesh_name: Option<String>,
/// Region tag, e.g. "US", "EU", "AU" (shown in discovery).
#[arg(long)]
pub(crate) region: Option<String>,
pub region: Option<String>,
/// Minimum mesh-llm node version required when creating a new mesh.
#[arg(long)]
pub(crate) min_node_version: Option<String>,
pub min_node_version: Option<String>,
/// Maximum mesh-llm node version allowed when creating a new mesh.
#[arg(long)]
pub(crate) max_node_version: Option<String>,
pub max_node_version: Option<String>,
/// Minimum protocol generation required when creating a new mesh.
#[arg(long)]
pub(crate) min_protocol_version: Option<u32>,
pub min_protocol_version: Option<u32>,
/// Maximum protocol generation allowed when creating a new mesh.
#[arg(long)]
pub(crate) max_protocol_version: Option<u32>,
pub max_protocol_version: Option<u32>,
/// Require release attestation when creating a new mesh.
#[arg(long)]
pub(crate) require_release_attestation: bool,
pub require_release_attestation: bool,
/// Allowed release signer key for mesh creation-time attestation policy (repeatable).
#[arg(long = "release-signer-key")]
pub(crate) release_signer_key: Vec<String>,
pub release_signer_key: Vec<String>,
/// Display name for this node.
#[arg(long)]
pub(crate) name: Option<String>,
pub name: Option<String>,
/// Internal plugin service mode.
#[arg(long, hide = true)]
pub(crate) plugin: Option<String>,
pub plugin: Option<String>,
/// Update mesh-llm before continuing for release-bundle installs if a newer bundled release is available.
#[arg(long, global = true)]
pub(crate) auto_update: bool,
pub auto_update: bool,
// ── Advanced options (hidden from default --help) ─────────────
/// Draft model for speculative decoding.
#[arg(long, hide = true)]
pub(crate) draft: Option<PathBuf>,
pub draft: Option<PathBuf>,
/// Max draft tokens (default: 8).
#[arg(long, default_value = "8", hide = true)]
pub(crate) draft_max: u16,
pub draft_max: u16,
/// Disable automatic draft model detection.
#[arg(long, hide = true)]
pub(crate) no_draft: bool,
pub no_draft: bool,
/// Force tensor split even if the model fits on one node.
#[arg(long, hide = true)]
pub(crate) split: bool,
pub split: bool,
/// Override context size (tokens). Default: auto-scaled to available VRAM.
#[arg(long, hide = true)]
pub(crate) ctx_size: Option<u32>,
pub ctx_size: Option<u32>,
/// Cap VRAM used for planning, local-fit decisions, and mesh advertisement (GB).
#[arg(long)]
pub(crate) max_vram: Option<f64>,
pub max_vram: Option<f64>,
/// Disable broadcasting GPU name, hostname, VRAM, and reserved bytes to peers. By default all nodes announce this hardware info.
#[arg(long = "no-enumerate-host", hide = true)]
pub(crate) no_enumerate_host: bool,
pub no_enumerate_host: bool,
/// Path to bundled mesh support binaries.
#[arg(long, hide = true)]
pub(crate) bin_dir: Option<PathBuf>,
pub bin_dir: Option<PathBuf>,
/// Override which bundled llama.cpp flavor to use.
#[arg(long, value_enum)]
pub(crate) llama_flavor: Option<crate::system::backend::BinaryFlavor>,
pub llama_flavor: Option<BinaryFlavor>,
/// Device override for local backend selection.
#[arg(long, hide = true)]
pub(crate) device: Option<String>,
pub device: Option<String>,
/// Deprecated tensor split override retained for CLI compatibility.
#[arg(long, hide = true)]
pub(crate) tensor_split: Option<String>,
pub tensor_split: Option<String>,
/// Override iroh relay URLs.
#[arg(long, hide = true)]
pub(crate) relay: Vec<String>,
pub relay: Vec<String>,
/// Per-relay bearer token for gated iroh relays, formatted as
/// `URL=TOKEN`. Repeatable. The token is sent as
@ -534,70 +576,74 @@ pub(crate) struct Cli {
/// Splits on the first `=` only, so tokens may contain `=` (base64
/// padding, JWTs, etc.).
#[arg(long = "relay-auth", value_parser = parse_relay_auth_pair, hide = true)]
pub(crate) relay_auth: Vec<(String, String)>,
pub relay_auth: Vec<(String, String)>,
/// Disable iroh relays even when public mesh discovery would normally use them.
#[arg(long = "disable-iroh-relays", hide = true)]
pub disable_iroh_relays: bool,
/// Bind QUIC to a fixed UDP port (for NAT port forwarding).
#[arg(long, hide = true)]
pub(crate) bind_port: Option<u16>,
pub bind_port: Option<u16>,
/// Bind mesh QUIC to a specific local IP address.
#[arg(long, hide = true)]
pub(crate) bind_ip: Option<IpAddr>,
pub bind_ip: Option<IpAddr>,
/// Bind to 0.0.0.0 (for containers/Fly.io).
#[arg(long, hide = true)]
pub(crate) listen_all: bool,
pub listen_all: bool,
/// Stop advertising when N clients connected.
#[arg(long, hide = true)]
pub(crate) max_clients: Option<usize>,
pub max_clients: Option<usize>,
/// Custom Nostr relay URLs.
#[arg(long, hide = true)]
pub(crate) nostr_relay: Vec<String>,
pub nostr_relay: Vec<String>,
/// Ignored (backward compat).
#[arg(long, hide = true)]
pub(crate) no_console: bool,
pub no_console: bool,
/// Optional path to the mesh-llm config file.
#[arg(long)]
pub(crate) config: Option<PathBuf>,
pub config: Option<PathBuf>,
/// Path to the owner keystore used to attest this node.
#[arg(long)]
pub(crate) owner_key: Option<PathBuf>,
pub owner_key: Option<PathBuf>,
/// Bind address for the owner-control listener. Defaults to 127.0.0.1:0 when owner identity is configured.
#[arg(long, hide = true)]
pub(crate) control_bind: Option<std::net::SocketAddr>,
pub control_bind: Option<std::net::SocketAddr>,
/// Advertised owner-control address encoded into the local-only bootstrap token.
#[arg(long, hide = true)]
pub(crate) control_advertise_addr: Option<std::net::SocketAddr>,
pub control_advertise_addr: Option<std::net::SocketAddr>,
/// Fail startup if owner attestation cannot be loaded or signed.
#[arg(long)]
pub(crate) owner_required: bool,
pub owner_required: bool,
/// Optional human label attached to this node certificate.
#[arg(long)]
pub(crate) node_label: Option<String>,
pub node_label: Option<String>,
/// Override peer ownership trust policy.
#[arg(long, value_enum)]
pub(crate) trust_policy: Option<TrustPolicy>,
pub trust_policy: Option<TrustPolicy>,
/// Add trusted owner IDs on top of the local trust store.
#[arg(long)]
pub(crate) trust_owner: Vec<String>,
pub trust_owner: Vec<String>,
/// Internal: set when this node joined via Nostr discovery (not --join).
#[arg(skip)]
pub(crate) nostr_discovery: bool,
pub nostr_discovery: bool,
}
pub(crate) fn validate_discovery_mode_args(cli: &Cli) -> anyhow::Result<()> {
pub fn validate_discovery_mode_args(cli: &Cli) -> anyhow::Result<()> {
if cli.mesh_discovery_mode != MeshDiscoveryMode::Mdns {
return Ok(());
}
@ -621,7 +667,7 @@ pub(crate) fn validate_discovery_mode_args(cli: &Cli) -> anyhow::Result<()> {
}
#[derive(Subcommand, Debug)]
pub(crate) enum Command {
pub enum Command {
/// Manage model storage, migration, and update checks.
Models {
#[command(subcommand)]
@ -642,7 +688,7 @@ pub(crate) enum Command {
version: Option<String>,
/// Install this release bundle flavor instead of the default installed flavor.
#[arg(long, value_enum, conflicts_with = "detect_flavor")]
flavor: Option<crate::system::backend::BinaryFlavor>,
flavor: Option<BinaryFlavor>,
/// Re-detect the best host backend flavor before selecting the release bundle.
#[arg(long, conflicts_with = "flavor")]
detect_flavor: bool,
@ -656,16 +702,18 @@ pub(crate) enum Command {
#[command(subcommand)]
command: Option<GpuCommand>,
},
/// Inspect and manage local runtime-served models.
#[command(hide = true)]
/// Inspect and manage native runtimes.
Runtime {
#[command(subcommand)]
command: Option<RuntimeCommand>,
},
/// Diagnose local mesh, runtime, and split-readiness problems.
Doctor {
/// Print machine-readable JSON for the default doctor report.
#[arg(long)]
json: bool,
#[command(subcommand)]
command: DoctorCommand,
command: Option<DoctorCommand>,
},
/// Load a local model into a running mesh-llm instance.
Load {
@ -870,7 +918,7 @@ pub(crate) enum Command {
}
#[derive(Subcommand, Debug)]
pub(crate) enum PluginCommand {
pub enum PluginCommand {
/// Install a native plugin from the catalog or a GitHub repository.
Install {
/// Plugin catalog name, GitHub owner/repo, or GitHub URL.
@ -911,7 +959,7 @@ pub(crate) enum PluginCommand {
}
#[derive(Subcommand, Debug)]
pub(crate) enum SkillCommand {
pub enum SkillCommand {
/// Install skills exposed by installed plugins into supported agent skill folders.
Install {
/// Agent to install for. Repeat to install to several agents.
@ -930,7 +978,7 @@ pub(crate) enum SkillCommand {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum SkillAgentArg {
pub enum SkillAgentArg {
Global,
Goose,
Pi,
@ -939,21 +987,8 @@ pub(crate) enum SkillAgentArg {
Claude,
}
impl From<SkillAgentArg> for mesh_llm_plugin_manager::SkillAgent {
fn from(value: SkillAgentArg) -> Self {
match value {
SkillAgentArg::Global => Self::Global,
SkillAgentArg::Goose => Self::Goose,
SkillAgentArg::Pi => Self::Pi,
SkillAgentArg::Codex => Self::Codex,
SkillAgentArg::Opencode => Self::Opencode,
SkillAgentArg::Claude => Self::Claude,
}
}
}
#[derive(Subcommand, Debug)]
pub(crate) enum DoctorCommand {
pub enum DoctorCommand {
/// Diagnose split-readiness for a model on a running local mesh node.
Split {
/// Model ref/name to diagnose.
@ -972,19 +1007,19 @@ pub(crate) enum DoctorCommand {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum RuntimeSurface {
pub enum RuntimeSurface {
Serve,
Client,
}
#[derive(Clone, Debug)]
pub(crate) struct NormalizedRuntimeArgs {
pub(crate) original: Vec<OsString>,
pub(crate) normalized: Vec<OsString>,
pub(crate) explicit_surface: Option<RuntimeSurface>,
pub struct NormalizedRuntimeArgs {
pub original: Vec<OsString>,
pub normalized: Vec<OsString>,
pub explicit_surface: Option<RuntimeSurface>,
}
pub(crate) fn normalize_runtime_surface_args<I, S>(args: I) -> NormalizedRuntimeArgs
pub fn normalize_runtime_surface_args<I, S>(args: I) -> NormalizedRuntimeArgs
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
@ -1103,7 +1138,7 @@ where
}
}
pub(crate) fn legacy_runtime_surface_warning(
pub fn legacy_runtime_surface_warning(
cli: &Cli,
original_args: &[OsString],
explicit_surface: Option<RuntimeSurface>,
@ -1179,7 +1214,7 @@ fn shell_display(arg: &OsString) -> String {
}
#[cfg(test)]
pub(crate) fn assert_mesh_requirements_docs_examples_parse() {
pub fn assert_mesh_requirements_docs_examples_parse() {
let unrestricted_args =
normalize_runtime_surface_args(["mesh-llm", "serve", "--model", "Qwen3-8B-Q4_K_M"]);
let unrestricted = Cli::parse_from(unrestricted_args.normalized.clone());
@ -1247,7 +1282,7 @@ pub(crate) fn assert_mesh_requirements_docs_examples_parse() {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::models::{ModelSearchSort, ModelsCommand};
use crate::models::{ModelSearchSort, ModelsCommand};
use clap::{CommandFactory, Parser, error::ErrorKind};
#[test]
@ -1745,10 +1780,7 @@ mod tests {
]);
let cli = Cli::parse_from(normalized.normalized);
assert_eq!(
cli.mesh_discovery_mode,
crate::network::discovery::MeshDiscoveryMode::Mdns
);
assert_eq!(cli.mesh_discovery_mode, MeshDiscoveryMode::Mdns);
assert_eq!(normalized.explicit_surface, Some(RuntimeSurface::Serve));
}
@ -1757,10 +1789,7 @@ mod tests {
let normalized = normalize_runtime_surface_args(["mesh-llm", "serve", "--auto"]);
let cli = Cli::parse_from(normalized.normalized);
assert_eq!(
cli.mesh_discovery_mode,
crate::network::discovery::MeshDiscoveryMode::Nostr
);
assert_eq!(cli.mesh_discovery_mode, MeshDiscoveryMode::Nostr);
}
#[test]
@ -1770,10 +1799,7 @@ mod tests {
let cli = Cli::parse_from(normalized.normalized);
assert!(cli.client);
assert_eq!(
cli.mesh_discovery_mode,
crate::network::discovery::MeshDiscoveryMode::Mdns
);
assert_eq!(cli.mesh_discovery_mode, MeshDiscoveryMode::Mdns);
}
#[test]

View file

@ -1,17 +1,86 @@
use clap::Subcommand;
use std::path::PathBuf;
use crate::cli::MeshGuardrailCliMode;
use crate::MeshGuardrailCliMode;
#[derive(Subcommand, Debug)]
pub(crate) enum RuntimeCommand {
pub enum RuntimeCommand {
/// List available or installed native runtimes.
List {
/// List release-manifest or bundled runtimes instead of installed runtimes.
#[arg(long, conflicts_with = "installed")]
available: bool,
/// List installed native runtimes. This is the default when no list mode is supplied.
#[arg(long, conflicts_with = "available")]
installed: bool,
/// Release manifest JSON to inspect.
#[arg(long)]
manifest: Option<PathBuf>,
/// Packaged native runtime directory to inspect. Repeatable.
#[arg(long = "bundle-dir")]
bundle_dirs: Vec<PathBuf>,
/// Override the native runtime cache root.
#[arg(long)]
cache_dir: Option<PathBuf>,
/// Print machine-readable JSON.
#[arg(long)]
json: bool,
},
/// Install the recommended native runtime, or an explicit flavor/runtime ID.
Install {
/// Optional runtime flavor or native runtime ID. Omit to install the recommended runtime.
runtime: Option<String>,
/// Release manifest JSON to resolve against.
#[arg(long)]
manifest: Option<PathBuf>,
/// Packaged native runtime directory to install from. Repeatable.
#[arg(long = "bundle-dir")]
bundle_dirs: Vec<PathBuf>,
/// Override the native runtime cache root.
#[arg(long)]
cache_dir: Option<PathBuf>,
/// Print machine-readable JSON.
#[arg(long)]
json: bool,
},
/// Remove an installed native runtime.
Remove {
/// Native runtime ID to remove.
native_runtime_id: String,
/// MeshLLM version. Defaults to the running MeshLLM version.
#[arg(long)]
mesh_version: Option<String>,
/// Override the native runtime cache root.
#[arg(long)]
cache_dir: Option<PathBuf>,
/// Print machine-readable JSON.
#[arg(long)]
json: bool,
},
/// Prune old native runtimes from the cache.
Prune {
/// Remove every runtime not matching the active MeshLLM version.
#[arg(long)]
active_only: bool,
/// Override the active MeshLLM version. Defaults to the running version.
#[arg(long)]
mesh_version: Option<String>,
/// Override the native runtime cache root.
#[arg(long)]
cache_dir: Option<PathBuf>,
/// Print machine-readable JSON.
#[arg(long)]
json: bool,
},
/// Show local model status on a running mesh-llm instance.
#[command(hide = true)]
Status {
/// Console/API port of the running mesh-llm instance (default: 3131)
#[arg(long, default_value = "3131")]
port: u16,
},
/// Show the local-only owner-control bootstrap policy for a running mesh-llm instance.
#[command(hide = true)]
Bootstrap {
/// Console/API port of the running mesh-llm instance (default: 3131)
#[arg(long, default_value = "3131")]
@ -21,6 +90,7 @@ pub(crate) enum RuntimeCommand {
json: bool,
},
/// Fetch config from a remote owner-control endpoint through the local management API.
#[command(hide = true)]
GetConfig {
/// Explicit owner-control endpoint token for the target node.
#[arg(long)]
@ -33,6 +103,7 @@ pub(crate) enum RuntimeCommand {
json: bool,
},
/// Refresh local inventory on a remote owner-control endpoint through the local management API.
#[command(hide = true)]
RefreshInventory {
/// Explicit owner-control endpoint token for the target node.
#[arg(long)]
@ -45,6 +116,7 @@ pub(crate) enum RuntimeCommand {
json: bool,
},
/// Apply config to a remote owner-control endpoint through the local management API.
#[command(hide = true)]
ApplyConfig {
/// Explicit owner-control endpoint token for the target node.
#[arg(long)]
@ -63,6 +135,7 @@ pub(crate) enum RuntimeCommand {
json: bool,
},
/// Load a local model into a running mesh-llm instance.
#[command(hide = true)]
Load {
/// Model name/path/url to load
name: String,
@ -71,7 +144,7 @@ pub(crate) enum RuntimeCommand {
port: u16,
},
/// Unload a local model from a running mesh-llm instance.
#[command(alias = "drop")]
#[command(alias = "drop", hide = true)]
Unload {
/// Model name to unload
name: String,
@ -80,6 +153,7 @@ pub(crate) enum RuntimeCommand {
port: u16,
},
/// Set mesh guardrail mode on running Skippy-backed models without restart.
#[command(hide = true)]
Guardrails {
/// Guardrail mode to apply to active Skippy-backed OpenAI surfaces.
#[arg(long, value_enum)]

View file

@ -1,4 +1,4 @@
pub(crate) fn single_quote(value: &str) -> String {
pub fn single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\"'\"'"))
}

View file

@ -0,0 +1,47 @@
[package]
name = "mesh-llm-commands"
edition.workspace = true
license.workspace = true
version.workspace = true
description = "User-facing command handlers for mesh-llm"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
keywords = ["llm", "mesh", "cli"]
categories = ["command-line-interface"]
publish = false
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
dirs = "6.0.0"
hf_hub = { package = "hf-hub", version = "1.0.0-rc.1", default-features = false, features = ["blocking"] }
hex = "0.4.3"
iroh = "1.0.0-rc.0"
json5 = "1.3.1"
mesh-llm-cli = { path = "../mesh-llm-cli", version = "0.68.0" }
mesh-llm-native-runtime = { path = "../mesh-llm-native-runtime", version = "0.68.0" }
mesh-llm-plugin-manager = { path = "../mesh-llm-plugin-manager", version = "0.68.0" }
mesh-llm-identity = { path = "../mesh-llm-identity", version = "0.68.0", features = ["host-io"] }
mesh-llm-runtime-install = { path = "../mesh-llm-runtime-install", version = "0.68.0" }
mesh-llm-system = { path = "../mesh-llm-system", version = "0.68.0", features = ["skippy-devices"] }
mesh-llm-tui = { path = "../mesh-llm-tui", version = "0.68.0" }
model-package = { path = "../model-package", version = "0.68.0" }
model-ref = { path = "../model-ref", version = "0.68.0" }
rpassword = "5"
reqwest = { version = "0.12", features = ["json"] }
serde.workspace = true
serde_json.workspace = true
serde_yaml = "0.9"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
url = "2"
zeroize = { version = "1", features = ["derive"] }
[dev-dependencies]
rand = "0.10"
serial_test = "3"
tempfile = "3"

View file

@ -0,0 +1,10 @@
# mesh-llm-commands
`mesh-llm-commands` owns command handlers that can run without depending on
`mesh-llm-host-runtime`.
This crate is part of the host-runtime decomposition: command handlers move
here first when they can be expressed in terms of lower-level domain crates.
The shipped `mesh-llm` binary can dispatch these handlers directly, while
`mesh-llm-host-runtime` keeps temporary compatibility shims until command
dispatch fully leaves the host runtime.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,17 +4,16 @@ use std::result::Result as StdResult;
use anyhow::{Context, Result, bail};
use iroh::{EndpointId, SecretKey};
use zeroize::Zeroizing;
use crate::cli::TrustCommand;
use crate::crypto::{
use mesh_llm_cli::{AuthCommand, TrustCommand};
use mesh_llm_identity::{
KEYCHAIN_SERVICE, OwnerKeychainLoadError, OwnerKeypair, SignedNodeOwnership, TrustPolicy,
TrustStore, default_keystore_path, default_node_ownership_path, default_trust_store_path,
keystore_exists, keystore_metadata, load_keystore, load_node_ownership,
load_owner_keypair_from_keychain, load_trust_store, save_keystore, save_keystore_with_keychain,
TrustStore, default_keystore_path, default_node_key_path, default_node_ownership_path,
default_trust_store_path, keystore_exists, keystore_metadata, load_keystore,
load_node_key_bytes_from_path, load_node_ownership, load_owner_keypair_from_keychain,
load_trust_store, save_keystore, save_keystore_with_keychain, save_node_key_bytes_to_path,
save_node_ownership, save_trust_store, sign_node_ownership, verify_node_ownership,
};
use crate::mesh::{default_node_key_path, load_node_key_from_path, save_node_key_to_path};
use zeroize::Zeroizing;
fn now_unix_ms() -> u64 {
std::time::SystemTime::now()
@ -33,7 +32,131 @@ fn resolve_owner_key_path(owner_key: Option<PathBuf>) -> Result<PathBuf> {
fn resolve_node_key_path(node_key: Option<PathBuf>) -> Result<PathBuf> {
match node_key {
Some(path) => Ok(path),
None => default_node_key_path(),
None => Ok(default_node_key_path()?),
}
}
fn load_node_key_from_path(path: &Path) -> Result<SecretKey> {
Ok(SecretKey::from_bytes(&load_node_key_bytes_from_path(path)?))
}
fn save_node_key_to_path(path: &Path, key: &SecretKey) -> Result<()> {
save_node_key_bytes_to_path(path, &key.to_bytes())?;
Ok(())
}
pub fn run_auth_command(command: &AuthCommand) -> Result<()> {
match command {
AuthCommand::Init {
owner_key,
force,
no_passphrase,
keychain,
} => run_init(owner_key.clone(), *force, *no_passphrase, *keychain),
AuthCommand::Status {
owner_key,
node_key,
node_ownership,
trust_store,
} => run_status(
owner_key.clone(),
node_key.clone(),
node_ownership.clone(),
trust_store.clone(),
),
AuthCommand::SignNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
} => run_sign_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
),
AuthCommand::RenewNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
} => run_renew_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
),
AuthCommand::VerifyNode {
file,
node_id,
trust_store,
trust_policy,
} => run_verify_node(
file.clone(),
node_id.clone(),
trust_store.clone(),
trust_policy.map(cli_trust_policy_to_identity),
),
AuthCommand::RotateNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
revoke_current,
reason,
trust_store,
} => run_rotate_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
*revoke_current,
reason.clone(),
trust_store.clone(),
),
AuthCommand::RevokeOwner {
owner_id,
reason,
trust_store,
} => run_revoke_owner(owner_id.clone(), reason.clone(), trust_store.clone()),
AuthCommand::RevokeNode {
cert_id,
node_id,
reason,
trust_store,
} => run_revoke_node(
cert_id.clone(),
node_id.clone(),
reason.clone(),
trust_store.clone(),
),
AuthCommand::RotateOwner {
owner_key,
no_passphrase,
force,
} => run_rotate_owner(owner_key.clone(), *no_passphrase, *force),
AuthCommand::Trust { command } => run_trust_command(command),
}
}
fn cli_trust_policy_to_identity(value: mesh_llm_cli::TrustPolicy) -> TrustPolicy {
match value {
mesh_llm_cli::TrustPolicy::Off => TrustPolicy::Off,
mesh_llm_cli::TrustPolicy::PreferOwned => TrustPolicy::PreferOwned,
mesh_llm_cli::TrustPolicy::RequireOwned => TrustPolicy::RequireOwned,
mesh_llm_cli::TrustPolicy::Allowlist => TrustPolicy::Allowlist,
}
}
@ -87,7 +210,7 @@ fn resolve_keystore_passphrase(path: &Path) -> Result<Option<Zeroizing<String>>>
return Ok(Some(Zeroizing::new(passphrase)));
}
Err(crate::crypto::CryptoError::MissingPassphrase.into())
Err(mesh_llm_identity::CryptoError::MissingPassphrase.into())
}
fn load_owner_keypair_from_path(path: &Path) -> Result<OwnerKeypair> {
@ -96,12 +219,14 @@ fn load_owner_keypair_from_path(path: &Path) -> Result<OwnerKeypair> {
match load_owner_keypair_from_keychain(path) {
Ok(keypair) => return Ok(keypair),
Err(OwnerKeychainLoadError::NoEntry)
| Err(OwnerKeychainLoadError::Crypto(crate::crypto::CryptoError::DecryptionFailed))
| Err(OwnerKeychainLoadError::Crypto(
crate::crypto::CryptoError::KeychainUnavailable { .. },
mesh_llm_identity::CryptoError::DecryptionFailed,
))
| Err(OwnerKeychainLoadError::Crypto(
crate::crypto::CryptoError::KeychainAccessDenied { .. },
mesh_llm_identity::CryptoError::KeychainUnavailable { .. },
))
| Err(OwnerKeychainLoadError::Crypto(
mesh_llm_identity::CryptoError::KeychainAccessDenied { .. },
)) => {}
Err(OwnerKeychainLoadError::Crypto(err)) => {
return Err(err)
@ -169,7 +294,7 @@ pub(crate) fn run_init(
}
let use_keychain = if keychain {
if !crate::crypto::keychain_available() {
if !mesh_llm_identity::keychain_available() {
bail!(
"No OS keychain backend is available on this host.\n\
Retry without --keychain to set a passphrase, or with --no-passphrase \
@ -179,7 +304,7 @@ pub(crate) fn run_init(
true
} else {
let available =
(!existing_keystore && !no_passphrase) && crate::crypto::keychain_available();
(!existing_keystore && !no_passphrase) && mesh_llm_identity::keychain_available();
should_default_to_keychain(existing_keystore, no_passphrase, available)
};
@ -752,16 +877,16 @@ fn encrypted_keystore_keychain_status(error: OwnerKeychainLoadError) -> String {
passphrase when the owner keystore is consumed)"
.into()
}
OwnerKeychainLoadError::Crypto(crate::crypto::CryptoError::DecryptionFailed) => {
OwnerKeychainLoadError::Crypto(mesh_llm_identity::CryptoError::DecryptionFailed) => {
"Keystore: encrypted (keychain entry could not unlock this keystore; \
provide the passphrase when the owner keystore is consumed or remove the stale \
keychain entry for this path)"
.into()
}
OwnerKeychainLoadError::Crypto(crate::crypto::CryptoError::KeychainUnavailable {
OwnerKeychainLoadError::Crypto(mesh_llm_identity::CryptoError::KeychainUnavailable {
reason,
}) => format!("Keystore: encrypted (keychain unavailable: {reason})"),
OwnerKeychainLoadError::Crypto(crate::crypto::CryptoError::KeychainAccessDenied {
OwnerKeychainLoadError::Crypto(mesh_llm_identity::CryptoError::KeychainAccessDenied {
reason,
}) => format!(
"Keystore: encrypted (keychain is locked or access was denied: {reason}; \
@ -781,143 +906,4 @@ fn should_default_to_keychain(
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn defaults_to_keychain_for_new_keystore_when_available() {
assert!(should_default_to_keychain(false, false, true));
}
#[test]
fn does_not_default_to_keychain_for_existing_keystore() {
assert!(!should_default_to_keychain(true, false, true));
}
#[test]
fn does_not_default_to_keychain_when_unavailable() {
assert!(!should_default_to_keychain(false, false, false));
}
#[test]
fn does_not_default_to_keychain_with_no_passphrase() {
assert!(!should_default_to_keychain(false, true, true));
}
#[test]
fn reports_stale_keychain_entry_as_encrypted_keystore() {
let message = encrypted_keystore_keychain_status(OwnerKeychainLoadError::Crypto(
crate::crypto::CryptoError::DecryptionFailed,
));
assert!(message.contains("keychain entry could not unlock this keystore"));
assert!(message.contains("remove the stale keychain entry for this path"));
}
#[test]
#[serial]
fn force_keychain_save_failure_restores_previous_secret() {
if !crate::crypto::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let tmp_dir =
std::env::temp_dir().join(format!("mesh-llm-force-rollback-{}", rand::random::<u64>()));
std::fs::create_dir_all(&tmp_dir).unwrap();
let blocking_file = tmp_dir.join("blocker");
std::fs::write(&blocking_file, b"not a directory").unwrap();
let bad_path = blocking_file.join("owner-keystore.json");
let account = crate::crypto::owner_keychain_account_for_path(&bad_path);
let previous_secret = "previous-unlock-secret-do-not-lose";
crate::crypto::keychain_set(KEYCHAIN_SERVICE, &account, previous_secret).unwrap();
let result = run_init(Some(bad_path.clone()), true, false, true);
assert!(
result.is_err(),
"run_init must fail when save cannot succeed"
);
let restored = crate::crypto::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert_eq!(
restored.as_deref(),
Some(previous_secret),
"previous keychain secret must be restored after failed force-init"
);
crate::crypto::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
std::fs::remove_dir_all(&tmp_dir).ok();
}
#[test]
#[serial]
fn fresh_keychain_save_failure_leaves_no_orphan() {
if !crate::crypto::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let tmp_dir =
std::env::temp_dir().join(format!("mesh-llm-fresh-rollback-{}", rand::random::<u64>()));
std::fs::create_dir_all(&tmp_dir).unwrap();
let blocking_file = tmp_dir.join("blocker");
std::fs::write(&blocking_file, b"not a directory").unwrap();
let bad_path = blocking_file.join("owner-keystore.json");
let account = crate::crypto::owner_keychain_account_for_path(&bad_path);
crate::crypto::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
let result = run_init(Some(bad_path.clone()), false, false, true);
assert!(
result.is_err(),
"run_init must fail when save cannot succeed"
);
let residual = crate::crypto::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert_eq!(
residual, None,
"a fresh init failure must leave no keychain entry behind"
);
std::fs::remove_dir_all(&tmp_dir).ok();
}
#[test]
#[serial]
fn init_defaults_to_keychain_then_load_round_trip() {
if !crate::crypto::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let dir =
std::env::temp_dir().join(format!("mesh-llm-keychain-rt-{}", rand::random::<u64>()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("owner-keystore.json");
run_init(Some(path.clone()), false, false, false)
.expect("auth init should default to keychain when available");
assert!(path.exists(), "keystore file should exist");
let info = keystore_metadata(&path).unwrap();
assert!(
info.encrypted,
"keystore should be encrypted when using keychain"
);
let account = crate::crypto::owner_keychain_account_for_path(&path);
let stored = crate::crypto::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert!(
stored.is_some(),
"keychain must have a passphrase entry for this keystore path"
);
let kp = load_owner_keypair_from_keychain(&path).expect("load via keychain must succeed");
assert_eq!(kp.owner_id(), info.owner_id);
crate::crypto::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
std::fs::remove_dir_all(&dir).ok();
}
}
mod tests;

View file

@ -0,0 +1,137 @@
use super::*;
use serial_test::serial;
#[test]
fn defaults_to_keychain_for_new_keystore_when_available() {
assert!(should_default_to_keychain(false, false, true));
}
#[test]
fn does_not_default_to_keychain_for_existing_keystore() {
assert!(!should_default_to_keychain(true, false, true));
}
#[test]
fn does_not_default_to_keychain_when_unavailable() {
assert!(!should_default_to_keychain(false, false, false));
}
#[test]
fn does_not_default_to_keychain_with_no_passphrase() {
assert!(!should_default_to_keychain(false, true, true));
}
#[test]
fn reports_stale_keychain_entry_as_encrypted_keystore() {
let message = encrypted_keystore_keychain_status(OwnerKeychainLoadError::Crypto(
mesh_llm_identity::CryptoError::DecryptionFailed,
));
assert!(message.contains("keychain entry could not unlock this keystore"));
assert!(message.contains("remove the stale keychain entry for this path"));
}
#[test]
#[serial]
fn force_keychain_save_failure_restores_previous_secret() {
if !mesh_llm_identity::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let tmp_dir =
std::env::temp_dir().join(format!("mesh-llm-force-rollback-{}", rand::random::<u64>()));
std::fs::create_dir_all(&tmp_dir).unwrap();
let blocking_file = tmp_dir.join("blocker");
std::fs::write(&blocking_file, b"not a directory").unwrap();
let bad_path = blocking_file.join("owner-keystore.json");
let account = mesh_llm_identity::owner_keychain_account_for_path(&bad_path);
let previous_secret = "previous-unlock-secret-do-not-lose";
mesh_llm_identity::keychain_set(KEYCHAIN_SERVICE, &account, previous_secret).unwrap();
let result = run_init(Some(bad_path.clone()), true, false, true);
assert!(
result.is_err(),
"run_init must fail when save cannot succeed"
);
let restored = mesh_llm_identity::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert_eq!(
restored.as_deref(),
Some(previous_secret),
"previous keychain secret must be restored after failed force-init"
);
mesh_llm_identity::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
std::fs::remove_dir_all(&tmp_dir).ok();
}
#[test]
#[serial]
fn fresh_keychain_save_failure_leaves_no_orphan() {
if !mesh_llm_identity::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let tmp_dir =
std::env::temp_dir().join(format!("mesh-llm-fresh-rollback-{}", rand::random::<u64>()));
std::fs::create_dir_all(&tmp_dir).unwrap();
let blocking_file = tmp_dir.join("blocker");
std::fs::write(&blocking_file, b"not a directory").unwrap();
let bad_path = blocking_file.join("owner-keystore.json");
let account = mesh_llm_identity::owner_keychain_account_for_path(&bad_path);
mesh_llm_identity::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
let result = run_init(Some(bad_path.clone()), false, false, true);
assert!(
result.is_err(),
"run_init must fail when save cannot succeed"
);
let residual = mesh_llm_identity::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert_eq!(
residual, None,
"a fresh init failure must leave no keychain entry behind"
);
std::fs::remove_dir_all(&tmp_dir).ok();
}
#[test]
#[serial]
fn init_defaults_to_keychain_then_load_round_trip() {
if !mesh_llm_identity::keychain_available() {
eprintln!("keychain backend unavailable, skipping");
return;
}
let dir = std::env::temp_dir().join(format!("mesh-llm-keychain-rt-{}", rand::random::<u64>()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("owner-keystore.json");
run_init(Some(path.clone()), false, false, false)
.expect("auth init should default to keychain when available");
assert!(path.exists(), "keystore file should exist");
let info = keystore_metadata(&path).unwrap();
assert!(
info.encrypted,
"keystore should be encrypted when using keychain"
);
let account = mesh_llm_identity::owner_keychain_account_for_path(&path);
let stored = mesh_llm_identity::keychain_get(KEYCHAIN_SERVICE, &account).unwrap();
assert!(
stored.is_some(),
"keychain must have a passphrase entry for this keystore path"
);
let kp = load_owner_keypair_from_keychain(&path).expect("load via keychain must succeed");
assert_eq!(kp.owner_id(), info.owner_id);
mesh_llm_identity::keychain_delete(KEYCHAIN_SERVICE, &account).ok();
std::fs::remove_dir_all(&dir).ok();
}

View file

@ -1,10 +1,9 @@
use anyhow::Result;
use mesh_llm_cli::benchmark::{BenchmarkCommand, GpuBenchmarkBackend, PromptImportSource};
use mesh_llm_system::benchmark;
use mesh_llm_system::benchmark_prompts::{self, ImportPromptsArgs};
use crate::cli::benchmark::{BenchmarkCommand, GpuBenchmarkBackend, PromptImportSource};
use crate::system::benchmark;
use crate::system::benchmark_prompts::{self, ImportPromptsArgs};
pub(crate) async fn dispatch_benchmark_command(command: &BenchmarkCommand) -> Result<()> {
pub async fn dispatch_benchmark_command(command: &BenchmarkCommand) -> Result<()> {
match command {
BenchmarkCommand::ImportPrompts {
source,
@ -17,7 +16,7 @@ pub(crate) async fn dispatch_benchmark_command(command: &BenchmarkCommand) -> Re
limit: *limit,
max_tokens: *max_tokens,
output: output.clone(),
user_agent_version: crate::VERSION,
user_agent_version: env!("CARGO_PKG_VERSION"),
};
benchmark_prompts::import_prompt_corpus(args).await
}

View file

@ -1,21 +1,19 @@
use anyhow::{Context, Result};
use serde_json::{Value, json};
use crate::cli::GpuCommand;
use crate::system::{
use mesh_llm_cli::GpuCommand;
use mesh_llm_system::{
benchmark::{self, SavedBenchmark},
hardware::{self, GpuFacts, HardwareSurvey},
};
use serde_json::{Value, json};
pub(crate) fn dispatch_gpu_command(json_output: bool, command: Option<&GpuCommand>) -> Result<()> {
pub fn dispatch_gpu_command(json_output: bool, command: Option<&GpuCommand>) -> Result<()> {
match command {
Some(GpuCommand::Detect { json }) => run_gpu_benchmark(json_output || *json),
None => run_gpus(json_output),
}
}
pub(crate) fn run_gpus(json_output: bool) -> Result<()> {
pub fn run_gpus(json_output: bool) -> Result<()> {
let mut hw = hardware::survey();
attach_cached_bandwidth(&mut hw);

View file

@ -0,0 +1,11 @@
#![forbid(unsafe_code)]
pub mod agent_cli;
pub mod auth;
pub mod benchmark;
pub mod gpus;
pub mod model_package;
pub mod plugin;
pub mod runtime_native;
pub mod skills;
pub mod update;

View file

@ -1,14 +1,14 @@
use anyhow::{Context, Result, bail};
use tokio_stream::StreamExt;
use model_package::jobs::HfJobsClient;
use model_package::permissions;
use model_package::prepare::{self, DiscoveredQuant, PrepareParams};
use model_package::script;
use ::model_package::jobs::HfJobsClient;
use ::model_package::permissions;
use ::model_package::prepare::{self, DiscoveredQuant, PrepareParams};
use ::model_package::script;
use serde_json::json;
/// All CLI arguments for `model-package`, bundled to avoid too-many-arguments.
pub(crate) struct ModelPrepareArgs<'a> {
pub struct ModelPrepareArgs<'a> {
pub source_repo: Option<&'a str>,
pub quant: Option<&'a str>,
pub target: Option<&'a str>,
@ -28,7 +28,7 @@ pub(crate) struct ModelPrepareArgs<'a> {
}
/// Dispatch the model-package command.
pub(crate) async fn dispatch_model_package(args: ModelPrepareArgs<'_>) -> Result<()> {
pub async fn dispatch_model_package(args: ModelPrepareArgs<'_>) -> Result<()> {
let ModelPrepareArgs {
source_repo,
quant,
@ -90,7 +90,7 @@ pub(crate) async fn dispatch_model_package(args: ModelPrepareArgs<'_>) -> Result
};
// Build HF client for API calls.
let hf_client = model_package::build_hf_client()?;
let hf_client = ::model_package::build_hf_client()?;
// If no quant specified, list available quants and exit.
// This path doesn't need HF_TOKEN — works for public repos.
@ -309,7 +309,7 @@ fn print_quant_table(quants: &[DiscoveredQuant]) {
async fn run_update_script() -> Result<()> {
eprintln!("📤 Uploading embedded script to meshllm/layer-split-output bucket...");
let client = model_package::build_hf_client()?;
let client = ::model_package::build_hf_client()?;
// Check permissions first.
let perms = permissions::check_permissions(&client).await?;
@ -354,7 +354,7 @@ async fn run_status(client: &HfJobsClient, job_id: &str, json_output: bool) -> R
}
async fn run_logs(client: &HfJobsClient, job_id: &str, json_output: bool) -> Result<()> {
use model_package::jobs::JobStage;
use ::model_package::jobs::JobStage;
let (namespace, id) = parse_job_id(job_id).await?;
@ -403,7 +403,7 @@ async fn run_cancel(client: &HfJobsClient, job_id: &str, json_output: bool) -> R
async fn run_list(client: &HfJobsClient, json_output: bool) -> Result<()> {
// We need to know the namespace — resolve via whoami.
let hf_client = model_package::build_hf_client()?;
let hf_client = ::model_package::build_hf_client()?;
let perms = permissions::check_permissions(&hf_client).await?;
let jobs = client.list(&perms.namespace).await?;
@ -433,7 +433,7 @@ async fn run_list(client: &HfJobsClient, json_output: bool) -> Result<()> {
/// Follow job logs until the job reaches a terminal state.
async fn follow_until_done(client: &HfJobsClient, namespace: &str, job_id: &str) -> Result<()> {
use model_package::jobs::JobStage;
use ::model_package::jobs::JobStage;
loop {
loop {
@ -523,7 +523,7 @@ async fn ensure_bucket_script_current(client: &hf_hub::HFClient) -> Result<()> {
}
}
fn redacted_spec(spec: &model_package::jobs::JobSpec) -> model_package::jobs::JobSpec {
fn redacted_spec(spec: &::model_package::jobs::JobSpec) -> ::model_package::jobs::JobSpec {
let mut redacted = spec.clone();
for value in redacted.secrets.values_mut() {
if value.len() > 8 {
@ -557,7 +557,7 @@ async fn parse_job_id(job_id: &str) -> Result<(String, String)> {
Ok((ns.to_string(), id.to_string()))
} else {
// Need to figure out namespace from the user's identity.
let hf_client = model_package::build_hf_client()?;
let hf_client = ::model_package::build_hf_client()?;
let perms = permissions::check_permissions(&hf_client).await?;
Ok((perms.namespace, job_id.to_string()))
}

View file

@ -7,11 +7,34 @@ use mesh_llm_plugin_manager::{
};
use reqwest::Client;
use crate::cli::terminal_progress::{SpinnerHandle, clear_stderr_line, start_spinner};
use crate::cli::{Cli, PluginCommand};
use crate::runtime;
use mesh_llm_cli::PluginCommand;
use mesh_llm_tui::terminal_progress::{SpinnerHandle, clear_stderr_line, start_spinner};
pub(crate) async fn run_plugin_command(command: &PluginCommand, cli: &Cli) -> Result<()> {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PluginListRows {
pub externals: Vec<RuntimePluginRow>,
pub inactive: Vec<InactivePluginRow>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RuntimePluginRow {
pub name: String,
pub command: String,
pub args: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InactivePluginRow {
pub name: String,
pub kind: String,
pub status: String,
pub error: Option<String>,
}
pub async fn run_plugin_command(
command: &PluginCommand,
runtime_rows: Option<&PluginListRows>,
) -> Result<bool> {
match command {
PluginCommand::Install { reference } => install(reference).await?,
PluginCommand::Update { name } => update(name).await?,
@ -20,9 +43,14 @@ pub(crate) async fn run_plugin_command(command: &PluginCommand, cli: &Cli) -> Re
PluginCommand::Delete { name } => delete(name)?,
PluginCommand::Info { name } => info(name)?,
PluginCommand::Search { query } => search(query.as_deref()).await?,
PluginCommand::List => list(cli)?,
PluginCommand::List => {
let Some(runtime_rows) = runtime_rows else {
return Ok(false);
};
list(runtime_rows)?;
}
}
Ok(())
Ok(true)
}
async fn install(reference: &str) -> Result<()> {
@ -113,7 +141,7 @@ async fn search(query: Option<&str>) -> Result<()> {
Ok(())
}
fn list(cli: &Cli) -> Result<()> {
fn list(runtime_rows: &PluginListRows) -> Result<()> {
let store = PluginStore::new(default_store_root()?);
for metadata in store.list()? {
let state = if metadata.enabled {
@ -127,8 +155,7 @@ fn list(cli: &Cli) -> Result<()> {
);
}
let resolved = runtime::load_resolved_plugins(cli)?;
for spec in resolved.externals {
for spec in &runtime_rows.externals {
println!(
"{}\tkind=runtime\tcommand={}\targs={}",
spec.name,
@ -136,13 +163,13 @@ fn list(cli: &Cli) -> Result<()> {
spec.args.join(" ")
);
}
for summary in resolved.inactive {
for summary in &runtime_rows.inactive {
println!(
"{}\tkind={}\tstate={}\terror={}",
summary.name,
summary.kind,
summary.status,
summary.error.unwrap_or_default()
summary.error.clone().unwrap_or_default()
);
}
Ok(())

View file

@ -0,0 +1,427 @@
use anyhow::Result;
use mesh_llm_native_runtime::{
HostRuntimeProfile, NativeRuntimePruneMode, NativeRuntimeResolver, RuntimeSelection,
};
use mesh_llm_runtime_install::{
CURRENT_MESH_VERSION, NativeRuntimeDownloadProgressCallback, NativeRuntimeInstallOptions,
NativeRuntimeInstallStatus, NativeRuntimeManifestOptions, host_runtime_profile,
install_native_runtime, load_release_manifest, native_runtime_cache,
};
use serde::Serialize;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub async fn run_native_runtime_list(
available: bool,
manifest_path: Option<&Path>,
bundle_dirs: &[PathBuf],
cache_dir: Option<&Path>,
json_output: bool,
) -> Result<()> {
let cache = native_runtime_cache(cache_dir)?;
if available {
if !json_output && manifest_path.is_none() && bundle_dirs.is_empty() {
eprintln!("🔎 Loading native runtime release manifest");
}
let manifest = load_release_manifest(NativeRuntimeManifestOptions {
manifest_path: manifest_path.map(Path::to_path_buf),
bundle_dirs: bundle_dirs.to_vec(),
..Default::default()
})
.await?;
let profile = host_runtime_profile();
let cache = native_runtime_cache(cache_dir)?;
let resolution = NativeRuntimeResolver::new(
CURRENT_MESH_VERSION,
profile.clone(),
manifest.clone(),
cache,
)
.resolve(&RuntimeSelection::Recommended)
.ok();
let rows = manifest
.artifacts
.iter()
.map(|artifact| {
let evaluation = resolution.as_ref().and_then(|resolution| {
resolution
.evaluated
.iter()
.find(|candidate| candidate.artifact.id == artifact.id)
});
let supported = evaluation.is_some_and(|candidate| candidate.compatible);
json!({
"id": artifact.id,
"mesh_version": artifact.mesh_version.as_deref(),
"skippy_abi": artifact.skippy_abi,
"backend": artifact.backend.kind.to_string(),
"os": artifact.platform.os,
"arch": artifact.platform.arch,
"supported": supported,
"rejection_reasons": evaluation.map(|candidate| &candidate.rejection_reasons),
"url": artifact.url.as_deref(),
})
})
.collect::<Vec<_>>();
if json_output {
println!("{}", serde_json::to_string_pretty(&rows)?);
} else {
print_available_runtimes(&rows);
}
return Ok(());
}
let installed = cache.installed()?;
if json_output {
println!("{}", serde_json::to_string_pretty(&installed)?);
} else {
print_installed_runtimes(&installed, cache.root());
}
Ok(())
}
pub async fn run_native_runtime_install(
requested_runtime: Option<&str>,
manifest_path: Option<&Path>,
bundle_dirs: &[PathBuf],
cache_dir: Option<&Path>,
json_output: bool,
) -> Result<()> {
let selection = RuntimeSelection::parse(requested_runtime)?;
if !json_output && manifest_path.is_none() && bundle_dirs.is_empty() {
eprintln!("🔎 Loading native runtime release manifest");
}
if !json_output {
eprintln!("🔎 Detecting host runtime profile");
}
let outcome = install_native_runtime(NativeRuntimeInstallOptions {
selection,
manifest_path: manifest_path.map(Path::to_path_buf),
bundle_dirs: bundle_dirs.to_vec(),
cache_dir: cache_dir.map(Path::to_path_buf),
progress: cli_download_progress(json_output),
..Default::default()
})
.await?;
match outcome.status {
NativeRuntimeInstallStatus::AlreadyInstalled => {
if json_output {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"status": "already_installed",
"runtime": outcome.runtime,
"resolution": outcome.resolution,
}))?
);
} else {
eprintln!(
"✅ Native runtime already installed: {}",
outcome.runtime.native_runtime_id
);
eprintln!(" path: {}", outcome.runtime.path.display());
}
}
NativeRuntimeInstallStatus::Installed => {
if json_output {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"status": "installed",
"runtime": outcome.runtime,
"resolution": outcome.resolution,
}))?
);
} else {
eprintln!("✅ Installed {}", outcome.runtime.native_runtime_id);
eprintln!(" version: {}", outcome.runtime.mesh_version);
eprintln!(" flavor: {}", outcome.runtime.flavor);
eprintln!(" path: {}", outcome.runtime.path.display());
}
}
}
Ok(())
}
struct DownloadProgress {
native_runtime_id: Option<String>,
last_percent: Option<u64>,
last_tick: Instant,
}
impl DownloadProgress {
fn new() -> Self {
Self {
native_runtime_id: None,
last_percent: None,
last_tick: Instant::now(),
}
}
fn tick(
&mut self,
native_runtime_id: &str,
downloaded: u64,
total: Option<u64>,
finished: bool,
) {
if self.native_runtime_id.is_none() {
self.native_runtime_id = Some(native_runtime_id.to_string());
eprintln!("⬇️ Downloading native runtime {native_runtime_id}");
}
if finished {
self.finish(downloaded);
return;
}
let should_print = match total {
Some(total) if total > 0 => {
let percent = downloaded.saturating_mul(100) / total;
let crossed_step = self
.last_percent
.map(|last| percent >= last.saturating_add(5))
.unwrap_or(true);
if crossed_step || percent == 100 {
self.last_percent = Some(percent);
true
} else {
false
}
}
_ => self.last_tick.elapsed() >= Duration::from_secs(1),
};
if should_print {
self.last_tick = Instant::now();
match total {
Some(total) if total > 0 => eprintln!(
" downloaded {} / {} ({})",
human_bytes(downloaded),
human_bytes(total),
format_args!("{}%", self.last_percent.unwrap_or(0))
),
_ => eprintln!(" downloaded {}", human_bytes(downloaded)),
}
}
}
fn finish(&mut self, downloaded: u64) {
eprintln!(" downloaded {}", human_bytes(downloaded));
}
}
fn cli_download_progress(json_output: bool) -> Option<NativeRuntimeDownloadProgressCallback> {
if json_output {
return None;
}
let progress = Arc::new(Mutex::new(DownloadProgress::new()));
Some(Arc::new(move |event| {
let Ok(mut progress) = progress.lock() else {
return;
};
progress.tick(
&event.native_runtime_id,
event.downloaded_bytes,
event.total_bytes,
event.finished,
);
}))
}
fn human_bytes(bytes: u64) -> String {
const UNITS: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
let mut value = bytes as f64;
let mut unit = UNITS[0];
for candidate in UNITS.iter().skip(1) {
if value < 1024.0 {
break;
}
value /= 1024.0;
unit = candidate;
}
if unit == "B" {
format!("{bytes} {unit}")
} else {
format!("{value:.1} {unit}")
}
}
pub fn run_native_runtime_remove(
native_runtime_id: &str,
mesh_version: Option<&str>,
cache_dir: Option<&Path>,
json_output: bool,
) -> Result<()> {
let version = mesh_version.unwrap_or(CURRENT_MESH_VERSION);
let cache = native_runtime_cache(cache_dir)?;
let removed = cache.remove(version, native_runtime_id)?;
if json_output {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"mesh_version": version,
"native_runtime_id": native_runtime_id,
"removed": removed,
}))?
);
} else if removed {
eprintln!("✅ Removed native runtime {native_runtime_id} for MeshLLM {version}");
} else {
eprintln!("🔎 Native runtime {native_runtime_id} for MeshLLM {version} was not installed");
}
Ok(())
}
pub fn run_native_runtime_prune(
active_only: bool,
mesh_version: Option<&str>,
cache_dir: Option<&Path>,
json_output: bool,
) -> Result<()> {
let version = mesh_version.unwrap_or(CURRENT_MESH_VERSION);
let mode = if active_only {
NativeRuntimePruneMode::ActiveOnly
} else {
NativeRuntimePruneMode::KeepActiveAndPrevious
};
let cache = native_runtime_cache(cache_dir)?;
let plan = cache.prune(version, mode)?;
if json_output {
println!("{}", serde_json::to_string_pretty(&plan)?);
} else if plan.remove_dirs.is_empty() {
eprintln!("✅ Native runtime cache already pruned");
} else {
eprintln!(
"✅ Pruned {} native runtime cache version(s)",
plan.remove_dirs.len()
);
for dir in plan.remove_dirs {
eprintln!(" removed: {}", dir.display());
}
}
Ok(())
}
pub fn run_native_runtime_doctor(json_output: bool) -> Result<()> {
let cache = native_runtime_cache(None)?;
let profile = host_runtime_profile();
let installed = cache.installed()?;
let current_version_runtimes = installed
.iter()
.filter(|runtime| runtime.mesh_version == CURRENT_MESH_VERSION)
.collect::<Vec<_>>();
let selected = current_version_runtimes
.iter()
.max_by_key(|runtime| runtime.manifest.runtime.backend.kind.default_rank());
let report = NativeRuntimeDoctorReport {
mesh_version: CURRENT_MESH_VERSION.to_string(),
host: profile,
cache_path: cache.root().to_path_buf(),
selected_runtime_id: selected.map(|runtime| runtime.native_runtime_id.clone()),
selected_runtime_flavor: selected.map(|runtime| runtime.flavor.clone()),
selected_runtime_path: selected.map(|runtime| runtime.path.clone()),
installed_count: installed.len(),
current_version_installed_count: current_version_runtimes.len(),
};
if json_output {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print_doctor_report(&report);
}
Ok(())
}
#[derive(Serialize)]
struct NativeRuntimeDoctorReport {
mesh_version: String,
host: HostRuntimeProfile,
cache_path: PathBuf,
selected_runtime_id: Option<String>,
selected_runtime_flavor: Option<String>,
selected_runtime_path: Option<PathBuf>,
installed_count: usize,
current_version_installed_count: usize,
}
fn print_available_runtimes(rows: &[serde_json::Value]) {
if rows.is_empty() {
println!("📦 No native runtime manifest entries found");
println!(" Pass --manifest or --bundle-dir to inspect available runtimes.");
return;
}
println!("📦 Available native runtimes");
for row in rows {
let status = if row["supported"].as_bool().unwrap_or(false) {
"compatible"
} else {
"not compatible"
};
println!(
" - {} {} ({}, {}/{})",
row["id"].as_str().unwrap_or("unknown"),
status,
row["backend"].as_str().unwrap_or("unknown"),
row["os"].as_str().unwrap_or("unknown"),
row["arch"].as_str().unwrap_or("unknown")
);
}
}
fn print_installed_runtimes(
installed: &[mesh_llm_native_runtime::InstalledNativeRuntime],
cache_root: &Path,
) {
if installed.is_empty() {
println!("📦 No native runtimes installed");
println!(" cache: {}", cache_root.display());
return;
}
println!("📦 Installed native runtimes");
println!(" cache: {}", cache_root.display());
for runtime in installed {
println!(
" - {} {} ({})",
runtime.native_runtime_id, runtime.mesh_version, runtime.flavor
);
println!(" path: {}", runtime.path.display());
}
}
fn print_doctor_report(report: &NativeRuntimeDoctorReport) {
println!("🩺 MeshLLM doctor");
println!();
println!("Native runtime:");
println!(" mesh version: {}", report.mesh_version);
println!(" cache: {}", report.cache_path.display());
println!(" host: {}/{}", report.host.os, report.host.arch);
let flavors = report
.host
.available_flavors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
println!(" detected flavors: {flavors}");
match &report.selected_runtime_id {
Some(id) => {
println!(" selected: {id}");
if let Some(flavor) = &report.selected_runtime_flavor {
println!(" flavor: {flavor}");
}
if let Some(path) = &report.selected_runtime_path {
println!(" path: {}", path.display());
}
}
None => {
println!(" selected: none");
println!(" status: no native runtime installed for this MeshLLM version");
}
}
println!(" installed: {}", report.installed_count);
println!(
" installed for current version: {}",
report.current_version_installed_count
);
}

View file

@ -4,9 +4,10 @@ use mesh_llm_plugin_manager::{
install_available_skills,
};
use crate::cli::{SkillAgentArg, SkillCommand, output::json_mode_enabled};
use mesh_llm_cli::{SkillAgentArg, SkillCommand};
use mesh_llm_tui::json_mode_enabled;
pub(crate) fn run_skills_command(command: &SkillCommand) -> Result<()> {
pub fn run_skills_command(command: &SkillCommand) -> Result<()> {
match command {
SkillCommand::Install {
agent,
@ -17,7 +18,7 @@ pub(crate) fn run_skills_command(command: &SkillCommand) -> Result<()> {
}
}
pub(crate) fn install_skills_for_agent(agent: SkillAgent) {
pub fn install_skills_for_agent(agent: SkillAgent) {
match PluginSkillInstallOptions::for_agent(agent).and_then(|options| {
let report = install_available_skills(&options)?;
Ok(report)
@ -41,7 +42,11 @@ fn install(agents: &[SkillAgentArg], all: bool, dry_run: bool, force: bool) -> R
options.skill_options.detected_only = false;
}
if !agents.is_empty() {
options.skill_options.agents = agents.iter().copied().map(Into::into).collect();
options.skill_options.agents = agents
.iter()
.copied()
.map(skill_agent_arg_to_manager)
.collect();
options.skill_options.detected_only = false;
}
let report = install_available_skills(&options)?;
@ -49,6 +54,17 @@ fn install(agents: &[SkillAgentArg], all: bool, dry_run: bool, force: bool) -> R
Ok(())
}
fn skill_agent_arg_to_manager(agent: SkillAgentArg) -> mesh_llm_plugin_manager::SkillAgent {
match agent {
SkillAgentArg::Global => mesh_llm_plugin_manager::SkillAgent::Global,
SkillAgentArg::Goose => mesh_llm_plugin_manager::SkillAgent::Goose,
SkillAgentArg::Pi => mesh_llm_plugin_manager::SkillAgent::Pi,
SkillAgentArg::Codex => mesh_llm_plugin_manager::SkillAgent::Codex,
SkillAgentArg::Opencode => mesh_llm_plugin_manager::SkillAgent::Opencode,
SkillAgentArg::Claude => mesh_llm_plugin_manager::SkillAgent::Claude,
}
}
fn print_agent_install_summary(agent: SkillAgent, report: &SkillInstallReport) {
if json_mode_enabled() {
return;

View file

@ -0,0 +1,35 @@
use anyhow::Result;
use mesh_llm_cli::{BinaryFlavor, Cli, Command};
use mesh_llm_system::{autoupdate, backend};
pub async fn run_update(cli: &Cli) -> Result<()> {
let (requested_version, flavor, detect_flavor) = match &cli.command {
Some(Command::Update {
version,
flavor,
detect_flavor,
}) => (
version.as_deref(),
binary_flavor_to_backend(*flavor),
*detect_flavor,
),
_ => (None, None, false),
};
autoupdate::run_update_command(autoupdate::UpdateCommandOptions {
flavor,
detect_flavor,
requested_version,
current_version: env!("CARGO_PKG_VERSION"),
})
.await
}
fn binary_flavor_to_backend(flavor: Option<BinaryFlavor>) -> Option<backend::BinaryFlavor> {
flavor.map(|flavor| match flavor {
BinaryFlavor::Cpu => backend::BinaryFlavor::Cpu,
BinaryFlavor::Cuda => backend::BinaryFlavor::Cuda,
BinaryFlavor::Rocm => backend::BinaryFlavor::Rocm,
BinaryFlavor::Vulkan => backend::BinaryFlavor::Vulkan,
BinaryFlavor::Metal => backend::BinaryFlavor::Metal,
})
}

View file

@ -2,16 +2,20 @@
name = "mesh-llm-config"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Configuration parsing and validation for mesh-llm"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
mesh-llm-types = { path = "../mesh-llm-types" }
mesh-llm-types = { path = "../mesh-llm-types", version = "0.68.0" }
semver = "1"
serde = { workspace = true }
skippy-protocol = { path = "../skippy-protocol" }
skippy-protocol = { path = "../skippy-protocol", version = "0.68.0" }
toml = "0.9"
toml_edit = "0.25"
dirs = "6.0.0"

View file

@ -2,11 +2,16 @@
name = "mesh-llm-console-server"
version.workspace = true
edition = "2021"
description = "Static file server for embedded Mesh LLM console assets"
license.workspace = true
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
mesh-llm-ui = { path = "../mesh-llm-ui", default-features = false }
mesh-llm-ui = { path = "../mesh-llm-ui", version = "0.68.0", default-features = false }
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "sync", "time"] }

View file

@ -0,0 +1,25 @@
[package]
name = "mesh-llm-embedded-runtime"
edition.workspace = true
license.workspace = true
version.workspace = true
description = "In-process full Mesh LLM node embedding API"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
keywords = ["llm", "mesh", "runtime", "embedding"]
categories = ["api-bindings", "network-programming"]
[features]
default = []
web-ui = ["mesh-llm-host-runtime/web-ui"]
dynamic-native-runtime = ["mesh-llm-host-runtime/dynamic-native-runtime"]
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
mesh-llm-host-runtime = { path = "../mesh-llm-host-runtime", version = "0.68.0", default-features = false }
serde_json.workspace = true

View file

@ -0,0 +1,36 @@
# mesh-llm-embedded-runtime
`mesh-llm-embedded-runtime` exposes the in-process full Mesh LLM node API for
applications that want a local OpenAI-compatible `/v1` endpoint without
spawning the `mesh-llm` CLI as a sidecar.
This crate is intentionally separate from the default `mesh-llm-sdk` facade so
client-only consumers do not pull in the full host runtime graph.
## Example
```rust,no_run
use mesh_llm_embedded_runtime::{
EmbeddedMeshNodeConfig, EmbeddedMeshNodeMode, start_embedded_node,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let node = start_embedded_node(
EmbeddedMeshNodeConfig::builder()
.mode(EmbeddedMeshNodeMode::Serve)
.model("unsloth/Qwen3-0.6B-GGUF:Q4_K_M")
.api_port(9337)
.console_port(3131)
.build(),
)
.await?;
println!("OpenAI API: {}", node.api_base_url());
println!("console: {}", node.console_url());
node.stop().await?;
Ok(())
}
```

View file

@ -0,0 +1,19 @@
#![forbid(unsafe_code)]
pub use mesh_llm_host_runtime::sdk::{
EmbeddedChatMessage, EmbeddedMeshAdmissionConfig, EmbeddedMeshDiscoveryMode,
EmbeddedMeshHttpConfig, EmbeddedMeshLogFormat, EmbeddedMeshNetworkConfig,
EmbeddedMeshNodeBuilder, EmbeddedMeshNodeConfig, EmbeddedMeshNodeHandle, EmbeddedMeshNodeMode,
EmbeddedMeshNodeStatus, EmbeddedMeshRequirementsConfig, EmbeddedMeshServingConfig,
EmbeddedMeshStorageConfig, EmbeddedServeConfig, EmbeddedServeHandle, EmbeddedServeMode,
EmbeddedServeStatus, EmbeddedServingController, EmbeddedTrustPolicy,
SIGNED_JOIN_TOKEN_MIN_PROTOCOL_VERSION, start_embedded_node, start_embedded_serve,
};
pub mod config {
pub use mesh_llm_host_runtime::sdk::config::*;
}
pub mod native_runtime {
pub use mesh_llm_host_runtime::sdk::native_runtime::*;
}

View file

@ -0,0 +1,19 @@
[package]
name = "mesh-llm-events"
edition.workspace = true
license.workspace = true
version.workspace = true
description = "Shared runtime event and output contracts for mesh-llm"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
keywords = ["llm", "mesh", "events"]
categories = ["command-line-interface"]
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
serde_json.workspace = true

View file

@ -0,0 +1,21 @@
# mesh-llm-events
`mesh-llm-events` owns the typed event contract shared by the mesh runtime,
CLI, SDK-facing embedded runtime, and terminal UI.
The crate intentionally does not render anything. It defines the structured
values that runtime code can emit and that presentation layers such as
`mesh-llm-tui` can render as pretty terminal output, TUI dashboard state, or
JSONL records.
## API Shape
- `LogFormat` selects pretty terminal output or JSONL.
- `OutputEvent` is the structured runtime event taxonomy.
- `RuntimeStatus`, `DashboardSnapshot`, and related dashboard row types are the
shared status model consumed by the TUI.
- `DashboardSnapshotProvider` lets runtime code provide periodic dashboard
snapshots without depending on a renderer.
Rendering, progress bars, alternate-screen handling, and terminal control stay
in `mesh-llm-tui`.

View file

@ -0,0 +1,876 @@
#![forbid(unsafe_code)]
use clap::ValueEnum;
use serde_json::Value;
use std::future::Future;
use std::io::{self, IsTerminal};
use std::pin::Pin;
use std::sync::{Arc, OnceLock, RwLock};
pub mod terminal_progress;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum LogFormat {
#[default]
Pretty,
Json,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RuntimeStatus {
NotReady,
Starting,
Loading,
Ready,
ShuttingDown,
Stopped,
Exited,
Warning,
Error,
}
impl RuntimeStatus {
pub fn as_str(&self) -> &'static str {
match self {
RuntimeStatus::NotReady => "NOT READY",
RuntimeStatus::Starting => "starting",
RuntimeStatus::Loading => "loading",
RuntimeStatus::Ready => "ready",
RuntimeStatus::ShuttingDown => "shutting down",
RuntimeStatus::Stopped => "stopped",
RuntimeStatus::Exited => "exited",
RuntimeStatus::Warning => "warning",
RuntimeStatus::Error => "error",
}
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConsoleSessionMode {
InteractiveDashboard,
Fallback,
None,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DashboardProcessRow {
pub name: String,
pub backend: String,
pub status: RuntimeStatus,
pub port: u16,
pub pid: u32,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DashboardEndpointRow {
pub label: String,
pub status: RuntimeStatus,
pub url: String,
pub port: u16,
pub pid: Option<u32>,
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub struct DashboardModelRow {
pub name: String,
pub role: Option<String>,
pub status: RuntimeStatus,
pub port: Option<u16>,
pub device: Option<String>,
pub slots: Option<usize>,
pub quantization: Option<String>,
pub ctx_size: Option<u32>,
pub ctx_used_tokens: Option<u64>,
pub lanes: Option<Vec<DashboardModelLane>>,
pub file_size_gb: Option<f64>,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DashboardModelLane {
pub index: usize,
pub active: bool,
}
#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DashboardAcceptedRequestBucket {
pub second_offset: u32,
pub accepted_count: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ModelProgressStatus {
Ensuring,
Downloading,
Ready,
}
impl ModelProgressStatus {
pub fn as_str(&self) -> &'static str {
match self {
ModelProgressStatus::Ensuring => "ensuring",
ModelProgressStatus::Downloading => "downloading",
ModelProgressStatus::Ready => "ready",
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub struct DashboardSnapshot {
pub llama_process_rows: Vec<DashboardProcessRow>,
pub webserver_rows: Vec<DashboardEndpointRow>,
pub loaded_model_rows: Vec<DashboardModelRow>,
pub current_inflight_requests: u64,
pub accepted_request_buckets: Vec<DashboardAcceptedRequestBucket>,
pub latency_samples_ms: Vec<u64>,
}
impl Default for DashboardSnapshot {
fn default() -> Self {
Self {
llama_process_rows: Vec::new(),
webserver_rows: Vec::new(),
loaded_model_rows: Vec::new(),
current_inflight_requests: 0,
accepted_request_buckets: (0..30)
.map(|second_offset| DashboardAcceptedRequestBucket {
second_offset,
accepted_count: 0,
})
.collect(),
latency_samples_ms: Vec::new(),
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DashboardLaunchPlan {
pub llama_process_rows: Vec<DashboardProcessRow>,
pub webserver_rows: Vec<DashboardEndpointRow>,
pub loaded_model_rows: Vec<DashboardModelRow>,
}
#[allow(dead_code)]
pub type DashboardSnapshotFuture<'a> = Pin<Box<dyn Future<Output = DashboardSnapshot> + Send + 'a>>;
#[allow(dead_code)]
pub trait DashboardSnapshotProvider: Send + Sync {
fn snapshot(&self) -> DashboardSnapshotFuture<'_>;
}
pub type OutputSinkFuture<'a, T> = Pin<Box<dyn Future<Output = io::Result<T>> + Send + 'a>>;
pub trait OutputSink: Send + Sync {
fn emit_event(&self, event: OutputEvent) -> io::Result<()>;
fn schedule_ready_prompt(&self) -> io::Result<()> {
Ok(())
}
fn write_ready_prompt(&self) -> io::Result<()> {
Ok(())
}
fn ready_prompt_active(&self) -> bool {
false
}
fn flush(&self) -> OutputSinkFuture<'_, ()> {
Box::pin(async { Ok(()) })
}
fn mode(&self) -> LogFormat {
LogFormat::Pretty
}
fn console_session_mode(&self) -> Option<ConsoleSessionMode> {
None
}
fn register_dashboard_snapshot_provider(&self, _provider: Arc<dyn DashboardSnapshotProvider>) {}
fn enter_tui(&self) -> OutputSinkFuture<'_, ()> {
Box::pin(async { Ok(()) })
}
fn exit_tui(&self) -> OutputSinkFuture<'_, ()> {
Box::pin(async { Ok(()) })
}
fn dispatch_tui_event(&self, _event: TuiEvent) -> OutputSinkFuture<'_, TuiControlFlow> {
Box::pin(async { Ok(TuiControlFlow::Continue) })
}
fn render_tui_if_dirty(&self) -> OutputSinkFuture<'_, bool> {
Box::pin(async { Ok(false) })
}
fn force_restore_tui_terminal(&self) -> io::Result<()> {
Ok(())
}
}
static OUTPUT_SINK: OnceLock<RwLock<Option<Arc<dyn OutputSink>>>> = OnceLock::new();
fn output_sink_slot() -> &'static RwLock<Option<Arc<dyn OutputSink>>> {
OUTPUT_SINK.get_or_init(|| RwLock::new(None))
}
pub fn set_output_sink(sink: Arc<dyn OutputSink>) {
if let Ok(mut slot) = output_sink_slot().write() {
*slot = Some(sink);
}
}
pub fn clear_output_sink() {
if let Ok(mut slot) = output_sink_slot().write() {
*slot = None;
}
}
pub fn output_sink() -> Option<Arc<dyn OutputSink>> {
output_sink_slot()
.read()
.ok()
.and_then(|slot| slot.as_ref().cloned())
}
pub fn emit_event(event: OutputEvent) -> io::Result<()> {
match output_sink() {
Some(sink) => sink.emit_event(event),
None => Ok(()),
}
}
pub async fn flush_output() -> io::Result<()> {
match output_sink() {
Some(sink) => sink.flush().await,
None => Ok(()),
}
}
pub fn schedule_ready_prompt() -> io::Result<()> {
match output_sink() {
Some(sink) => sink.schedule_ready_prompt(),
None => Ok(()),
}
}
pub fn json_mode_enabled() -> bool {
output_sink().is_some_and(|sink| matches!(sink.mode(), LogFormat::Json))
}
pub fn interactive_tui_active() -> bool {
output_sink().is_some_and(|sink| {
matches!(sink.mode(), LogFormat::Pretty)
&& matches!(
sink.console_session_mode(),
Some(ConsoleSessionMode::InteractiveDashboard)
)
})
}
pub fn current_console_session_mode() -> ConsoleSessionMode {
console_session_mode(
std::io::stdin().is_terminal(),
std::io::stderr().is_terminal(),
)
}
pub fn console_session_mode(stdin_is_tty: bool, stderr_is_tty: bool) -> ConsoleSessionMode {
console_session_mode_for_term(
stdin_is_tty,
stderr_is_tty,
std::env::var("TERM").ok().as_deref(),
)
}
pub fn console_session_mode_for_term(
stdin_is_tty: bool,
stderr_is_tty: bool,
term: Option<&str>,
) -> ConsoleSessionMode {
if stdin_is_tty && stderr_is_tty && terminal_supports_dashboard(term) {
ConsoleSessionMode::InteractiveDashboard
} else {
ConsoleSessionMode::Fallback
}
}
fn terminal_supports_dashboard(term: Option<&str>) -> bool {
match term.map(str::trim).filter(|term| !term.is_empty()) {
Some(term) => term != "dumb",
None => false,
}
}
pub fn sort_dashboard_endpoint_rows(rows: &mut [DashboardEndpointRow]) {
rows.sort_by(|left, right| {
dashboard_endpoint_sort_bucket(left)
.cmp(&dashboard_endpoint_sort_bucket(right))
.then_with(|| left.label.cmp(&right.label))
});
}
fn dashboard_endpoint_sort_bucket(row: &DashboardEndpointRow) -> u8 {
if row.label.starts_with("Plugin: ") {
1
} else {
0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TuiKeyEvent {
Tab,
BackTab,
Backspace,
Enter,
Escape,
Left,
Right,
Up,
Down,
PageUp,
PageDown,
Interrupt,
Char(char),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TuiEvent {
Key(TuiKeyEvent),
Resize { columns: u16, rows: u16 },
MouseDown { column: u16, row: u16 },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TuiControlFlow {
Continue,
Quit,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutputLevel {
Debug,
Info,
Warn,
Error,
}
impl OutputLevel {
pub fn as_str(&self) -> &'static str {
match self {
OutputLevel::Debug => "debug",
OutputLevel::Info => "info",
OutputLevel::Warn => "warn",
OutputLevel::Error => "error",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LlamaInstanceKind {
LlamaServer,
}
impl LlamaInstanceKind {
pub fn as_str(&self) -> &'static str {
match self {
LlamaInstanceKind::LlamaServer => "llama-server",
}
}
pub fn sort_key(&self) -> u8 {
match self {
LlamaInstanceKind::LlamaServer => 0,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub enum OutputEvent {
Info {
message: String,
context: Option<String>,
},
Startup {
version: String,
message: Option<String>,
},
LaunchPlan {
plan: DashboardLaunchPlan,
},
NodeIdentity {
node_id: String,
mesh_id: Option<String>,
},
InviteToken {
token: String,
mesh_id: String,
mesh_name: Option<String>,
},
DiscoveryStarting {
source: String,
},
MeshFound {
mesh: String,
peers: usize,
region: Option<String>,
},
DiscoveryJoined {
mesh: String,
},
DiscoveryFailed {
message: String,
detail: Option<String>,
},
WaitingForPeers {
detail: Option<String>,
},
PassiveMode {
role: String,
status: RuntimeStatus,
capacity_gb: Option<f64>,
models_on_disk: Option<Vec<String>>,
detail: Option<String>,
},
PeerJoined {
peer_id: String,
label: Option<String>,
},
PeerLeft {
peer_id: String,
reason: Option<String>,
},
ModelQueued {
model: String,
},
ModelLoading {
model: String,
source: Option<String>,
},
ModelLoaded {
model: String,
bytes: Option<u64>,
},
ModelUnloading {
model: String,
},
ModelUnloaded {
model: String,
},
HostElected {
model: String,
host: String,
role: Option<String>,
capacity_gb: Option<f64>,
},
RpcServerStarting {
port: u16,
device: String,
log_path: Option<String>,
},
RpcReady {
port: u16,
device: String,
log_path: Option<String>,
},
RpcStartupFailed {
port: u16,
device: String,
log_path: Option<String>,
detail: String,
},
LlamaStarting {
model: Option<String>,
http_port: u16,
ctx_size: Option<u32>,
log_path: Option<String>,
},
LlamaReady {
model: Option<String>,
port: u16,
ctx_size: Option<u32>,
log_path: Option<String>,
},
LlamaStartupFailed {
model: Option<String>,
http_port: u16,
ctx_size: Option<u32>,
log_path: Option<String>,
detail: String,
},
ModelReady {
model: String,
internal_port: Option<u16>,
role: Option<String>,
},
MultiModelMode {
count: usize,
models: Vec<String>,
},
WebserverStarting {
url: String,
},
WebserverReady {
url: String,
},
ApiStarting {
url: String,
},
ApiReady {
url: String,
},
RuntimeReady {
api_url: String,
console_url: Option<String>,
api_port: u16,
console_port: Option<u16>,
models_count: Option<usize>,
pi_command: Option<String>,
goose_command: Option<String>,
},
ModelDownloadProgress {
label: String,
file: Option<String>,
downloaded_bytes: Option<u64>,
total_bytes: Option<u64>,
status: ModelProgressStatus,
},
RequestRouted {
model: String,
target: String,
},
Warning {
message: String,
context: Option<String>,
},
Error {
message: String,
context: Option<String>,
},
ShutdownRequested {
signal: &'static str,
},
Shutdown {
reason: Option<String>,
},
LlamaNativeLog {
message: String,
category: &'static str,
params: Vec<(String, Value)>,
},
}
impl OutputEvent {
pub fn event_name(&self) -> &'static str {
match self {
OutputEvent::Info { .. } => "info",
OutputEvent::Startup { .. } => "startup",
OutputEvent::LaunchPlan { .. } => "launch_plan",
OutputEvent::NodeIdentity { .. } => "node_identity",
OutputEvent::InviteToken { .. } => "invite_token",
OutputEvent::DiscoveryStarting { .. } => "discovery_starting",
OutputEvent::MeshFound { .. } => "mesh_found",
OutputEvent::DiscoveryJoined { .. } => "discovery_joined",
OutputEvent::DiscoveryFailed { .. } => "discovery_failed",
OutputEvent::WaitingForPeers { .. } => "waiting_for_peers",
OutputEvent::PassiveMode { .. } => "passive_mode",
OutputEvent::PeerJoined { .. } => "peer_joined",
OutputEvent::PeerLeft { .. } => "peer_left",
OutputEvent::ModelQueued { .. } => "model_queued",
OutputEvent::ModelLoading { .. } => "model_loading",
OutputEvent::ModelLoaded { .. } => "model_loaded",
OutputEvent::ModelUnloading { .. } => "model_unloading",
OutputEvent::ModelUnloaded { .. } => "model_unloaded",
OutputEvent::HostElected { .. } => "host_elected",
OutputEvent::RpcServerStarting { .. } => "rpc_server_starting",
OutputEvent::RpcReady { .. } => "rpc_ready",
OutputEvent::RpcStartupFailed { .. } => "rpc_startup_failed",
OutputEvent::LlamaStarting { .. } => "llama_starting",
OutputEvent::LlamaReady { .. } => "llama_ready",
OutputEvent::LlamaStartupFailed { .. } => "llama_startup_failed",
OutputEvent::ModelReady { .. } => "model_ready",
OutputEvent::MultiModelMode { .. } => "multi_model_mode",
OutputEvent::WebserverStarting { .. } => "webserver_starting",
OutputEvent::WebserverReady { .. } => "webserver_ready",
OutputEvent::ApiStarting { .. } => "api_starting",
OutputEvent::ApiReady { .. } => "api_ready",
OutputEvent::RuntimeReady { .. } => "ready",
OutputEvent::ModelDownloadProgress { .. } => "model_download_progress",
OutputEvent::RequestRouted { .. } => "request_routed",
OutputEvent::Warning { .. } => "warning",
OutputEvent::Error { .. } => "error",
OutputEvent::ShutdownRequested { signal } => signal,
OutputEvent::Shutdown { .. } => "shutdown",
OutputEvent::LlamaNativeLog { category, .. } => category,
}
}
pub fn level(&self) -> OutputLevel {
match self {
OutputEvent::RpcStartupFailed { .. } | OutputEvent::LlamaStartupFailed { .. } => {
OutputLevel::Error
}
OutputEvent::LlamaNativeLog { .. } => OutputLevel::Debug,
OutputEvent::Warning { .. } => OutputLevel::Warn,
OutputEvent::Error { .. } => OutputLevel::Error,
_ => OutputLevel::Info,
}
}
pub fn message(&self) -> String {
match self {
OutputEvent::Info { message, .. } => message.clone(),
OutputEvent::Startup { message, .. } => message
.clone()
.unwrap_or_else(|| "mesh-llm starting".to_string()),
OutputEvent::LaunchPlan { plan } => format!(
"startup plan ready ({} process(es), {} endpoint(s), {} model(s))",
plan.llama_process_rows.len(),
plan.webserver_rows.len(),
plan.loaded_model_rows.len()
),
OutputEvent::NodeIdentity { node_id, mesh_id } => match mesh_id {
Some(mesh_id) => format!("node {node_id} joined mesh {mesh_id}"),
None => format!("node {node_id} initialized"),
},
OutputEvent::InviteToken {
mesh_id, mesh_name, ..
} => {
let mesh_label = format_invite_mesh_label(mesh_name.as_deref(), mesh_id);
format!("invite token ready for mesh {mesh_label}")
}
OutputEvent::DiscoveryStarting { source } => format!("discovering mesh via {source}"),
OutputEvent::MeshFound { mesh, peers, .. } => {
format!("discovered mesh {mesh} ({peers} peer(s))")
}
OutputEvent::DiscoveryJoined { mesh } => format!("joined mesh {mesh}"),
OutputEvent::DiscoveryFailed { message, detail } => match detail {
Some(detail) => format!("{message}: {detail}"),
None => message.clone(),
},
OutputEvent::WaitingForPeers { detail } => detail
.clone()
.unwrap_or_else(|| "waiting for peers".to_string()),
OutputEvent::PassiveMode {
role,
status,
capacity_gb,
models_on_disk,
detail,
} => {
let mut line = detail
.clone()
.unwrap_or_else(|| format!("{role} {}", status.as_str()));
if let Some(capacity_gb) = capacity_gb {
line.push_str(&format!(" ({capacity_gb:.1}GB capacity)"));
}
if let Some(models_on_disk) = models_on_disk
&& !models_on_disk.is_empty()
{
line.push_str(&format!(" models={}", models_on_disk.join(", ")));
}
line
}
OutputEvent::PeerJoined { peer_id, .. } => format!("peer {peer_id} joined"),
OutputEvent::PeerLeft { peer_id, .. } => format!("peer {peer_id} left"),
OutputEvent::ModelQueued { model } => format!("queued model {model}"),
OutputEvent::ModelLoading { model, .. } => format!("loading model {model}"),
OutputEvent::ModelLoaded { model, .. } => format!("loaded model {model}"),
OutputEvent::ModelUnloading { model } => format!("unloading model {model}"),
OutputEvent::ModelUnloaded { model } => format!("unloaded model {model}"),
OutputEvent::HostElected {
model, host, role, ..
} => match role {
Some(role) => format!("{model} elected {host} as {role}"),
None => format!("{model} elected {host} as host"),
},
OutputEvent::RpcServerStarting { port, log_path, .. } => {
let msg = format!("rpc-server starting on port {port}");
append_log_path(msg, log_path)
}
OutputEvent::RpcReady { port, log_path, .. } => {
let msg = format!("rpc-server ready on port {port}");
append_log_path(msg, log_path)
}
OutputEvent::RpcStartupFailed {
port,
detail,
log_path,
..
} => {
let msg = format!("rpc-server failed to start on port {port}: {detail}");
append_log_path(msg, log_path)
}
OutputEvent::LlamaStarting {
http_port,
log_path,
..
} => {
let msg = format!("llama-server starting on port {http_port}");
append_log_path(msg, log_path)
}
OutputEvent::LlamaReady { port, log_path, .. } => {
let msg = format!("llama-server ready on port {port}");
append_log_path(msg, log_path)
}
OutputEvent::LlamaStartupFailed {
model,
http_port,
detail,
log_path,
..
} => {
let msg = match model {
Some(model) => {
format!(
"llama-server failed to start for {model} on port {http_port}: {detail}"
)
}
None => format!("llama-server failed to start on port {http_port}: {detail}"),
};
append_log_path(msg, log_path)
}
OutputEvent::ModelReady {
model,
internal_port,
..
} => match internal_port {
Some(port) => format!("model {model} ready on port {port}"),
None => format!("model {model} ready"),
},
OutputEvent::WebserverStarting { url } => format!("web console starting at {url}"),
OutputEvent::WebserverReady { url } => format!("web console ready at {url}"),
OutputEvent::ApiStarting { url } => format!("api starting at {url}"),
OutputEvent::ApiReady { url } => format!("api ready at {url}"),
OutputEvent::RuntimeReady { .. } => "mesh-llm runtime ready".to_string(),
OutputEvent::ModelDownloadProgress {
label,
file,
downloaded_bytes,
total_bytes,
status,
} => format_model_download_progress_message(
label,
file.as_deref(),
*downloaded_bytes,
*total_bytes,
status,
),
OutputEvent::MultiModelMode { count, models } => {
if models.is_empty() {
format!("Multi-model mode: {count} model(s)")
} else {
format!("Multi-model mode: {count} model(s): {}", models.join(", "))
}
}
OutputEvent::RequestRouted { model, target } => {
format!("routed request for {model} to {target}")
}
OutputEvent::Warning { message, .. } => message.clone(),
OutputEvent::Error { message, .. } => message.clone(),
OutputEvent::ShutdownRequested { signal } => format!("shutdown requested ({signal})"),
OutputEvent::Shutdown { reason } => reason
.clone()
.unwrap_or_else(|| "mesh-llm shutting down".to_string()),
OutputEvent::LlamaNativeLog { message, .. } => message.clone(),
}
}
}
fn append_log_path(message: String, log_path: &Option<String>) -> String {
if let Some(path) = log_path {
format!("{message}\n ↳ log={path}")
} else {
message
}
}
fn format_invite_mesh_label(mesh_name: Option<&str>, mesh_id: &str) -> String {
match mesh_name.map(str::trim).filter(|name| !name.is_empty()) {
Some(name) => format!("{name} ({mesh_id})"),
None => mesh_id.to_string(),
}
}
pub fn format_model_download_progress_message(
label: &str,
file: Option<&str>,
downloaded_bytes: Option<u64>,
total_bytes: Option<u64>,
status: &ModelProgressStatus,
) -> String {
let target = file.unwrap_or(label);
if let Some(package) = label.strip_prefix("layer package ") {
return match status {
ModelProgressStatus::Ensuring => {
format!("ensuring layer package artifact {target} for {package}")
}
ModelProgressStatus::Downloading => match (downloaded_bytes, total_bytes) {
(Some(downloaded), Some(total)) if total > 0 => format!(
"downloading layer package artifact {target} for {package} {}/{}",
format_display_bytes(downloaded),
format_display_bytes(total)
),
(Some(downloaded), _) if downloaded > 0 => format!(
"downloading layer package artifact {target} for {package} {}",
format_display_bytes(downloaded)
),
_ => format!("downloading layer package artifact {target} for {package}"),
},
ModelProgressStatus::Ready => match total_bytes {
Some(total) if total > 0 => format!(
"layer package artifact {target} ready for {package} ({})",
format_display_bytes(total)
),
_ => format!("layer package artifact {target} ready for {package}"),
},
};
}
match status {
ModelProgressStatus::Ensuring => format!("ensuring model {target}"),
ModelProgressStatus::Downloading => match (downloaded_bytes, total_bytes) {
(Some(downloaded), Some(total)) if total > 0 => format!(
"downloading model {target} {}/{}",
format_display_bytes(downloaded),
format_display_bytes(total)
),
(Some(downloaded), _) if downloaded > 0 => {
format!(
"downloading model {target} {}",
format_display_bytes(downloaded)
)
}
_ => format!("downloading model {target}"),
},
ModelProgressStatus::Ready => match total_bytes {
Some(total) if total > 0 => {
format!("model {target} ready ({})", format_display_bytes(total))
}
_ => format!("model {target} ready"),
},
}
}
fn format_display_bytes(bytes: u64) -> String {
if bytes >= 1_000_000_000 {
format!("{:.1}GB", bytes as f64 / 1e9)
} else if bytes >= 1_000_000 {
format!("{:.0}MB", bytes as f64 / 1e6)
} else if bytes >= 1_000 {
format!("{:.0}KB", bytes as f64 / 1e3)
} else {
format!("{bytes}B")
}
}

View file

@ -7,8 +7,8 @@ use std::sync::{
use std::thread;
use std::time::Duration;
pub(crate) fn clear_stderr_line() -> Result<()> {
if crate::cli::output::json_mode_enabled() {
pub fn clear_stderr_line() -> Result<()> {
if crate::json_mode_enabled() {
return Ok(());
}
eprint!("\r\x1b[2K");
@ -18,13 +18,13 @@ pub(crate) fn clear_stderr_line() -> Result<()> {
Ok(())
}
pub(crate) struct SpinnerHandle {
pub struct SpinnerHandle {
done: Arc<AtomicBool>,
thread: Option<thread::JoinHandle<()>>,
}
impl SpinnerHandle {
pub(crate) fn finish(&mut self) {
pub fn finish(&mut self) {
self.done.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
@ -39,8 +39,8 @@ impl Drop for SpinnerHandle {
}
}
pub(crate) fn start_spinner(message: &str) -> SpinnerHandle {
if crate::cli::output::json_mode_enabled() {
pub fn start_spinner(message: &str) -> SpinnerHandle {
if crate::json_mode_enabled() {
return SpinnerHandle {
done: Arc::new(AtomicBool::new(true)),
thread: None,
@ -70,25 +70,25 @@ pub(crate) fn start_spinner(message: &str) -> SpinnerHandle {
}
}
pub(crate) struct DeterminateProgressLine {
pub struct DeterminateProgressLine {
prefix: String,
}
impl DeterminateProgressLine {
pub(crate) fn new(prefix: impl Into<String>) -> Self {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
pub(crate) fn draw_counts(
pub fn draw_counts(
&self,
label: &str,
current: usize,
total: usize,
detail: Option<&str>,
) -> Result<()> {
if crate::cli::output::json_mode_enabled() {
if crate::json_mode_enabled() {
return Ok(());
}
let percent = if total > 0 {

View file

@ -10,12 +10,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
[features]
default = []
host = ["mesh-llm-node"]
embedded-runtime = ["mesh-llm-host-runtime"]
embedded-runtime = []
[dependencies]
mesh-llm-api-server = { path = "../mesh-llm-api-server" }
mesh-llm-console-server = { path = "../mesh-llm-console-server" }
mesh-llm-host-runtime = { path = "../mesh-llm-host-runtime", default-features = false, optional = true }
mesh-llm-sdk = { path = "../mesh-llm-sdk", default-features = false, features = ["client", "node", "console", "serving"] }
mesh-llm-node = { path = "../mesh-llm-node", optional = true }
thiserror = "2"
tokio = { version = "1", features = ["rt-multi-thread"] }
@ -25,4 +23,4 @@ uniffi = "=0.31.0"
uniffi = { version = "=0.31.0", features = ["build"] }
[dev-dependencies]
mesh-llm-api-server = { path = "../mesh-llm-api-server" }
mesh-llm-sdk = { path = "../mesh-llm-sdk", default-features = false, features = ["client", "node", "console"] }

View file

@ -5,16 +5,19 @@ language bindings, including model management, inference, and serving control
when built with the host runtime feature.
This crate is the bridge used by the generated Swift and Kotlin SDKs. It should
stay thin and map the public Rust API from `crates/mesh-llm-api-server/` into an FFI-safe surface.
stay thin and map the canonical Rust API from `crates/mesh-llm-sdk/` into an
FFI-safe surface.
Layering:
- `crates/mesh-client/` implements low-level client behavior
- `crates/mesh-llm-api-server/` defines the public Rust SDK
- `crates/mesh-llm-api-client/` and `crates/mesh-llm-api-server/` implement the
lower-level Rust client and node APIs
- `crates/mesh-llm-sdk/` defines the public SDK facade and feature model
- `crates/mesh-llm-ffi/` adapts that SDK for cross-language consumers
The FFI layer should expose public model ids as the same full model refs used by
mesh and `/v1/models`; it should not derive identities from GGUF filenames.
Application code should usually depend on `crates/mesh-llm-api-server/` directly unless it is
building a non-Rust binding.
Application code should usually depend on `crates/mesh-llm-sdk/` directly unless
it is building a non-Rust binding.

View file

@ -1,16 +1,21 @@
use mesh_llm_api_server::OwnerKeypair;
use mesh_llm_api_server::events::{Event, EventListener as CoreEventListener};
use mesh_llm_api_server::{
ChatMessage, ChatRequest, ClientBuilder, DevicePolicy as ApiDevicePolicy, InviteToken,
MeshApiError, MeshClient, MeshNode, ModelKind as ApiModelKind, ModelSource as ApiModelSource,
PublicMeshQuery as ApiPublicMeshQuery, RequestId, ResponsesRequest,
ServingModelState as ApiServingModelState, UnloadModelOptions as ApiUnloadModelOptions,
UnloadTarget as ApiUnloadTarget, create_auto_client as sdk_create_auto_client,
create_auto_node as sdk_create_auto_node, discover_public_meshes as sdk_discover_public_meshes,
};
#[cfg(feature = "embedded-runtime")]
use mesh_llm_host_runtime::sdk::{EmbeddedChatMessage, EmbeddedServingController};
use mesh_llm_sdk::embedded_runtime::{EmbeddedChatMessage, EmbeddedServingController};
use mesh_llm_sdk::events::{Event, EventListener as CoreEventListener};
use mesh_llm_sdk::node as sdk_node;
use mesh_llm_sdk::node::{
DevicePolicy as ApiDevicePolicy, MeshNode, ModelKind as ApiModelKind,
ModelSource as ApiModelSource, ServingModelState as ApiServingModelState,
UnloadModelOptions as ApiUnloadModelOptions, UnloadTarget as ApiUnloadTarget,
create_auto_node as sdk_create_auto_node,
};
use mesh_llm_sdk::{
ChatMessage, ChatRequest, ClientBuilder, InviteToken, MeshApiError, MeshClient, OwnerKeypair,
PublicMeshQuery as ApiPublicMeshQuery, RequestId, ResponsesRequest,
create_auto_client as sdk_create_auto_client,
discover_public_meshes as sdk_discover_public_meshes,
};
use std::future::Future;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@ -63,6 +68,8 @@ pub enum FfiError {
ServingUnsupported(String),
#[error("console failed: {0}")]
ConsoleFailed(String),
#[error("native runtime failed: {0}")]
NativeRuntimeFailed(String),
}
#[derive(uniffi::Record)]
@ -311,11 +318,72 @@ pub enum ClientEvent {
Disconnected { reason: String },
}
#[derive(uniffi::Enum)]
pub enum NativeRuntimeVerificationPolicyNative {
RequireChecksum,
RequireChecksumAndSignature,
}
#[derive(uniffi::Enum)]
pub enum NativeRuntimePruneModeNative {
KeepActiveAndPrevious,
ActiveOnly,
}
#[derive(uniffi::Record)]
pub struct NativeRuntimeInstallOptionsNative {
pub mesh_version: Option<String>,
pub skippy_abi_version: Option<String>,
pub selection: String,
pub manifest_path: Option<String>,
pub manifest_url: Option<String>,
pub bundle_dirs: Vec<String>,
pub cache_dir: Option<String>,
pub verification_policy: NativeRuntimeVerificationPolicyNative,
pub allow_download: bool,
}
#[derive(uniffi::Record)]
pub struct NativeRuntimeDownloadProgressNative {
pub native_runtime_id: String,
pub url: String,
pub downloaded_bytes: u64,
pub total_bytes: Option<u64>,
pub finished: bool,
}
#[derive(uniffi::Record)]
pub struct InstalledNativeRuntimeNative {
pub mesh_version: String,
pub native_runtime_id: String,
pub flavor: String,
pub path: String,
pub skippy_abi_version: Option<String>,
}
#[derive(uniffi::Record)]
pub struct NativeRuntimeInstallOutcomeNative {
pub status: String,
pub runtime: InstalledNativeRuntimeNative,
pub selected_native_runtime_id: String,
pub selected_source: String,
}
#[derive(uniffi::Record)]
pub struct NativeRuntimePruneResultNative {
pub removed_dirs: Vec<String>,
}
#[uniffi::export(callback_interface)]
pub trait EventListener: Send + Sync {
fn on_event(&self, event: ClientEvent);
}
#[uniffi::export(callback_interface)]
pub trait NativeRuntimeProgressListener: Send + Sync {
fn on_progress(&self, event: NativeRuntimeDownloadProgressNative);
}
struct EventListenerBridge {
inner: Box<dyn EventListener>,
}
@ -359,7 +427,7 @@ pub struct MeshNodeHandle {
#[derive(uniffi::Object)]
pub struct ConsoleHandle {
inner: Mutex<Option<mesh_llm_console_server::ConsoleServerHandle>>,
inner: Mutex<Option<mesh_llm_sdk::console::ConsoleServerHandle>>,
url: String,
}
@ -374,6 +442,69 @@ pub fn generate_owner_keypair_hex() -> String {
OwnerKeypair::generate().to_hex()
}
#[uniffi::export]
pub fn current_mesh_version() -> String {
mesh_llm_sdk::native_runtime::CURRENT_MESH_VERSION.to_string()
}
#[uniffi::export]
pub fn current_skippy_abi_version() -> String {
mesh_llm_sdk::native_runtime::current_skippy_abi_version()
}
#[uniffi::export]
pub fn install_native_runtime(
options: NativeRuntimeInstallOptionsNative,
progress: Option<Box<dyn NativeRuntimeProgressListener>>,
) -> Result<NativeRuntimeInstallOutcomeNative, FfiError> {
let options = runtime_install_options(options, progress)?;
block_on(mesh_llm_sdk::native_runtime::install_native_runtime(
options,
))
.map(NativeRuntimeInstallOutcomeNative::from)
.map_err(map_native_runtime_error)
}
#[uniffi::export]
pub fn installed_native_runtimes(
cache_dir: Option<String>,
) -> Result<Vec<InstalledNativeRuntimeNative>, FfiError> {
native_runtime_cache(cache_dir)?
.installed()
.map(|runtimes| {
runtimes
.into_iter()
.map(InstalledNativeRuntimeNative::from)
.collect()
})
.map_err(map_native_runtime_error)
}
#[uniffi::export]
pub fn remove_native_runtime(
cache_dir: Option<String>,
mesh_version: String,
native_runtime_id: String,
) -> Result<bool, FfiError> {
native_runtime_cache(cache_dir)?
.remove(&mesh_version, &native_runtime_id)
.map_err(map_native_runtime_error)
}
#[uniffi::export]
pub fn prune_native_runtimes(
cache_dir: Option<String>,
active_mesh_version: Option<String>,
mode: NativeRuntimePruneModeNative,
) -> Result<NativeRuntimePruneResultNative, FfiError> {
let active_mesh_version = active_mesh_version
.unwrap_or_else(|| mesh_llm_sdk::native_runtime::CURRENT_MESH_VERSION.to_string());
native_runtime_cache(cache_dir)?
.prune(&active_mesh_version, mode.into())
.map(NativeRuntimePruneResultNative::from)
.map_err(map_native_runtime_error)
}
#[uniffi::export]
pub fn discover_public_meshes(query: PublicMeshQuery) -> Result<Vec<PublicMesh>, FfiError> {
block_on(sdk_discover_public_meshes(query.into()))
@ -587,7 +718,7 @@ impl MeshNodeHandle {
}
pub fn status(&self) -> ClientStatus {
let status = block_on(self.node.status().node()).unwrap_or(mesh_llm_api_server::Status {
let status = block_on(self.node.status().node()).unwrap_or(sdk_node::Status {
connected: false,
peer_count: 0,
});
@ -697,14 +828,10 @@ impl MeshNodeHandle {
}
pub fn search_models(&self, query: ModelSearchQuery) -> Result<Vec<ModelSummary>, FfiError> {
block_on(
self.node
.models()
.search(mesh_llm_api_server::ModelSearchQuery {
query: query.query,
limit: query.limit.map(|limit| limit as usize),
}),
)
block_on(self.node.models().search(sdk_node::ModelSearchQuery {
query: query.query,
limit: query.limit.map(|limit| limit as usize),
}))
.map(|models| models.into_iter().map(ModelSummary::from).collect())
.map_err(map_model_error)
}
@ -731,7 +858,7 @@ impl MeshNodeHandle {
block_on(
self.node
.models()
.download(model_ref, mesh_llm_api_server::DownloadOptions),
.download(model_ref, sdk_node::DownloadOptions),
)
.map(DownloadedModel::from)
.map_err(map_model_error)
@ -744,7 +871,7 @@ impl MeshNodeHandle {
) -> Result<DeleteModelResult, FfiError> {
block_on(self.node.models().delete(
model_ref,
mesh_llm_api_server::DeleteModelOptions {
sdk_node::DeleteModelOptions {
force: options.force,
},
))
@ -753,13 +880,9 @@ impl MeshNodeHandle {
}
pub fn cleanup_models(&self, policy: CleanupPolicy) -> Result<CleanupResult, FfiError> {
block_on(
self.node
.models()
.cleanup(mesh_llm_api_server::CleanupPolicy {
remove_all: policy.remove_all,
}),
)
block_on(self.node.models().cleanup(sdk_node::CleanupPolicy {
remove_all: policy.remove_all,
}))
.map(CleanupResult::from)
.map_err(map_model_error)
}
@ -768,7 +891,7 @@ impl MeshNodeHandle {
block_on(
self.node
.models()
.prune_derived_cache(mesh_llm_api_server::PrunePolicy {
.prune_derived_cache(sdk_node::PrunePolicy {
remove_all: policy.remove_all,
}),
)
@ -783,7 +906,7 @@ impl MeshNodeHandle {
) -> Result<ServedModel, FfiError> {
block_on(self.node.serving().load(
model_ref,
mesh_llm_api_server::LoadModelOptions {
sdk_node::LoadModelOptions {
device_policy: options.device_policy.into(),
},
))
@ -842,8 +965,8 @@ impl MeshNodeHandle {
&self,
options: ConsoleOptionsNative,
) -> Result<Arc<ConsoleHandle>, FfiError> {
let handle = block_on(mesh_llm_console_server::start_file_console(
mesh_llm_console_server::ConsoleServerOptions {
let handle = block_on(mesh_llm_sdk::console::start_file_console(
mesh_llm_sdk::console::ConsoleServerOptions {
asset_dir: options.asset_dir.into(),
port: options.port.unwrap_or(0),
listen_all: options.listen_all,
@ -884,6 +1007,47 @@ fn non_empty_path(value: Option<String>) -> Option<String> {
})
}
fn runtime_install_options(
options: NativeRuntimeInstallOptionsNative,
progress: Option<Box<dyn NativeRuntimeProgressListener>>,
) -> Result<mesh_llm_sdk::native_runtime::NativeRuntimeInstallOptions, FfiError> {
let progress = progress.map(runtime_progress_callback);
Ok(mesh_llm_sdk::native_runtime::NativeRuntimeInstallOptions {
mesh_version: options
.mesh_version
.unwrap_or_else(|| mesh_llm_sdk::native_runtime::CURRENT_MESH_VERSION.to_string()),
skippy_abi_version: options
.skippy_abi_version
.unwrap_or_else(mesh_llm_sdk::native_runtime::current_skippy_abi_version),
selection: mesh_llm_sdk::native_runtime::RuntimeSelection::parse(Some(
options.selection.as_str(),
))
.map_err(map_native_runtime_error)?,
manifest_path: options.manifest_path.map(PathBuf::from),
manifest_url: options.manifest_url,
bundle_dirs: options.bundle_dirs.into_iter().map(PathBuf::from).collect(),
cache_dir: options.cache_dir.map(PathBuf::from),
verification_policy: options.verification_policy.into(),
progress,
allow_download: options.allow_download,
})
}
fn runtime_progress_callback(
listener: Box<dyn NativeRuntimeProgressListener>,
) -> mesh_llm_sdk::native_runtime::NativeRuntimeDownloadProgressCallback {
let listener: Arc<dyn NativeRuntimeProgressListener> = Arc::from(listener);
Arc::new(move |event| listener.on_progress(event.into()))
}
fn native_runtime_cache(
cache_dir: Option<String>,
) -> Result<mesh_llm_sdk::native_runtime::NativeRuntimeCache, FfiError> {
let cache_dir = cache_dir.map(PathBuf::from);
mesh_llm_sdk::native_runtime::native_runtime_cache(cache_dir.as_deref())
.map_err(map_native_runtime_error)
}
fn parse_owner_keypair(owner_keypair_bytes_hex: &str) -> Result<OwnerKeypair, FfiError> {
// An empty keypair is rejected rather than silently generating a fresh identity:
// a caller that forgets to pass their persisted owner keypair would otherwise
@ -940,6 +1104,10 @@ fn map_stream_error(error: MeshApiError) -> FfiError {
}
}
fn map_native_runtime_error(error: impl ToString) -> FfiError {
FfiError::NativeRuntimeFailed(error.to_string())
}
impl From<ChatRequestNative> for ChatRequest {
fn from(value: ChatRequestNative) -> Self {
Self {
@ -979,8 +1147,8 @@ impl From<PublicMeshQuery> for ApiPublicMeshQuery {
}
}
impl From<mesh_llm_api_server::PublicMesh> for PublicMesh {
fn from(value: mesh_llm_api_server::PublicMesh) -> Self {
impl From<sdk_node::PublicMesh> for PublicMesh {
fn from(value: sdk_node::PublicMesh) -> Self {
Self {
invite_token: value.invite_token,
serving: value.serving,
@ -1000,18 +1168,18 @@ impl From<mesh_llm_api_server::PublicMesh> for PublicMesh {
}
}
impl From<mesh_llm_api_server::CapabilityLevel> for CapabilityLevel {
fn from(value: mesh_llm_api_server::CapabilityLevel) -> Self {
impl From<sdk_node::CapabilityLevel> for CapabilityLevel {
fn from(value: sdk_node::CapabilityLevel) -> Self {
match value {
mesh_llm_api_server::CapabilityLevel::None => Self::None,
mesh_llm_api_server::CapabilityLevel::Likely => Self::Likely,
mesh_llm_api_server::CapabilityLevel::Supported => Self::Supported,
sdk_node::CapabilityLevel::None => Self::None,
sdk_node::CapabilityLevel::Likely => Self::Likely,
sdk_node::CapabilityLevel::Supported => Self::Supported,
}
}
}
impl From<mesh_llm_api_server::ModelCapabilities> for ModelCapabilities {
fn from(value: mesh_llm_api_server::ModelCapabilities) -> Self {
impl From<sdk_node::ModelCapabilities> for ModelCapabilities {
fn from(value: sdk_node::ModelCapabilities) -> Self {
Self {
multimodal: value.multimodal,
vision: value.vision.into(),
@ -1023,8 +1191,8 @@ impl From<mesh_llm_api_server::ModelCapabilities> for ModelCapabilities {
}
}
impl From<mesh_llm_api_server::ModelSummary> for ModelSummary {
fn from(value: mesh_llm_api_server::ModelSummary) -> Self {
impl From<sdk_node::ModelSummary> for ModelSummary {
fn from(value: sdk_node::ModelSummary) -> Self {
Self {
id: value.id,
name: value.name,
@ -1056,8 +1224,8 @@ impl From<ApiModelKind> for ModelKind {
}
}
impl From<mesh_llm_api_server::ModelDetails> for ModelDetails {
fn from(value: mesh_llm_api_server::ModelDetails) -> Self {
impl From<sdk_node::ModelDetails> for ModelDetails {
fn from(value: sdk_node::ModelDetails) -> Self {
Self {
id: value.id,
name: value.name,
@ -1076,8 +1244,8 @@ impl From<mesh_llm_api_server::ModelDetails> for ModelDetails {
}
}
impl From<mesh_llm_api_server::InstalledModel> for InstalledModel {
fn from(value: mesh_llm_api_server::InstalledModel) -> Self {
impl From<sdk_node::InstalledModel> for InstalledModel {
fn from(value: sdk_node::InstalledModel) -> Self {
Self {
model_ref: value.model_ref,
path: path_to_string(value.path),
@ -1087,16 +1255,16 @@ impl From<mesh_llm_api_server::InstalledModel> for InstalledModel {
}
}
impl From<mesh_llm_api_server::ModelCacheStatus> for ModelCacheStatus {
fn from(value: mesh_llm_api_server::ModelCacheStatus) -> Self {
impl From<sdk_node::ModelCacheStatus> for ModelCacheStatus {
fn from(value: sdk_node::ModelCacheStatus) -> Self {
Self {
cache_dir: value.cache_dir.map(path_to_string),
}
}
}
impl From<mesh_llm_api_server::DownloadedModel> for DownloadedModel {
fn from(value: mesh_llm_api_server::DownloadedModel) -> Self {
impl From<sdk_node::DownloadedModel> for DownloadedModel {
fn from(value: sdk_node::DownloadedModel) -> Self {
Self {
model_ref: value.model_ref,
paths: value.paths.into_iter().map(path_to_string).collect(),
@ -1106,8 +1274,8 @@ impl From<mesh_llm_api_server::DownloadedModel> for DownloadedModel {
}
}
impl From<mesh_llm_api_server::DeleteModelResult> for DeleteModelResult {
fn from(value: mesh_llm_api_server::DeleteModelResult) -> Self {
impl From<sdk_node::DeleteModelResult> for DeleteModelResult {
fn from(value: sdk_node::DeleteModelResult) -> Self {
Self {
deleted_paths: value
.deleted_paths
@ -1119,8 +1287,8 @@ impl From<mesh_llm_api_server::DeleteModelResult> for DeleteModelResult {
}
}
impl From<mesh_llm_api_server::CleanupResult> for CleanupResult {
fn from(value: mesh_llm_api_server::CleanupResult) -> Self {
impl From<sdk_node::CleanupResult> for CleanupResult {
fn from(value: sdk_node::CleanupResult) -> Self {
Self {
deleted_paths: value
.deleted_paths
@ -1137,8 +1305,8 @@ impl From<mesh_llm_api_server::CleanupResult> for CleanupResult {
}
}
impl From<mesh_llm_api_server::PruneResult> for PruneResult {
fn from(value: mesh_llm_api_server::PruneResult) -> Self {
impl From<sdk_node::PruneResult> for PruneResult {
fn from(value: sdk_node::PruneResult) -> Self {
Self {
deleted_paths: value
.deleted_paths
@ -1173,8 +1341,8 @@ impl From<ApiServingModelState> for ServingModelState {
}
}
impl From<mesh_llm_api_server::ServedModel> for ServedModel {
fn from(value: mesh_llm_api_server::ServedModel) -> Self {
impl From<sdk_node::ServedModel> for ServedModel {
fn from(value: sdk_node::ServedModel) -> Self {
Self {
model_ref: value.model_ref,
model_id: value.model_id,
@ -1188,8 +1356,8 @@ impl From<mesh_llm_api_server::ServedModel> for ServedModel {
}
}
impl From<mesh_llm_api_server::ServingStatus> for ServingStatus {
fn from(value: mesh_llm_api_server::ServingStatus) -> Self {
impl From<sdk_node::ServingStatus> for ServingStatus {
fn from(value: sdk_node::ServingStatus) -> Self {
Self {
enabled: value.enabled,
models: value.models.into_iter().map(ServedModel::from).collect(),
@ -1197,6 +1365,94 @@ impl From<mesh_llm_api_server::ServingStatus> for ServingStatus {
}
}
impl From<NativeRuntimeVerificationPolicyNative>
for mesh_llm_sdk::native_runtime::NativeRuntimeVerificationPolicy
{
fn from(value: NativeRuntimeVerificationPolicyNative) -> Self {
match value {
NativeRuntimeVerificationPolicyNative::RequireChecksum => Self::RequireChecksum,
NativeRuntimeVerificationPolicyNative::RequireChecksumAndSignature => {
Self::RequireChecksumAndSignature
}
}
}
}
impl From<NativeRuntimePruneModeNative> for mesh_llm_sdk::native_runtime::NativeRuntimePruneMode {
fn from(value: NativeRuntimePruneModeNative) -> Self {
match value {
NativeRuntimePruneModeNative::KeepActiveAndPrevious => Self::KeepActiveAndPrevious,
NativeRuntimePruneModeNative::ActiveOnly => Self::ActiveOnly,
}
}
}
impl From<mesh_llm_sdk::native_runtime::NativeRuntimeDownloadProgress>
for NativeRuntimeDownloadProgressNative
{
fn from(value: mesh_llm_sdk::native_runtime::NativeRuntimeDownloadProgress) -> Self {
Self {
native_runtime_id: value.native_runtime_id,
url: value.url,
downloaded_bytes: value.downloaded_bytes,
total_bytes: value.total_bytes,
finished: value.finished,
}
}
}
impl From<mesh_llm_sdk::native_runtime::InstalledNativeRuntime> for InstalledNativeRuntimeNative {
fn from(value: mesh_llm_sdk::native_runtime::InstalledNativeRuntime) -> Self {
Self {
mesh_version: value.mesh_version,
native_runtime_id: value.native_runtime_id,
flavor: value.flavor,
path: path_to_string(value.path),
skippy_abi_version: Some(value.manifest.runtime.skippy_abi),
}
}
}
impl From<mesh_llm_sdk::native_runtime::NativeRuntimeInstallOutcome>
for NativeRuntimeInstallOutcomeNative
{
fn from(value: mesh_llm_sdk::native_runtime::NativeRuntimeInstallOutcome) -> Self {
Self {
status: match value.status {
mesh_llm_sdk::native_runtime::NativeRuntimeInstallStatus::AlreadyInstalled => {
"already_installed".to_string()
}
mesh_llm_sdk::native_runtime::NativeRuntimeInstallStatus::Installed => {
"installed".to_string()
}
},
runtime: value.runtime.into(),
selected_native_runtime_id: value.resolution.selected.id,
selected_source: native_runtime_source_name(&value.resolution.source),
}
}
}
impl From<mesh_llm_sdk::native_runtime::CachePrunePlan> for NativeRuntimePruneResultNative {
fn from(value: mesh_llm_sdk::native_runtime::CachePrunePlan) -> Self {
Self {
removed_dirs: value.remove_dirs.into_iter().map(path_to_string).collect(),
}
}
}
fn native_runtime_source_name(
source: &mesh_llm_sdk::native_runtime::NativeRuntimeSource,
) -> String {
match source {
mesh_llm_sdk::native_runtime::NativeRuntimeSource::Installed { .. } => "installed",
mesh_llm_sdk::native_runtime::NativeRuntimeSource::Bundle { .. } => "bundle",
mesh_llm_sdk::native_runtime::NativeRuntimeSource::Download { .. } => "download",
mesh_llm_sdk::native_runtime::NativeRuntimeSource::Missing => "missing",
}
.to_string()
}
impl From<UnloadTarget> for ApiUnloadTarget {
fn from(value: UnloadTarget) -> Self {
match value {

View file

@ -24,6 +24,33 @@ namespace mesh_ffi {
sequence<PublicMesh> discover_public_meshes(PublicMeshQuery query);
string generate_owner_keypair_hex();
string current_mesh_version();
string current_skippy_abi_version();
[Throws=FfiError]
NativeRuntimeInstallOutcomeNative install_native_runtime(
NativeRuntimeInstallOptionsNative options,
NativeRuntimeProgressListener? progress
);
[Throws=FfiError]
sequence<InstalledNativeRuntimeNative> installed_native_runtimes(string? cache_dir);
[Throws=FfiError]
boolean remove_native_runtime(
string? cache_dir,
string mesh_version,
string native_runtime_id
);
[Throws=FfiError]
NativeRuntimePruneResultNative prune_native_runtimes(
string? cache_dir,
string? active_mesh_version,
NativeRuntimePruneModeNative mode
);
};
[Error]
@ -41,6 +68,7 @@ enum FfiError {
"ServingFailed",
"ServingUnsupported",
"ConsoleFailed",
"NativeRuntimeFailed",
};
dictionary ConsoleOptionsNative {
@ -159,6 +187,10 @@ callback interface EventListener {
void on_event(ClientEvent event);
};
callback interface NativeRuntimeProgressListener {
void on_progress(NativeRuntimeDownloadProgressNative event);
};
dictionary ModelNative {
string id;
string name;
@ -373,3 +405,54 @@ interface ClientEvent {
Failed(string request_id, string error);
Disconnected(string reason);
};
[Enum]
interface NativeRuntimeVerificationPolicyNative {
RequireChecksum();
RequireChecksumAndSignature();
};
[Enum]
interface NativeRuntimePruneModeNative {
KeepActiveAndPrevious();
ActiveOnly();
};
dictionary NativeRuntimeInstallOptionsNative {
string? mesh_version;
string? skippy_abi_version;
string selection;
string? manifest_path;
string? manifest_url;
sequence<string> bundle_dirs;
string? cache_dir;
NativeRuntimeVerificationPolicyNative verification_policy;
boolean allow_download;
};
dictionary NativeRuntimeDownloadProgressNative {
string native_runtime_id;
string url;
u64 downloaded_bytes;
u64? total_bytes;
boolean finished;
};
dictionary InstalledNativeRuntimeNative {
string mesh_version;
string native_runtime_id;
string flavor;
string path;
string? skippy_abi_version;
};
dictionary NativeRuntimeInstallOutcomeNative {
string status;
InstalledNativeRuntimeNative runtime;
string selected_native_runtime_id;
string selected_source;
};
dictionary NativeRuntimePruneResultNative {
sequence<string> removed_dirs;
};

View file

@ -15,7 +15,7 @@ fn node_stream_exports_compile() {
#[test]
fn node_exports_compile() {
let keypair = mesh_llm_api_server::OwnerKeypair::generate().to_hex();
let keypair = mesh_llm_sdk::OwnerKeypair::generate().to_hex();
let result = create_node(keypair, "valid-token".to_string(), None, None, false);
assert!(result.is_ok());
}

View file

@ -25,7 +25,7 @@ fn ffi_client_runs_against_live_mesh() {
let owner_keypair_hex = env::var("MESH_SDK_OWNER_KEYPAIR_HEX")
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| mesh_llm_api_server::OwnerKeypair::generate().to_hex());
.unwrap_or_else(|| mesh_llm_sdk::OwnerKeypair::generate().to_hex());
let handle =
create_node(owner_keypair_hex, invite_token, None, None, false).expect("create_node");
handle.start().expect("start");

View file

@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
fn valid_owner_keypair_hex() -> String {
mesh_llm_api_server::OwnerKeypair::generate().to_hex()
mesh_llm_sdk::OwnerKeypair::generate().to_hex()
}
fn valid_node() -> Arc<meshllm_ffi::MeshNodeHandle> {
@ -141,7 +141,7 @@ fn create_node_with_invalid_owner_keypair_fails() {
#[test]
fn create_node_uses_supplied_owner_keypair() {
let owner_keypair_hex = {
let keypair = mesh_llm_api_server::OwnerKeypair::generate();
let keypair = mesh_llm_sdk::OwnerKeypair::generate();
keypair.to_hex()
};

View file

@ -3,6 +3,11 @@ name = "mesh-llm-gpu-bench"
version.workspace = true
edition = "2024"
build = "build.rs"
license.workspace = true
description = "Local GPU bandwidth benchmark helpers for mesh-llm"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
[dependencies]
anyhow = "1"

View file

@ -0,0 +1,4 @@
# mesh-llm-gpu-bench
Local GPU bandwidth benchmark helpers used by mesh-llm hardware detection and
runtime planning.

View file

@ -4,6 +4,9 @@ edition.workspace = true
license.workspace = true
version.workspace = true
description = "Reusable guardrail and compaction primitives for mesh-llm OpenAI-compatible paths"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
[lints]
workspace = true

View file

@ -0,0 +1,4 @@
# mesh-llm-guardrails
Reusable guardrail and compaction primitives shared by mesh-llm OpenAI-compatible
request paths.

View file

@ -0,0 +1,16 @@
[package]
name = "mesh-llm-hardware-profile"
edition.workspace = true
license.workspace = true
version.workspace = true
description = "Host hardware profile detection for Mesh LLM native runtime selection"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
[lints]
workspace = true
[dependencies]
mesh-llm-native-runtime = { path = "../mesh-llm-native-runtime", version = "0.68.0" }

View file

@ -0,0 +1,10 @@
# mesh-llm-hardware-profile
`mesh-llm-hardware-profile` detects the local operating system, architecture,
GPU labels, and compatible native runtime flavors used by Mesh LLM native
runtime selection.
The crate is intentionally small and publishable. It avoids depending on the
host application runtime so the SDK, installer, updater, and CLI can share the
same flavor ranking input without pulling in the full Mesh LLM app graph.

View file

@ -0,0 +1,328 @@
use mesh_llm_native_runtime::{
HostCudaProfile, HostGpuProfile, HostRocmProfile, HostRuntimeProfile, HostVulkanProfile,
NativeRuntimeBackendKind,
};
use std::collections::BTreeSet;
use std::process::Command;
pub fn host_runtime_profile() -> HostRuntimeProfile {
let mut gpus = detect_gpus();
apply_gpu_arch_overrides(&mut gpus);
let cuda = detect_cuda_profile(&gpus);
let rocm = detect_rocm_profile(&gpus);
let vulkan = detect_vulkan_profile();
HostRuntimeProfile {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
target_triple: option_env!("TARGET").map(str::to_string),
available_flavors: detected_native_runtime_flavors(
&gpus,
cuda.as_ref(),
rocm.as_ref(),
vulkan.as_ref(),
),
gpus,
cuda,
rocm,
vulkan,
}
}
pub fn detected_native_runtime_flavors(
gpus: &[HostGpuProfile],
cuda: Option<&HostCudaProfile>,
rocm: Option<&HostRocmProfile>,
vulkan: Option<&HostVulkanProfile>,
) -> BTreeSet<NativeRuntimeBackendKind> {
let mut flavors = BTreeSet::from([NativeRuntimeBackendKind::Cpu]);
if cfg!(target_os = "macos") {
flavors.insert(NativeRuntimeBackendKind::Metal);
}
if cuda.is_some() {
flavors.insert(NativeRuntimeBackendKind::Cuda);
}
if rocm.is_some() {
flavors.insert(NativeRuntimeBackendKind::Rocm);
}
if vulkan.is_some() {
flavors.insert(NativeRuntimeBackendKind::Vulkan);
}
for gpu in gpus {
insert_label_flavors(&mut flavors, &gpu.display_name);
if let Some(device) = &gpu.backend_device {
insert_label_flavors(&mut flavors, device);
}
}
flavors
}
fn detect_gpus() -> Vec<HostGpuProfile> {
let labels = gpu_labels();
labels
.into_iter()
.enumerate()
.map(|(index, label)| HostGpuProfile {
display_name: label,
backend_device: None,
stable_id: Some(format!("detected-{index}")),
vram_bytes: None,
unified_memory: cfg!(target_os = "macos"),
cuda_sm: None,
rocm_gfx: None,
})
.collect()
}
fn detect_cuda_profile(gpus: &[HostGpuProfile]) -> Option<HostCudaProfile> {
let mut toolkit_majors = env_u32_set("MESH_LLM_CUDA_TOOLKIT_MAJORS");
if let Some(major) = env_u32("MESH_LLM_CUDA_TOOLKIT_MAJOR") {
toolkit_majors.insert(major);
}
if toolkit_majors.is_empty() {
toolkit_majors.extend(cuda_majors_from_nvidia_smi());
}
let mut gpu_arches = env_string_set("MESH_LLM_CUDA_GPU_ARCHES");
gpu_arches.extend(gpus.iter().filter_map(|gpu| gpu.cuda_sm.clone()));
let has_cuda_label = gpus.iter().any(|gpu| {
let label = gpu.display_name.to_ascii_lowercase();
label.contains("nvidia") || label.contains("cuda")
});
if toolkit_majors.is_empty() && gpu_arches.is_empty() && !has_cuda_label {
return None;
}
Some(HostCudaProfile {
toolkit_majors,
driver_version: std::env::var("MESH_LLM_CUDA_DRIVER_VERSION").ok(),
gpu_arches,
})
}
fn detect_rocm_profile(gpus: &[HostGpuProfile]) -> Option<HostRocmProfile> {
let mut gpu_arches = env_string_set("MESH_LLM_ROCM_GPU_ARCHES");
gpu_arches.extend(gpus.iter().filter_map(|gpu| gpu.rocm_gfx.clone()));
let version = std::env::var("MESH_LLM_ROCM_VERSION").ok();
let has_rocm_label = gpus.iter().any(|gpu| {
let label = gpu.display_name.to_ascii_lowercase();
label.contains("amd") || label.contains("radeon") || label.contains("rocm")
});
if gpu_arches.is_empty() && version.is_none() && !has_rocm_label {
return None;
}
Some(HostRocmProfile {
version,
gpu_arches,
})
}
fn detect_vulkan_profile() -> Option<HostVulkanProfile> {
let api_version = std::env::var("MESH_LLM_VULKAN_API_VERSION").ok();
let enabled = std::env::var("MESH_LLM_VULKAN_AVAILABLE")
.ok()
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"));
if enabled || api_version.is_some() || command_output("vulkaninfo", &["--summary"]).is_some() {
return Some(HostVulkanProfile { api_version });
}
None
}
fn apply_gpu_arch_overrides(gpus: &mut [HostGpuProfile]) {
let cuda_arches = env_string_vec("MESH_LLM_CUDA_GPU_ARCHES");
let rocm_arches = env_string_vec("MESH_LLM_ROCM_GPU_ARCHES");
for (index, gpu) in gpus.iter_mut().enumerate() {
gpu.cuda_sm = cuda_arches.get(index).cloned();
gpu.rocm_gfx = rocm_arches.get(index).cloned();
}
}
fn cuda_majors_from_nvidia_smi() -> BTreeSet<u32> {
let mut majors = BTreeSet::new();
let Some(output) = command_output("nvidia-smi", &[]) else {
return majors;
};
for token in output.split_whitespace() {
if let Some(major) = cuda_major_from_token(token) {
majors.insert(major);
}
}
majors
}
fn cuda_major_from_token(token: &str) -> Option<u32> {
token
.strip_prefix("CUDA")?
.trim_start_matches("Version:")
.trim_matches(|ch: char| !ch.is_ascii_digit())
.split('.')
.next()
.and_then(|value| value.parse::<u32>().ok())
}
fn gpu_labels() -> Vec<String> {
let mut labels = Vec::new();
append_command_lines(&mut labels, "nvidia-smi", &["-L"]);
append_command_lines(&mut labels, "rocminfo", &[]);
append_command_lines(&mut labels, "vulkaninfo", &["--summary"]);
append_platform_gpu_labels(&mut labels);
labels.sort();
labels.dedup();
labels
}
#[cfg(target_os = "linux")]
fn append_platform_gpu_labels(labels: &mut Vec<String>) {
append_command_lines(labels, "lspci", &[]);
append_linux_nvidia_proc_labels(labels);
}
#[cfg(target_os = "linux")]
fn append_linux_nvidia_proc_labels(labels: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir("/proc/driver/nvidia/gpus") else {
return;
};
for entry in entries.flatten() {
let path = entry.path().join("information");
let Ok(info) = std::fs::read_to_string(path) else {
continue;
};
labels.extend(info.lines().map(str::to_string));
}
}
#[cfg(target_os = "windows")]
fn append_platform_gpu_labels(labels: &mut Vec<String>) {
append_command_lines(
labels,
"powershell",
&[
"-NoProfile",
"-Command",
"Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name",
],
);
}
#[cfg(target_os = "macos")]
fn append_platform_gpu_labels(labels: &mut Vec<String>) {
append_command_lines(labels, "system_profiler", &["SPDisplaysDataType"]);
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
fn append_platform_gpu_labels(_labels: &mut Vec<String>) {}
fn append_command_lines(labels: &mut Vec<String>, program: &str, args: &[&str]) {
let Some(output) = command_output(program, args) else {
return;
};
labels.extend(
output
.lines()
.map(str::trim)
.filter(|line| looks_like_gpu_label(line))
.map(str::to_string),
);
}
fn command_output(program: &str, args: &[&str]) -> Option<String> {
let output = Command::new(program).args(args).output().ok()?;
output
.status
.success()
.then(|| String::from_utf8(output.stdout).ok())
.flatten()
}
fn looks_like_gpu_label(line: &str) -> bool {
let label = line.to_ascii_lowercase();
label.contains("gpu")
|| label.contains("nvidia")
|| label.contains("cuda")
|| label.contains("amd")
|| label.contains("radeon")
|| label.contains("rocm")
|| label.contains("vulkan")
|| label.contains("metal")
}
fn insert_label_flavors(flavors: &mut BTreeSet<NativeRuntimeBackendKind>, label: &str) {
let label = label.to_ascii_lowercase();
if label.contains("cuda") || label.contains("nvidia") {
flavors.insert(NativeRuntimeBackendKind::Cuda);
}
if label.contains("rocm")
|| label.contains("hip")
|| label.contains("amd")
|| label.contains("radeon")
{
flavors.insert(NativeRuntimeBackendKind::Rocm);
}
if label.contains("vulkan") {
flavors.insert(NativeRuntimeBackendKind::Vulkan);
}
}
fn env_u32(name: &str) -> Option<u32> {
std::env::var(name).ok()?.parse().ok()
}
fn env_u32_set(name: &str) -> BTreeSet<u32> {
env_string_vec(name)
.into_iter()
.filter_map(|value| value.parse().ok())
.collect()
}
fn env_string_set(name: &str) -> BTreeSet<String> {
env_string_vec(name).into_iter().collect()
}
fn env_string_vec(name: &str) -> Vec<String> {
std::env::var(name)
.ok()
.map(|value| {
value
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn profile(label: &str) -> HostGpuProfile {
HostGpuProfile {
display_name: label.to_string(),
backend_device: None,
stable_id: None,
vram_bytes: None,
unified_memory: false,
cuda_sm: None,
rocm_gfx: None,
}
}
#[test]
fn nvidia_labels_enable_cuda() {
let flavors = detected_native_runtime_flavors(
&[profile("NVIDIA GeForce RTX 4090")],
None,
None,
None,
);
assert!(flavors.contains(&NativeRuntimeBackendKind::Cpu));
assert!(flavors.contains(&NativeRuntimeBackendKind::Cuda));
}
#[test]
fn amd_labels_enable_rocm() {
let flavors =
detected_native_runtime_flavors(&[profile("AMD Radeon PRO W7900")], None, None, None);
assert!(flavors.contains(&NativeRuntimeBackendKind::Rocm));
}
}

View file

@ -2,6 +2,11 @@
name = "mesh-llm-host-runtime"
version.workspace = true
edition = "2024"
license.workspace = true
description = "Host runtime orchestration for mesh-llm nodes"
repository = "https://github.com/Mesh-LLM/mesh-llm"
homepage = "https://github.com/Mesh-LLM/mesh-llm"
readme = "README.md"
[features]
default = ["web-ui"]
@ -12,37 +17,45 @@ web-ui = ["mesh-llm-ui/embed-assets"]
gpu-bench-cuda = ["mesh-llm-system/gpu-bench-cuda"]
gpu-bench-hip = ["mesh-llm-system/gpu-bench-hip"]
gpu-bench-intel = ["mesh-llm-system/gpu-bench-intel"]
dynamic-native-runtime = [
"mesh-llm-system/dynamic-native-runtime",
"skippy-runtime/dynamic-native-runtime",
"skippy-server/dynamic-native-runtime",
]
[lints]
workspace = true
[dependencies]
ansi-to-tui = "8"
bytes = "1"
mesh-mixture-of-agents = { path = "../mesh-mixture-of-agents" }
mesh-llm-config = { path = "../mesh-llm-config" }
mesh-llm-plugin = { path = "../mesh-llm-plugin" }
mesh-llm-plugin-manager = { path = "../mesh-llm-plugin-manager" }
mesh-llm-identity = { path = "../mesh-llm-identity" }
mesh-llm-guardrails = { path = "../mesh-llm-guardrails" }
mesh-llm-protocol = { path = "../mesh-llm-protocol" }
mesh-llm-routing = { path = "../mesh-llm-routing" }
mesh-llm-system = { path = "../mesh-llm-system", features = ["skippy-devices"] }
mesh-llm-types = { path = "../mesh-llm-types" }
mesh-llm-ui = { path = "../mesh-llm-ui", default-features = false }
mesh-llm-node = { path = "../mesh-llm-node" }
mesh-llm-api-server = { path = "../mesh-llm-api-server" }
mesh-client = { package = "mesh-llm-client", path = "../mesh-client", features = ["host-io"] }
model-artifact = { path = "../model-artifact" }
model-package = { path = "../model-package" }
model-ref = { path = "../model-ref" }
model-resolver = { path = "../model-resolver" }
openai-frontend = { path = "../openai-frontend" }
skippy-protocol = { path = "../skippy-protocol" }
skippy-coordinator = { path = "../skippy-coordinator" }
skippy-runtime = { path = "../skippy-runtime" }
skippy-server = { path = "../skippy-server" }
skippy-topology = { path = "../skippy-topology" }
mesh-mixture-of-agents = { path = "../mesh-mixture-of-agents", version = "0.68.0" }
mesh-llm-config = { path = "../mesh-llm-config", version = "0.68.0" }
mesh-llm-events = { path = "../mesh-llm-events", version = "0.68.0" }
mesh-llm-plugin = { path = "../mesh-llm-plugin", version = "0.68.0" }
mesh-llm-plugin-manager = { path = "../mesh-llm-plugin-manager", version = "0.68.0" }
mesh-llm-identity = { path = "../mesh-llm-identity", version = "0.68.0", features = ["host-io"] }
mesh-llm-native-runtime = { path = "../mesh-llm-native-runtime", version = "0.68.0" }
mesh-llm-runtime-install = { path = "../mesh-llm-runtime-install", version = "0.68.0" }
mesh-llm-guardrails = { path = "../mesh-llm-guardrails", version = "0.68.0" }
mesh-llm-protocol = { path = "../mesh-llm-protocol", version = "0.68.0" }
mesh-llm-routing = { path = "../mesh-llm-routing", version = "0.68.0" }
mesh-llm-system = { path = "../mesh-llm-system", version = "0.68.0", features = ["skippy-devices"] }
mesh-llm-types = { path = "../mesh-llm-types", version = "0.68.0" }
mesh-llm-ui = { path = "../mesh-llm-ui", version = "0.68.0", default-features = false }
mesh-llm-node = { path = "../mesh-llm-node", version = "0.68.0" }
mesh-llm-api-server = { path = "../mesh-llm-api-server", version = "0.68.0" }
mesh-client = { package = "mesh-llm-client", path = "../mesh-client", version = "0.68.0", features = ["host-io"] }
model-artifact = { path = "../model-artifact", version = "0.68.0" }
model-hf = { path = "../model-hf", version = "0.68.0" }
model-package = { path = "../model-package", version = "0.68.0" }
model-ref = { path = "../model-ref", version = "0.68.0" }
model-resolver = { path = "../model-resolver", version = "0.68.0" }
openai-frontend = { path = "../openai-frontend", version = "0.68.0" }
skippy-protocol = { path = "../skippy-protocol", version = "0.68.0" }
skippy-coordinator = { path = "../skippy-coordinator", version = "0.68.0" }
skippy-runtime = { path = "../skippy-runtime", version = "0.68.0" }
skippy-server = { path = "../skippy-server", version = "0.68.0" }
skippy-topology = { path = "../skippy-topology", version = "0.68.0" }
iroh = "1.0.0-rc.0"
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
@ -53,7 +66,6 @@ serde_json = "1"
serde_yaml = "0.9"
anyhow = "1"
async-trait = "0.1"
arboard = "3"
rand = "0.10"
base64 = "0.22"
dirs = "6.0.0"
@ -65,8 +77,11 @@ opentelemetry_sdk = { version = "0.31.0", default-features = false, features = [
opentelemetry-otlp = { version = "0.31.0", default-features = false, features = ["metrics", "http-proto", "reqwest-blocking-client"] }
rustls = "0.23.36"
reqwest = { version = "0.12", features = ["stream", "json"] }
flate2 = "1"
futures-util = "0.3"
semver = "1"
sha2 = "0.10"
tar = "0.4"
ed25519-dalek = { version = "=3.0.0-pre.7", features = ["rand_core"] }
crypto_box = "0.9"
chacha20poly1305 = "0.10"
@ -81,7 +96,6 @@ http = "1"
http-body-util = "0.1"
tokio-stream = "0.1"
crossterm = "0.28"
ratatui = "0.30"
url = "2"
urlencoding = "2"
libc = "0.2.183"
@ -95,15 +109,14 @@ toml = "0.9"
zip = { version = "2", default-features = false, features = ["deflate"] }
hf_hub = { package = "hf-hub", version = "1.0.0-rc.1", default-features = false, features = ["blocking"] }
tabwriter = "1"
tempfile = "3"
[dev-dependencies]
serial_test = "3"
mesh-client = { package = "mesh-llm-client", path = "../mesh-client" }
tempfile = "3"
mesh-client = { package = "mesh-llm-client", path = "../mesh-client", version = "0.68.0" }
# Used by the gated-relay regression test to spawn an in-process iroh-relay
# with AccessConfig::Restricted, then build a real iroh::Endpoint from our
# relay_map_from_urls output and verify --relay-auth tokens reach the relay
# on the WebSocket upgrade (and that the wrong token is rejected).
iroh = { version = "1.0.0-rc.0", features = ["test-utils"] }
iroh-relay = { version = "1.0.0-rc.0", features = ["server", "test-utils"] }
futures-util = "0.3"

View file

@ -1,23 +1,9 @@
# mesh-llm-host-runtime
`mesh-llm-host-runtime` owns the host runtime implementation behind the
`mesh-llm` binary.
`mesh-llm-host-runtime` composes the host-side mesh node runtime. It wires
model resolution, local serving, discovery, networking, runtime state, plugins,
the management API, and the shipped CLI entrypoint used by the `mesh-llm`
binary.
This crate includes the remaining host-only subsystems that coordinate a local
node:
- CLI parsing, command handlers, terminal output, and dashboard rendering
- management API routes and OpenAI-compatible ingress wiring
- mesh node orchestration, gossip integration, peer state, and host routing glue
- embedded skippy runtime coordination and stage deployment
- host-side plugin runtime, MCP bridge, and built-in plugins
- runtime-data aggregation and API view shaping
- host-local model catalog, inventory, and download flows that still coordinate
CLI/runtime behavior
The package named `mesh-llm` is now the app assembly crate: it exposes the
binary entrypoint and re-exports this runtime crate for compatibility. Keep new
pure/shared ownership in narrower crates such as `mesh-llm-types`,
`mesh-llm-protocol`, `mesh-llm-routing`, `mesh-llm-identity`,
`mesh-llm-system`, `mesh-llm-ui`, `model-*`, `openai-frontend`, and
`skippy-*`.
This crate is being split so reusable CLI, TUI, SDK, and embeddable runtime
surfaces can be published and consumed independently.

View file

@ -43,6 +43,10 @@ impl Serialize for PublicationState {
}
pub enum RuntimeControlRequest {
Join {
invite_token: String,
resp: tokio::sync::oneshot::Sender<anyhow::Result<()>>,
},
Load {
spec: String,
resp: tokio::sync::oneshot::Sender<anyhow::Result<RuntimeLoadResponse>>,
@ -56,7 +60,9 @@ pub enum RuntimeControlRequest {
mode: GuardrailMode,
resp: tokio::sync::oneshot::Sender<anyhow::Result<OpenAiGuardrailModeUpdateResponse>>,
},
Shutdown,
Shutdown {
source: &'static str,
},
}
#[derive(Clone, Debug, Serialize)]

File diff suppressed because it is too large Load diff

View file

@ -1,38 +0,0 @@
use anyhow::Result;
pub(crate) async fn dispatch_download_command(name: Option<&str>, draft: bool) -> Result<()> {
match name {
Some(query) => {
let model_ref = crate::models::find_remote_catalog_model_exact(query)
.map(|model| crate::models::remote_catalog_model_ref(&model))
.unwrap_or_else(|| query.to_string());
let (_path, details) =
crate::models::download_model_ref_with_progress_details(&model_ref, true).await?;
if draft {
if let Some(draft_name) = details
.as_ref()
.and_then(|details| details.draft.as_deref())
{
let draft_ref = crate::models::find_remote_catalog_model_exact(draft_name)
.map(|model| crate::models::remote_catalog_model_ref(&model))
.unwrap_or_else(|| draft_name.to_string());
crate::models::download_model_ref_with_progress_details(&draft_ref, true)
.await?;
} else {
eprintln!("⚠ No draft model available for {}", query);
}
}
}
None => {
crate::models::remote_catalog::ensure_catalog()?;
eprintln!("Available models:");
eprintln!();
for model in crate::models::remote_catalog::loaded_models()? {
let size = model.size.as_deref().unwrap_or("?");
let description = model.description.as_deref().unwrap_or("");
eprintln!(" {:40} {:>6} {}", model.name, size, description);
}
}
}
Ok(())
}

View file

@ -1,251 +0,0 @@
mod agent_cli;
mod auth;
mod benchmark;
mod discover;
mod doctor;
mod download;
mod gpus;
mod model_package;
mod models;
mod plugin;
mod plugin_cli;
mod runtime;
mod skills;
mod update;
use anyhow::Result;
use crate::cli::commands::agent_cli::{run_claude, run_goose, run_opencode, run_pi};
use crate::cli::commands::benchmark::dispatch_benchmark_command;
use crate::cli::commands::discover::{DiscoverOptions, run_discover, run_stop};
use crate::cli::commands::doctor::dispatch_doctor_command;
use crate::cli::commands::download::dispatch_download_command;
use crate::cli::commands::gpus::dispatch_gpu_command;
use crate::cli::commands::models::dispatch_models_command;
use crate::cli::commands::plugin::run_plugin_command;
use crate::cli::commands::plugin_cli::run_external_plugin_command;
use crate::cli::commands::runtime::{dispatch_runtime_command, run_drop, run_load, run_status};
use crate::cli::commands::skills::run_skills_command;
use crate::cli::commands::update::run_update;
use crate::cli::{AuthCommand, Cli, Command};
use crate::network::nostr;
pub(crate) async fn dispatch(cli: &Cli) -> Result<bool> {
let Some(cmd) = cli.command.as_ref() else {
return Ok(false);
};
dispatch_command(cli, cmd).await?;
Ok(true)
}
async fn dispatch_command(cli: &Cli, cmd: &Command) -> Result<()> {
match cmd {
Command::Auth { command } => dispatch_auth_command(command),
Command::ModelPrepare { .. } => dispatch_model_prepare(cmd).await,
_ => dispatch_general_command(cli, cmd).await,
}
}
async fn dispatch_general_command(cli: &Cli, cmd: &Command) -> Result<()> {
match cmd {
Command::Models { command } => {
dispatch_models_command(command).await?;
Ok(())
}
Command::Download { name, draft } => {
dispatch_download_command(name.as_deref(), *draft).await
}
Command::Update { .. } => run_update(cli).await,
Command::Gpus { json, command } => {
dispatch_gpu_command(*json, command.as_ref())?;
Ok(())
}
Command::Runtime { command } => dispatch_runtime_command(command.as_ref()).await,
Command::Doctor { command } => dispatch_doctor_command(command).await,
Command::Load { name, port } => run_load(name, *port).await,
Command::Unload { name, port } => run_drop(name, *port).await,
Command::Status { port } => run_status(*port).await,
Command::Stop => run_stop(),
Command::Discover {
name,
model,
min_vram,
region,
auto,
relay,
} => {
run_discover(DiscoverOptions {
name: name.clone(),
model: model.clone(),
min_vram_gb: *min_vram,
region: region.clone(),
auto_join: *auto,
relays: relay.clone(),
discovery_mode: cli.mesh_discovery_mode,
supplied_join_tokens: cli.join.clone(),
})
.await
}
Command::RotateKey => nostr::rotate_keys(),
Command::Goose { model, port } => run_goose(model.clone(), *port).await,
Command::Claude { model, port } => run_claude(model.clone(), *port).await,
Command::Pi { model, host, write } => run_pi(model.clone(), host, *write).await,
Command::Opencode { model, host, write } => run_opencode(model.clone(), host, *write).await,
Command::Skills { command } => run_skills_command(command),
Command::Plugin { command } => run_plugin_command(command, cli).await,
Command::Benchmark { command } => dispatch_benchmark_command(command).await,
Command::ModelPrepare { .. } => dispatch_model_prepare(cmd).await,
Command::Auth { command } => dispatch_auth_command(command),
Command::ExternalPlugin(args) => run_external_plugin_command(cli, args).await,
}
}
async fn dispatch_model_prepare(cmd: &Command) -> Result<()> {
let Command::ModelPrepare {
source_repo,
quant,
target,
model_id,
flavor,
timeout,
mesh_llm_ref,
dry_run,
confirm,
follow,
json,
status,
logs,
cancel,
list,
update_script,
} = cmd
else {
unreachable!("dispatch_model_prepare called for non-model-prepare command");
};
model_package::dispatch_model_package(model_package::ModelPrepareArgs {
source_repo: source_repo.as_deref(),
quant: quant.as_deref(),
target: target.as_deref(),
model_id: model_id.as_deref(),
flavor,
timeout,
mesh_llm_ref,
dry_run: *dry_run,
confirm: *confirm,
follow: *follow,
json: *json,
status: status.as_deref(),
logs: logs.as_deref(),
cancel: cancel.as_deref(),
list: *list,
update_script: *update_script,
})
.await
}
fn dispatch_auth_command(command: &AuthCommand) -> Result<()> {
match command {
AuthCommand::Init {
owner_key,
force,
no_passphrase,
keychain,
} => auth::run_init(owner_key.clone(), *force, *no_passphrase, *keychain),
AuthCommand::Status {
owner_key,
node_key,
node_ownership,
trust_store,
} => auth::run_status(
owner_key.clone(),
node_key.clone(),
node_ownership.clone(),
trust_store.clone(),
),
AuthCommand::SignNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
} => auth::run_sign_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
),
AuthCommand::RenewNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
} => auth::run_renew_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
),
AuthCommand::VerifyNode {
file,
node_id,
trust_store,
trust_policy,
} => auth::run_verify_node(
file.clone(),
node_id.clone(),
trust_store.clone(),
*trust_policy,
),
AuthCommand::RotateNode {
owner_key,
node_key,
out,
hostname_hint,
node_label,
expires_in_hours,
revoke_current,
reason,
trust_store,
} => auth::run_rotate_node(
owner_key.clone(),
node_key.clone(),
out.clone(),
node_label.clone(),
hostname_hint.clone(),
*expires_in_hours,
*revoke_current,
reason.clone(),
trust_store.clone(),
),
AuthCommand::RevokeOwner {
owner_id,
reason,
trust_store,
} => auth::run_revoke_owner(owner_id.clone(), reason.clone(), trust_store.clone()),
AuthCommand::RevokeNode {
cert_id,
node_id,
reason,
trust_store,
} => auth::run_revoke_node(
cert_id.clone(),
node_id.clone(),
reason.clone(),
trust_store.clone(),
),
AuthCommand::RotateOwner {
owner_key,
no_passphrase,
force,
} => auth::run_rotate_owner(owner_key.clone(), *no_passphrase, *force),
AuthCommand::Trust { command } => auth::run_trust_command(command),
}
}

View file

@ -1,23 +0,0 @@
use anyhow::Result;
use crate::cli::Cli;
use crate::cli::Command;
use crate::system::autoupdate;
pub async fn run_update(cli: &Cli) -> Result<()> {
let (requested_version, flavor, detect_flavor) = match &cli.command {
Some(Command::Update {
version,
flavor,
detect_flavor,
}) => (version.as_deref(), *flavor, *detect_flavor),
_ => (None, None, false),
};
autoupdate::run_update_command(autoupdate::UpdateCommandOptions {
flavor,
detect_flavor,
requested_version,
current_version: crate::VERSION,
})
.await
}

View file

@ -0,0 +1,65 @@
//! Public support API for the `mesh-llm` binary command handlers.
//!
//! This is intentionally not a command implementation module. It exposes the
//! host-runtime operations the binary crate needs after CLI ownership moved out
//! of host-runtime.
pub mod discovery {
pub mod nostr {
pub use crate::network::nostr::{
DiscoveredMesh, MeshFilter, MeshListing, discover, rotate_keys, score_mesh,
};
}
pub use crate::discovery::{DiscoveryScope, MeshDiscoveryMode};
pub use crate::mesh::load_last_mesh_id;
pub use crate::network::discovery::{LAN_SERVICE_TYPE, LanDiscoveredMesh, discover_lan};
pub use crate::runtime::instance::{
RuntimeProcessTarget, collect_runtime_stop_targets, runtime_root,
};
pub use crate::runtime::nostr_relays;
}
pub mod models {
pub mod election {
pub use crate::inference::election::total_model_bytes;
}
pub mod skippy {
pub use crate::inference::skippy::{
CertificationGateStatus, SkippyCertificationRequest, certify_layer_package,
identity_from_layer_package, is_layer_package_ref, materialized_stage_cache_dir,
materialized_stages_for_sources, prune_unpinned_materialized_stages,
remove_materialized_stages_for_sources, resolve_hf_package_to_local,
};
}
pub use crate::models::remote_catalog;
pub use crate::models::{
DeleteResult, ModelCapabilities, ModelCleanupPlan, ModelCleanupResult, ModelDetails,
ResolvedModel, SearchArtifactFilter, SearchHit, SearchProgress, SearchSort,
ShowVariantsProgress, delete, download_model_ref_with_progress_details,
execute_model_cleanup, find_model_path, find_remote_catalog_model_exact,
huggingface_hub_cache_dir, huggingface_identity_for_path, installed_model_capabilities,
installed_model_display_name, installed_model_huggingface_ref,
layered_package_layer_count_for_path, layered_package_total_bytes_for_path,
load_model_usage_record_for_path, model_usage_cache_dir, plan_model_cleanup,
remote_catalog_model_draft_ref, remote_catalog_model_ref, run_update,
scan_installed_models, search_catalog_json_payload, search_catalog_models,
search_huggingface, search_huggingface_json_payload, show_exact_model,
show_model_variants_with_progress,
};
pub use crate::models::{capabilities, catalog};
}
pub mod plugin {
pub use crate::plugin::{
ExternalPluginSpec, GpuAssignment, GpuConfig, MeshConfig, PluginHostMode, PluginManager,
ResolvedPlugins, ToolCallResult, bundled_cli_plugin_spec, load_config,
};
pub use crate::runtime::load_resolved_plugins;
}
pub mod runtime_instances {
pub use crate::runtime::instance::{LocalInstanceSnapshot, runtime_root, scan_local_instances};
}

View file

@ -0,0 +1,410 @@
use std::{error::Error, fmt};
use mesh_llm_identity::{
NodeOwnershipClaim, OwnershipStatus, OwnershipSummary, SignedNodeOwnership, TrustPolicy,
TrustStore, verify_node_ownership,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ControlPlaneAuthError {
MissingLocalOwnerIdentity {
local_status: OwnershipStatus,
},
MissingRemoteOwnerAttestation,
RemoteOwnerMismatch {
local_owner_id: String,
remote_owner_id: String,
},
RemoteOwnershipInvalid {
status: OwnershipStatus,
owner_id: Option<String>,
cert_id: Option<String>,
},
TargetNodeMismatch {
expected_node_id: String,
actual_node_id: String,
},
UnsupportedTrustPolicy {
policy: TrustPolicy,
},
}
impl fmt::Display for ControlPlaneAuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingLocalOwnerIdentity { local_status } => {
write!(f, "missing local owner identity ({local_status:?})")
}
Self::MissingRemoteOwnerAttestation => write!(f, "missing remote owner attestation"),
Self::RemoteOwnerMismatch {
local_owner_id,
remote_owner_id,
} => write!(
f,
"remote owner mismatch (local {local_owner_id}, remote {remote_owner_id})"
),
Self::RemoteOwnershipInvalid {
status,
owner_id,
cert_id,
} => write!(
f,
"remote ownership invalid ({status:?}, owner_id={}, cert_id={})",
owner_id.as_deref().unwrap_or("unknown"),
cert_id.as_deref().unwrap_or("unknown")
),
Self::TargetNodeMismatch {
expected_node_id,
actual_node_id,
} => write!(
f,
"target node mismatch (expected {expected_node_id}, got {actual_node_id})"
),
Self::UnsupportedTrustPolicy { policy } => {
write!(f, "unsupported control-plane trust policy {policy:?}")
}
}
}
}
impl Error for ControlPlaneAuthError {}
pub fn verify_control_plane_target_node(
target_node_id: &[u8],
actual_local_endpoint_id: &[u8; 32],
) -> Result<(), ControlPlaneAuthError> {
if target_node_id == actual_local_endpoint_id {
return Ok(());
}
Err(ControlPlaneAuthError::TargetNodeMismatch {
expected_node_id: hex::encode(actual_local_endpoint_id),
actual_node_id: hex::encode(target_node_id),
})
}
pub fn verify_control_plane_peer_ownership(
local_owner: &OwnershipSummary,
remote_ownership: Option<&crate::proto::node::SignedNodeOwnership>,
actual_remote_endpoint_id: &[u8; 32],
trust_store: &TrustStore,
trust_policy: TrustPolicy,
now_unix_ms: u64,
) -> Result<OwnershipSummary, ControlPlaneAuthError> {
let Some(local_owner_id) = local_owner
.owner_id
.as_ref()
.filter(|_| local_owner.verified)
else {
return Err(ControlPlaneAuthError::MissingLocalOwnerIdentity {
local_status: local_owner.status.clone(),
});
};
match trust_policy {
TrustPolicy::Off | TrustPolicy::PreferOwned | TrustPolicy::RequireOwned => {}
TrustPolicy::Allowlist => {
return Err(ControlPlaneAuthError::UnsupportedTrustPolicy {
policy: trust_policy,
});
}
}
let remote_ownership = remote_ownership
.map(proto_signed_node_ownership_to_local)
.ok_or(ControlPlaneAuthError::MissingRemoteOwnerAttestation)?;
let remote_summary = verify_node_ownership(
Some(&remote_ownership),
actual_remote_endpoint_id,
trust_store,
TrustPolicy::Off,
now_unix_ms,
);
match remote_summary.status {
OwnershipStatus::Verified => {
let Some(remote_owner_id) = remote_summary.owner_id.as_ref() else {
return Err(ControlPlaneAuthError::RemoteOwnershipInvalid {
status: remote_summary.status.clone(),
owner_id: None,
cert_id: remote_summary.cert_id.clone(),
});
};
if remote_owner_id != local_owner_id {
return Err(ControlPlaneAuthError::RemoteOwnerMismatch {
local_owner_id: local_owner_id.clone(),
remote_owner_id: remote_owner_id.clone(),
});
}
Ok(remote_summary)
}
OwnershipStatus::MismatchedNodeId => Err(ControlPlaneAuthError::TargetNodeMismatch {
expected_node_id: hex::encode(actual_remote_endpoint_id),
actual_node_id: remote_ownership.claim.node_endpoint_id,
}),
_ => Err(ControlPlaneAuthError::RemoteOwnershipInvalid {
status: remote_summary.status.clone(),
owner_id: remote_summary.owner_id.clone(),
cert_id: remote_summary.cert_id.clone(),
}),
}
}
fn proto_signed_node_ownership_to_local(
attestation: &crate::proto::node::SignedNodeOwnership,
) -> SignedNodeOwnership {
SignedNodeOwnership {
claim: NodeOwnershipClaim {
version: attestation.version,
cert_id: attestation.cert_id.clone(),
owner_id: attestation.owner_id.clone(),
owner_sign_public_key: hex::encode(&attestation.owner_sign_public_key),
node_endpoint_id: hex::encode(&attestation.node_endpoint_id),
issued_at_unix_ms: attestation.issued_at_unix_ms,
expires_at_unix_ms: attestation.expires_at_unix_ms,
node_label: attestation.node_label.clone(),
hostname_hint: attestation.hostname_hint.clone(),
},
signature: hex::encode(&attestation.signature),
}
}
#[cfg(test)]
mod tests {
use super::*;
use mesh_llm_identity::{OwnerKeypair, sign_node_ownership};
fn current_time_unix_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn proto_signed_node_ownership(
ownership: &SignedNodeOwnership,
) -> crate::proto::node::SignedNodeOwnership {
crate::proto::node::SignedNodeOwnership {
version: ownership.claim.version,
cert_id: ownership.claim.cert_id.clone(),
owner_id: ownership.claim.owner_id.clone(),
owner_sign_public_key: hex::decode(&ownership.claim.owner_sign_public_key)
.expect("test owner_sign_public_key must decode"),
node_endpoint_id: hex::decode(&ownership.claim.node_endpoint_id)
.expect("test node_endpoint_id must decode"),
issued_at_unix_ms: ownership.claim.issued_at_unix_ms,
expires_at_unix_ms: ownership.claim.expires_at_unix_ms,
node_label: ownership.claim.node_label.clone(),
hostname_hint: ownership.claim.hostname_hint.clone(),
signature: hex::decode(&ownership.signature).expect("test signature must decode"),
}
}
fn verified_local_owner_summary(owner: &OwnerKeypair) -> OwnershipSummary {
OwnershipSummary {
owner_id: Some(owner.owner_id()),
status: OwnershipStatus::Verified,
verified: true,
..OwnershipSummary::default()
}
}
#[test]
fn control_plane_auth_same_owner_without_gossip() {
let owner = OwnerKeypair::generate();
let local_owner = verified_local_owner_summary(&owner);
let remote_node_endpoint_id = [0x52; 32];
let remote_ownership = sign_node_ownership(
&owner,
&remote_node_endpoint_id,
current_time_unix_ms() + 60_000,
Some("remote-worker".into()),
None,
)
.unwrap();
let summary = verify_control_plane_peer_ownership(
&local_owner,
Some(&proto_signed_node_ownership(&remote_ownership)),
&remote_node_endpoint_id,
&TrustStore::default(),
TrustPolicy::Off,
current_time_unix_ms(),
)
.expect("same-owner direct control attestation must succeed without gossip state");
assert!(summary.verified);
assert_eq!(summary.owner_id.as_deref(), Some(owner.owner_id().as_str()));
assert_eq!(summary.node_label.as_deref(), Some("remote-worker"));
}
#[test]
fn control_plane_auth_rejects_wrong_owner() {
let local_owner = OwnerKeypair::generate();
let remote_owner = OwnerKeypair::generate();
let remote_node_endpoint_id = [0x62; 32];
let remote_ownership = sign_node_ownership(
&remote_owner,
&remote_node_endpoint_id,
current_time_unix_ms() + 60_000,
None,
None,
)
.unwrap();
let err = verify_control_plane_peer_ownership(
&verified_local_owner_summary(&local_owner),
Some(&proto_signed_node_ownership(&remote_ownership)),
&remote_node_endpoint_id,
&TrustStore::default(),
TrustPolicy::Off,
current_time_unix_ms(),
)
.expect_err("different-owner control attestation must fail closed");
assert!(matches!(
err,
ControlPlaneAuthError::RemoteOwnerMismatch { .. }
));
}
#[test]
fn control_plane_auth_rejects_wrong_node_id() {
let owner = OwnerKeypair::generate();
let local_owner = verified_local_owner_summary(&owner);
let claimed_node_endpoint_id = [0x71; 32];
let actual_remote_endpoint_id = [0x72; 32];
let remote_ownership = sign_node_ownership(
&owner,
&claimed_node_endpoint_id,
current_time_unix_ms() + 60_000,
None,
None,
)
.unwrap();
let err = verify_control_plane_peer_ownership(
&local_owner,
Some(&proto_signed_node_ownership(&remote_ownership)),
&actual_remote_endpoint_id,
&TrustStore::default(),
TrustPolicy::Off,
current_time_unix_ms(),
)
.expect_err("wrong peer node id must fail closed");
assert!(matches!(
err,
ControlPlaneAuthError::TargetNodeMismatch { .. }
));
}
#[test]
fn control_plane_auth_rejects_bad_signature() {
let owner = OwnerKeypair::generate();
let local_owner = verified_local_owner_summary(&owner);
let remote_node_endpoint_id = [0x81; 32];
let mut remote_ownership = proto_signed_node_ownership(
&sign_node_ownership(
&owner,
&remote_node_endpoint_id,
current_time_unix_ms() + 60_000,
None,
None,
)
.unwrap(),
);
remote_ownership.signature[0] ^= 0xFF;
let err = verify_control_plane_peer_ownership(
&local_owner,
Some(&remote_ownership),
&remote_node_endpoint_id,
&TrustStore::default(),
TrustPolicy::Off,
current_time_unix_ms(),
)
.expect_err("bad-signature control attestation must fail closed");
assert!(matches!(
err,
ControlPlaneAuthError::RemoteOwnershipInvalid {
status: OwnershipStatus::InvalidSignature,
..
}
));
}
#[test]
fn control_plane_auth_rejects_missing_local_owner_identity() {
let owner = OwnerKeypair::generate();
let remote_node_endpoint_id = [0x91; 32];
let remote_ownership = sign_node_ownership(
&owner,
&remote_node_endpoint_id,
current_time_unix_ms() + 60_000,
None,
None,
)
.unwrap();
let err = verify_control_plane_peer_ownership(
&OwnershipSummary::default(),
Some(&proto_signed_node_ownership(&remote_ownership)),
&remote_node_endpoint_id,
&TrustStore::default(),
TrustPolicy::Off,
current_time_unix_ms(),
)
.expect_err("missing local owner identity must fail closed");
assert!(matches!(
err,
ControlPlaneAuthError::MissingLocalOwnerIdentity {
local_status: OwnershipStatus::Unsigned,
}
));
}
#[test]
fn control_plane_auth_rejects_unsupported_trust_policy() {
let owner = OwnerKeypair::generate();
let local_owner = verified_local_owner_summary(&owner);
let remote_node_endpoint_id = [0xA1; 32];
let remote_ownership = sign_node_ownership(
&owner,
&remote_node_endpoint_id,
current_time_unix_ms() + 60_000,
None,
None,
)
.unwrap();
let err = verify_control_plane_peer_ownership(
&local_owner,
Some(&proto_signed_node_ownership(&remote_ownership)),
&remote_node_endpoint_id,
&TrustStore::default(),
TrustPolicy::Allowlist,
current_time_unix_ms(),
)
.expect_err("allowlist trust policy must fail closed for owner-control auth");
assert!(matches!(
err,
ControlPlaneAuthError::UnsupportedTrustPolicy {
policy: TrustPolicy::Allowlist,
}
));
}
#[test]
fn control_plane_auth_rejects_target_node_mismatch() {
let err = verify_control_plane_target_node(&[0xCD; 32], &[0xAB; 32])
.expect_err("wrong target node id must fail closed");
assert!(matches!(
err,
ControlPlaneAuthError::TargetNodeMismatch { .. }
));
}
}

View file

@ -1,27 +1,8 @@
mod keychain;
mod keystore;
mod ownership;
mod control_plane;
pub(crate) mod release_attestation;
pub use self::keychain::{
DEFAULT_OWNER_ACCOUNT, KEYCHAIN_SERVICE, OwnerKeychainLoadError,
delete_secret as keychain_delete, get_secret as keychain_get,
is_available as keychain_available, load_owner_keypair_from_keychain,
owner_account_for_path as owner_keychain_account_for_path, save_keystore_with_keychain,
set_secret as keychain_set,
};
pub(crate) use self::keystore::write_keystore_bytes_atomically;
pub use self::keystore::{
KeystoreInfo, default_keystore_path, keystore_exists, keystore_metadata, load_keystore,
save_keystore,
};
pub use self::ownership::{
ControlPlaneAuthError, DEFAULT_NODE_CERT_LIFETIME_SECS, DEFAULT_NODE_CERT_RENEW_WINDOW_SECS,
NodeOwnershipClaim, OwnershipStatus, OwnershipSummary, SignedNodeOwnership, TrustPolicy,
TrustStore, certificate_needs_renewal, default_node_ownership_path, default_trust_store_path,
load_node_ownership, load_trust_store, save_node_ownership, save_trust_store,
sign_node_ownership, verify_control_plane_peer_ownership, verify_control_plane_target_node,
verify_node_ownership,
pub use self::control_plane::{
ControlPlaneAuthError, verify_control_plane_peer_ownership, verify_control_plane_target_node,
};
pub use self::release_attestation::{
EmbeddedReleaseAttestation, LoadedEmbeddedReleaseAttestation, ReleaseAttestationClaims,
@ -31,7 +12,17 @@ pub use self::release_attestation::{
load_release_signer_trust_store, parse_release_signer_public_key, release_signer_key_id,
save_release_signer_trust_store, verify_release_attestation,
};
pub(crate) use mesh_llm_identity::keystore::write_keystore_bytes_atomically;
pub use mesh_llm_identity::{
CryptoError, OpenedMessage, OwnerKeypair, SignedEncryptedEnvelope, open_message,
owner_id_from_verifying_key, seal_message,
CryptoError, DEFAULT_NODE_CERT_LIFETIME_SECS, DEFAULT_NODE_CERT_RENEW_WINDOW_SECS,
DEFAULT_OWNER_ACCOUNT, KEYCHAIN_SERVICE, KeystoreInfo, NodeOwnershipClaim, OpenedMessage,
OwnerKeychainLoadError, OwnerKeypair, OwnershipStatus, OwnershipSummary,
SignedEncryptedEnvelope, SignedNodeOwnership, TrustPolicy, TrustStore,
certificate_needs_renewal, default_keystore_path, default_node_ownership_path,
default_trust_store_path, keychain_available, keychain_delete, keychain_get, keychain_set,
keystore_exists, keystore_metadata, load_keystore, load_node_ownership,
load_owner_keypair_from_keychain, load_trust_store, open_message, owner_id_from_verifying_key,
owner_keychain_account_for_path, save_keystore, save_keystore_with_keychain,
save_node_ownership, save_trust_store, seal_message, sign_node_ownership,
verify_node_ownership,
};

View file

@ -9,8 +9,7 @@ use mesh_llm_system::embedded_release_footer::{
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use super::CryptoError;
use super::keystore::write_keystore_bytes_atomically;
use super::{CryptoError, write_keystore_bytes_atomically};
pub const RELEASE_BUILD_ATTESTATION_VERSION: u32 = 1;
pub const RELEASE_SIGNER_TRUST_STORE_VERSION: u32 = 1;

View file

@ -0,0 +1,47 @@
use serde::Serialize;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MeshDiscoveryMode {
#[default]
Nostr,
Mdns,
}
impl MeshDiscoveryMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::Nostr => "nostr",
Self::Mdns => "mdns",
}
}
pub const fn source(self) -> &'static str {
match self {
Self::Nostr => "nostr-relay",
Self::Mdns => "mdns-sd",
}
}
pub const fn scope(self) -> DiscoveryScope {
match self {
Self::Nostr => DiscoveryScope::Public,
Self::Mdns => DiscoveryScope::Lan,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiscoveryScope {
Public,
Lan,
}
impl DiscoveryScope {
pub const fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::Lan => "lan",
}
}
}

View file

@ -8,146 +8,21 @@ fn passive_path_tui_still_starts_immediately() {
runtime::assert_passive_path_immediate_spawn_behavior();
}
#[tokio::test]
async fn non_serving_subcommands_retain_plain_output() {
runtime::assert_non_serving_dispatch_short_circuit_behavior().await;
}
#[test]
fn startup_lifecycle_transitions_pending_partial_ready_failed() {
cli::output::assert_startup_lifecycle_transitions_pending_partial_ready_failed();
}
#[test]
fn startup_lifecycle_keeps_runtime_ready_as_final_edge() {
cli::output::assert_startup_lifecycle_keeps_runtime_ready_as_final_edge();
}
#[test]
fn startup_failures_surface_in_tui_events_and_status() {
cli::output::assert_startup_failures_surface_in_tui_events_and_status();
}
#[test]
fn startup_failure_summary_sanitizes_multiline_detail() {
cli::output::assert_startup_failure_summary_sanitizes_multiline_detail();
}
#[test]
fn rpc_and_llama_startup_failures_mark_components_failed() {
cli::output::assert_rpc_and_llama_startup_failures_mark_components_failed();
}
#[test]
fn discovery_and_join_failures_mark_startup_mesh_component_failed() {
cli::output::assert_discovery_and_join_failures_mark_startup_mesh_component_failed();
}
#[test]
fn post_ready_peer_churn_does_not_reopen_startup_failure() {
cli::output::assert_post_ready_peer_churn_does_not_reopen_startup_failure();
}
#[test]
fn interactive_handler_spawns_once_across_startup_callbacks() {
runtime::assert_interactive_handler_spawns_once_across_startup_callbacks();
}
#[test]
fn startup_history_is_visible_after_late_tui_attach() {
cli::output::assert_startup_history_is_visible_after_late_tui_attach();
}
#[test]
fn startup_history_keeps_order_when_tui_attaches_late() {
cli::output::assert_startup_history_keeps_order_when_tui_attaches_late();
}
#[test]
fn endpoint_rows_remain_starting_until_ready_events() {
cli::output::assert_endpoint_rows_remain_starting_until_ready_events();
}
#[test]
fn startup_launch_plan_renders_not_ready_rows_before_actions() {
cli::output::assert_startup_launch_plan_renders_not_ready_rows_before_actions();
}
#[test]
fn tui_model_progress_renders_dashboard_without_loading_screen() {
cli::output::assert_tui_model_progress_renders_dashboard_without_loading_screen();
}
#[test]
fn tui_startup_progress_continues_in_dashboard_after_model_download_ready() {
cli::output::assert_tui_startup_progress_continues_in_dashboard_after_model_download_ready();
}
#[test]
fn startup_progress_after_launch_plan_shows_dashboard_not_loader() {
cli::output::assert_startup_progress_after_launch_plan_shows_dashboard_not_loader();
}
#[test]
fn planned_rows_transition_from_not_ready_to_ready_events() {
cli::output::assert_planned_rows_transition_from_not_ready_to_ready_events();
}
#[test]
fn launch_plan_rows_survive_empty_startup_snapshot() {
cli::output::assert_launch_plan_rows_survive_empty_startup_snapshot();
}
#[test]
fn launch_plan_preserves_distinct_port_zero_endpoint_rows() {
cli::output::assert_launch_plan_preserves_distinct_port_zero_endpoint_rows();
}
#[test]
fn snapshot_upsert_preserves_distinct_port_zero_endpoint_rows() {
cli::output::assert_snapshot_upsert_preserves_distinct_port_zero_endpoint_rows();
}
#[test]
fn planned_port_zero_process_rows_bind_to_concrete_startup_events() {
cli::output::assert_planned_port_zero_process_rows_bind_to_concrete_startup_events();
}
#[test]
fn startup_launch_plan_describes_planned_runtime_before_process_start() {
runtime::assert_startup_launch_plan_describes_planned_runtime_before_process_start();
}
#[test]
fn fallback_mode_surfaces_startup_failures_without_tui() {
cli::output::assert_fallback_mode_surfaces_startup_failures_without_tui();
}
#[test]
fn quitting_during_startup_cancels_without_late_ready_render() {
runtime::assert_quitting_during_startup_cancels_without_late_ready_render();
}
#[test]
fn interactive_preterminal_render_uses_plain_event_output() {
cli::output::assert_interactive_preterminal_render_uses_plain_event_output();
}
#[test]
fn interactive_post_terminal_exit_resumes_plain_event_output() {
cli::output::assert_interactive_post_terminal_exit_resumes_plain_event_output();
}
#[test]
fn tui_model_card_separates_name_from_metadata_columns() {
cli::output::assert_tui_model_card_separates_name_from_metadata_columns();
}
#[test]
fn mesh_requirements_docs_examples_parse() {
cli::assert_mesh_requirements_docs_examples_parse();
}
#[test]
fn mesh_requirements_policy_canonical_hash_is_stable() {
mesh::requirements::tests::assert_mesh_requirements_policy_canonical_hash_is_stable();

View file

@ -13,17 +13,17 @@ use super::materialization::{
const RUNTIME_SMOKE_TIMEOUT: Duration = Duration::from_secs(120);
#[derive(Clone, Debug)]
pub(crate) struct SkippyCertificationRequest {
pub(crate) model_ref: String,
pub(crate) package_only: bool,
pub(crate) api_base: Option<String>,
pub(crate) prompt: String,
pub(crate) max_tokens: u32,
pub struct SkippyCertificationRequest {
pub model_ref: String,
pub package_only: bool,
pub api_base: Option<String>,
pub prompt: String,
pub max_tokens: u32,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CertificationGateStatus {
pub enum CertificationGateStatus {
Passed,
Failed,
Incomplete,
@ -31,43 +31,43 @@ pub(crate) enum CertificationGateStatus {
}
#[derive(Debug, Serialize)]
pub(crate) struct SkippyCertificationReport {
pub(crate) schema_version: u32,
pub(crate) status: CertificationGateStatus,
pub(crate) input: String,
pub(crate) resolved_package_ref: String,
pub(crate) local_package_dir: String,
pub(crate) model_id: String,
pub(crate) manifest_sha256: String,
pub(crate) source_model_path: String,
pub(crate) source_model_sha256: String,
pub(crate) source_model_bytes: Option<u64>,
pub(crate) layer_count: u32,
pub(crate) package_gate: CertificationGate,
pub(crate) materialized_stages: Vec<CertifiedStage>,
pub(crate) runtime_gates: Vec<CertificationGate>,
pub struct SkippyCertificationReport {
pub schema_version: u32,
pub status: CertificationGateStatus,
pub input: String,
pub resolved_package_ref: String,
pub local_package_dir: String,
pub model_id: String,
pub manifest_sha256: String,
pub source_model_path: String,
pub source_model_sha256: String,
pub source_model_bytes: Option<u64>,
pub layer_count: u32,
pub package_gate: CertificationGate,
pub materialized_stages: Vec<CertifiedStage>,
pub runtime_gates: Vec<CertificationGate>,
}
#[derive(Debug, Serialize)]
pub(crate) struct CertificationGate {
pub(crate) name: String,
pub(crate) status: CertificationGateStatus,
pub struct CertificationGate {
pub name: String,
pub status: CertificationGateStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) details: Option<String>,
pub details: Option<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct CertifiedStage {
pub(crate) stage_id: String,
pub(crate) layer_start: u32,
pub(crate) layer_end: u32,
pub(crate) include_embeddings: bool,
pub(crate) include_output: bool,
pub(crate) selected_part_count: usize,
pub(crate) verified_artifacts: usize,
pub(crate) cached_artifacts: usize,
pub(crate) materialized_path: String,
pub(crate) materialized_bytes: u64,
pub struct CertifiedStage {
pub stage_id: String,
pub layer_start: u32,
pub layer_end: u32,
pub include_embeddings: bool,
pub include_output: bool,
pub selected_part_count: usize,
pub verified_artifacts: usize,
pub cached_artifacts: usize,
pub materialized_path: String,
pub materialized_bytes: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -78,7 +78,7 @@ struct CertificationStageRange {
include_output: bool,
}
pub(crate) async fn certify_layer_package(
pub async fn certify_layer_package(
request: SkippyCertificationRequest,
) -> Result<SkippyCertificationReport> {
let resolved_package_ref = resolve_certification_package_ref(&request.model_ref)?;
@ -119,7 +119,7 @@ pub(crate) async fn certify_layer_package(
})
}
pub(crate) fn resolve_certification_package_ref(input: &str) -> Result<String> {
pub fn resolve_certification_package_ref(input: &str) -> Result<String> {
if let Ok(package_ref) = StagePackageRef::parse(input) {
if let Some(package_ref) = package_ref.as_package_ref() {
return Ok(package_ref);

View file

@ -15,15 +15,15 @@ use skippy_runtime::package::{
self, LayerPackageInfo, PackageIntegrityOptions, PackageStageRequest,
};
use crate::cli::output::{ModelProgressStatus, OutputEvent, emit_event, interactive_tui_active};
use crate::cli::terminal_progress::{SpinnerHandle, start_spinner};
use mesh_llm_events::terminal_progress::{SpinnerHandle, start_spinner};
use mesh_llm_events::{ModelProgressStatus, OutputEvent, emit_event, interactive_tui_active};
use super::StageLoadRequest;
mod cache_resolution;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum StagePackageRef {
pub enum StagePackageRef {
LocalPackage(PathBuf),
HuggingFacePackage {
repo: String,
@ -33,7 +33,7 @@ pub(crate) enum StagePackageRef {
}
impl StagePackageRef {
pub(crate) fn parse(value: &str) -> Result<Self> {
pub fn parse(value: &str) -> Result<Self> {
if let Some(rest) = value.strip_prefix("hf://") {
let (repo, revision) = if let Some((repo, revision)) = rest.split_once('@') {
(repo, Some(revision.to_string()))
@ -62,14 +62,14 @@ impl StagePackageRef {
bail!("not a skippy package ref: {value}");
}
pub(crate) fn is_distributable_package(&self) -> bool {
pub fn is_distributable_package(&self) -> bool {
matches!(
self,
Self::LocalPackage(_) | Self::HuggingFacePackage { .. }
)
}
pub(crate) fn as_package_ref(&self) -> Option<String> {
pub fn as_package_ref(&self) -> Option<String> {
match self {
Self::LocalPackage(path) => Some(path.to_string_lossy().to_string()),
Self::HuggingFacePackage { repo, revision } => Some(match revision {
@ -82,47 +82,47 @@ impl StagePackageRef {
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct StagePackageInfo {
pub(crate) package_ref: String,
pub(crate) package_dir: PathBuf,
pub(crate) manifest_sha256: String,
pub(crate) model_id: String,
pub(crate) source_model_path: String,
pub(crate) source_model_sha256: String,
pub(crate) source_model_bytes: Option<u64>,
pub(crate) layer_count: u32,
pub(crate) activation_width: u32,
pub(crate) projector_path: Option<String>,
pub(crate) layers: Vec<StagePackageLayerInfo>,
pub struct StagePackageInfo {
pub package_ref: String,
pub package_dir: PathBuf,
pub manifest_sha256: String,
pub model_id: String,
pub source_model_path: String,
pub source_model_sha256: String,
pub source_model_bytes: Option<u64>,
pub layer_count: u32,
pub activation_width: u32,
pub projector_path: Option<String>,
pub layers: Vec<StagePackageLayerInfo>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct StagePackageLayerInfo {
pub(crate) layer_index: u32,
pub(crate) tensor_count: usize,
pub(crate) tensor_bytes: u64,
pub(crate) artifact_bytes: u64,
pub struct StagePackageLayerInfo {
pub layer_index: u32,
pub tensor_count: usize,
pub tensor_bytes: u64,
pub artifact_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct MaterializedStageArtifact {
pub(crate) path: PathBuf,
pub(crate) manifest_sha256: String,
pub(crate) source_model_path: String,
pub(crate) source_model_sha256: String,
pub(crate) source_model_bytes: Option<u64>,
pub struct MaterializedStageArtifact {
pub path: PathBuf,
pub manifest_sha256: String,
pub source_model_path: String,
pub source_model_sha256: String,
pub source_model_bytes: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedStagePackage {
pub(crate) local_ref: String,
pub(crate) source_model_path: String,
pub(crate) source_model_sha256: String,
pub(crate) source_model_bytes: Option<u64>,
pub struct ResolvedStagePackage {
pub local_ref: String,
pub source_model_path: String,
pub source_model_sha256: String,
pub source_model_bytes: Option<u64>,
}
#[derive(Debug)]
pub(crate) struct MaterializedStagePin {
pub struct MaterializedStagePin {
path: PathBuf,
}
@ -141,14 +141,14 @@ struct PinFile {
stage_id: String,
}
pub(crate) fn configure_materialized_stage_cache() {
pub fn configure_materialized_stage_cache() {
if std::env::var_os("SKIPPY_MATERIALIZED_DIR").is_none() {
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("SKIPPY_MATERIALIZED_DIR", materialized_stage_cache_dir()) };
}
}
pub(crate) fn materialized_stage_cache_dir() -> PathBuf {
pub fn materialized_stage_cache_dir() -> PathBuf {
crate::models::mesh_llm_cache_dir().join("skippy-stages")
}
@ -556,7 +556,7 @@ fn draw_layer_package_file_progress(
}
}
pub(crate) fn is_layer_package_ref(value: &str) -> bool {
pub fn is_layer_package_ref(value: &str) -> bool {
StagePackageRef::parse(value).is_ok_and(|package_ref| package_ref.is_distributable_package())
}
@ -785,7 +785,7 @@ fn download_layer_package_file(
Ok(path)
}
pub(crate) fn resolve_hf_package_to_local(
pub fn resolve_hf_package_to_local(
package_ref: &str,
layer_start: u32,
layer_end: u32,
@ -1068,7 +1068,7 @@ fn safe_manifest_file_path(path: &str) -> Result<PathBuf> {
Ok(path.to_path_buf())
}
pub(crate) fn ensure_package_manifest_sha(package_ref: &str, expected_sha256: &str) -> Result<()> {
pub fn ensure_package_manifest_sha(package_ref: &str, expected_sha256: &str) -> Result<()> {
if expected_sha256.trim().is_empty() {
return Ok(());
}
@ -1086,7 +1086,7 @@ pub(crate) fn ensure_package_manifest_sha(package_ref: &str, expected_sha256: &s
Ok(())
}
pub(crate) fn inspect_stage_package(package_ref: &str) -> Result<StagePackageInfo> {
pub fn inspect_stage_package(package_ref: &str) -> Result<StagePackageInfo> {
// Resolve hf:// to local for inspection, downloading the manifest and any
// shared package metadata that resolver path needs.
let local_ref = resolve_hf_package_to_local(package_ref, 0, 0, false, false)?;
@ -1098,9 +1098,7 @@ pub(crate) fn inspect_stage_package(package_ref: &str) -> Result<StagePackageInf
/// Resolve an `hf://` package ref in a stage load request to a local directory.
/// Returns the resolved local path if the package ref needed resolution, or `None`
/// if it was already local / not a layer package.
pub(crate) fn resolve_stage_load_package(
load: &StageLoadRequest,
) -> Result<Option<ResolvedStagePackage>> {
pub fn resolve_stage_load_package(load: &StageLoadRequest) -> Result<Option<ResolvedStagePackage>> {
if load.load_mode != LoadMode::LayerPackage {
return Ok(None);
}
@ -1127,7 +1125,7 @@ pub(crate) fn resolve_stage_load_package(
}))
}
pub(crate) fn materialize_stage_config(
pub fn materialize_stage_config(
config: &StageConfig,
) -> Result<Option<(MaterializedStageArtifact, MaterializedStagePin)>> {
if config.load_mode != LoadMode::LayerPackage {
@ -1186,7 +1184,7 @@ pub(crate) fn materialize_stage_config(
Ok(Some((artifact, pin)))
}
pub(crate) fn prune_unpinned_materialized_stages() -> Result<usize> {
pub fn prune_unpinned_materialized_stages() -> Result<usize> {
let root = materialized_stage_cache_dir();
if !root.is_dir() {
return Ok(0);
@ -1229,7 +1227,7 @@ pub(crate) fn prune_unpinned_materialized_stages() -> Result<usize> {
Ok(removed)
}
pub(crate) fn remove_materialized_stages_for_sources(sources: &[PathBuf]) -> Result<usize> {
pub fn remove_materialized_stages_for_sources(sources: &[PathBuf]) -> Result<usize> {
let candidates = materialized_stage_removal_candidates(sources)?;
let mut removed = 0usize;
for candidate in candidates {
@ -1241,7 +1239,7 @@ pub(crate) fn remove_materialized_stages_for_sources(sources: &[PathBuf]) -> Res
Ok(removed)
}
pub(crate) fn materialized_stages_for_sources(sources: &[PathBuf]) -> Result<Vec<PathBuf>> {
pub fn materialized_stages_for_sources(sources: &[PathBuf]) -> Result<Vec<PathBuf>> {
Ok(materialized_stage_removal_candidates(sources)?
.into_iter()
.filter(|candidate| candidate.artifact_path.exists())

View file

@ -36,19 +36,19 @@ use skippy_server::{
embedded_openai_backend, telemetry::Telemetry, telemetry::TelemetryLevel,
};
pub(crate) use certification::{
pub use certification::{
CertificationGateStatus, SkippyCertificationRequest, certify_layer_package,
};
pub(crate) use family_policy::{family_policy_for_model_path, family_policy_for_stage_config};
pub(crate) use hooks::MeshAutoHookPolicy;
pub(crate) use kv_cache::KvCachePolicy;
pub(crate) use materialization::{
pub use materialization::{
configure_materialized_stage_cache, is_layer_package_ref, materialize_stage_config,
materialized_stage_cache_dir, materialized_stages_for_sources,
prune_unpinned_materialized_stages, remove_materialized_stages_for_sources,
resolve_hf_package_to_local,
};
pub(crate) use package::{
pub use package::{
SkippyPackageIdentity, identity_from_layer_package, synthetic_direct_gguf_package,
};
#[allow(unused_imports)]

View file

@ -9,23 +9,23 @@ use serde::Serialize;
use sha2::{Digest, Sha256};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SkippyPackageIdentity {
pub(crate) package_ref: String,
pub(crate) manifest_sha256: String,
pub(crate) source_model_path: PathBuf,
pub(crate) source_model_sha256: String,
pub(crate) source_model_bytes: u64,
pub(crate) source_files: Vec<SkippyPackageSourceFile>,
pub(crate) layer_count: u32,
pub(crate) activation_width: u32,
pub(crate) tensor_count: u64,
pub struct SkippyPackageIdentity {
pub package_ref: String,
pub manifest_sha256: String,
pub source_model_path: PathBuf,
pub source_model_sha256: String,
pub source_model_bytes: u64,
pub source_files: Vec<SkippyPackageSourceFile>,
pub layer_count: u32,
pub activation_width: u32,
pub tensor_count: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub(crate) struct SkippyPackageSourceFile {
pub(crate) path: PathBuf,
pub(crate) bytes: u64,
pub(crate) sha256: String,
pub struct SkippyPackageSourceFile {
pub path: PathBuf,
pub bytes: u64,
pub sha256: String,
}
#[derive(Serialize)]
@ -52,7 +52,7 @@ struct SyntheticGgufManifestFile {
sha256: String,
}
pub(crate) fn synthetic_direct_gguf_package(
pub fn synthetic_direct_gguf_package(
model_id: &str,
model_path: &Path,
) -> Result<SkippyPackageIdentity> {
@ -300,7 +300,7 @@ fn hex_lower(bytes: &[u8]) -> String {
/// the manifest and shared metadata that the resolver requires, but not layer
/// files. Layer artifacts are fetched later by the node that materializes or
/// loads its assigned stage.
pub(crate) fn identity_from_layer_package(package_ref: &str) -> Result<SkippyPackageIdentity> {
pub fn identity_from_layer_package(package_ref: &str) -> Result<SkippyPackageIdentity> {
// Resolve hf:// to a local package dir for lightweight package inspection.
let local_ref =
super::materialization::resolve_hf_package_to_local(package_ref, 0, 0, false, false)?;

View file

@ -2,13 +2,14 @@
mod api;
mod capture;
mod cli;
pub mod command_support;
pub mod crypto;
mod inference;
pub mod discovery;
pub mod inference;
mod mesh;
mod models;
pub mod models;
mod network;
mod plugin;
pub mod plugin;
mod plugins;
mod protocol;
mod runtime;
@ -37,23 +38,45 @@ pub use mesh::requirements::{
};
use anyhow::Result;
use std::time::Duration;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub use runtime::{
MeshGuardrailMode, RuntimeOptions, RuntimeSurface, console_session_mode_for_runtime_surface,
};
pub async fn run() -> Result<()> {
initialize_host_runtime()?;
runtime::run().await
}
pub async fn run_main() -> i32 {
match run().await {
Ok(()) => 0,
Err(err) => {
let _ = cli::output::emit_fatal_error(&err);
tokio::time::sleep(Duration::from_millis(50)).await;
1
}
pub async fn run_runtime(
options: RuntimeOptions,
explicit_surface: Option<RuntimeSurface>,
legacy_warning: Option<String>,
) -> Result<()> {
initialize_host_runtime()?;
run_runtime_initialized(options, explicit_surface, legacy_warning).await
}
pub async fn run_runtime_initialized(
options: RuntimeOptions,
explicit_surface: Option<RuntimeSurface>,
legacy_warning: Option<String>,
) -> Result<()> {
runtime::run_cli(options, explicit_surface, legacy_warning).await
}
pub fn initialize_host_runtime() -> Result<()> {
#[cfg(feature = "dynamic-native-runtime")]
if let Some(runtime) = system::native_runtime::try_load_installed_native_runtime()? {
tracing::info!(
native_runtime_id = %runtime.native_runtime_id,
libraries = ?runtime.libraries,
"Loaded MeshLLM native runtime"
);
}
Ok(())
}
#[cfg(test)]

View file

@ -16,6 +16,7 @@ use anyhow::{Context, Result};
use base64::Engine;
use iroh::endpoint::Connection;
use iroh::{Endpoint, EndpointAddr, EndpointId, SecretKey, TransportAddr};
use mesh_llm_events::OutputEvent;
use prost::Message;
use serde::{Deserialize, Serialize};
use serde_json::json;
@ -52,14 +53,14 @@ const SIGNED_BOOTSTRAP_TOKEN_LIFETIME_MS: u64 = 24 * 60 * 60 * 1000;
const RECENT_MESH_REJECTION_LIMIT: usize = 16;
fn emit_mesh_info(message: String) {
let _ = crate::cli::output::emit_event(crate::cli::output::OutputEvent::Info {
let _ = mesh_llm_events::emit_event(OutputEvent::Info {
message,
context: None,
});
}
fn emit_mesh_warning(message: String) {
let _ = crate::cli::output::emit_event(crate::cli::output::OutputEvent::Warning {
let _ = mesh_llm_events::emit_event(OutputEvent::Warning {
message,
context: None,
});
@ -399,16 +400,17 @@ pub struct RelayConfig<'a> {
pub enum RelayPolicy {
#[default]
DefaultPublic,
ExplicitlyDisabled,
Disabled,
}
impl RelayPolicy {
fn uses_relay(self) -> bool {
pub(crate) fn uses_relay(self) -> bool {
matches!(self, Self::DefaultPublic)
}
fn uses_raw_stun(self) -> bool {
matches!(self, Self::DefaultPublic)
matches!(self, Self::DefaultPublic | Self::ExplicitlyDisabled)
}
}
@ -557,7 +559,7 @@ fn filter_endpoint_addr_for_bind_ip(
fn effective_relay_urls(policy: RelayPolicy, relay_urls: &[String]) -> Vec<String> {
match policy {
RelayPolicy::Disabled => Vec::new(),
RelayPolicy::Disabled | RelayPolicy::ExplicitlyDisabled => Vec::new(),
RelayPolicy::DefaultPublic if relay_urls.is_empty() => vec![
"https://usw1-2.relay.michaelneale.mesh-llm.iroh.link./".into(),
"https://aps1-1.relay.michaelneale.mesh-llm.iroh.link./".into(),
@ -588,12 +590,15 @@ mod relay_policy_tests {
}
#[test]
fn disabled_policy_uses_no_relays_or_raw_stun() {
fn disabled_policy_uses_no_relays_but_explicit_disable_keeps_raw_stun() {
let custom = vec!["https://relay.example/".to_string()];
assert!(effective_relay_urls(RelayPolicy::Disabled, &custom).is_empty());
assert!(effective_relay_urls(RelayPolicy::ExplicitlyDisabled, &custom).is_empty());
assert!(!RelayPolicy::Disabled.uses_relay());
assert!(!RelayPolicy::ExplicitlyDisabled.uses_relay());
assert!(!RelayPolicy::Disabled.uses_raw_stun());
assert!(RelayPolicy::ExplicitlyDisabled.uses_raw_stun());
}
}
@ -2032,7 +2037,12 @@ fn relay_mode_for_startup(relay: RelayConfig<'_>) -> iroh::endpoint::RelayMode {
tracing::info!("Relay: {:?}", urls);
iroh::endpoint::RelayMode::Custom(relay_map_from_urls(&urls, relay.auths))
} else {
tracing::info!("Relay: disabled by LAN-only discovery mode");
let reason = match relay.policy {
RelayPolicy::ExplicitlyDisabled => "disabled by embedded config",
RelayPolicy::Disabled => "disabled by LAN-only discovery mode",
RelayPolicy::DefaultPublic => unreachable!("default public uses relays"),
};
tracing::info!("Relay: {reason}");
iroh::endpoint::RelayMode::Disabled
}
}
@ -2449,11 +2459,15 @@ pub(crate) fn is_peer_admitted(peers: &HashMap<EndpointId, PeerInfo>, id: &Endpo
/// - `STREAM_GOSSIP (0x01)`: the admission handshake itself.
/// - `STREAM_ROUTE_REQUEST (0x05)`: passive/client request-only path — caller
/// is NEVER promoted to `state.peers`.
/// - `STREAM_TUNNEL_HTTP (0x04)`: passive SDK inference path for callers that
/// have an invite token but should not need a local `/v1` HTTP listener.
///
/// Every other stream — including tunnel (0x02 / 0x04) — requires the
/// remote to have completed gossip first.
/// Every other stream — including raw tunnel (0x02) — requires the remote to
/// have completed gossip first.
pub(crate) fn stream_allowed_before_admission(stream_type: u8) -> bool {
stream_type == STREAM_GOSSIP || stream_type == STREAM_ROUTE_REQUEST
stream_type == STREAM_GOSSIP
|| stream_type == STREAM_ROUTE_REQUEST
|| stream_type == STREAM_TUNNEL_HTTP
}
pub(crate) fn ingest_tunnel_map(
@ -10242,19 +10256,8 @@ pub fn clear_public_identity() {
/// Load secret key from ~/.mesh-llm/key, or create a new one and save it.
async fn load_or_create_key() -> Result<SecretKey> {
let key_path = default_node_key_path()?;
let dir = key_path
.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid node key path {}", key_path.display()))?;
ensure_private_node_key_dir(dir)?;
if key_path.exists() {
ensure_private_node_key_file(&key_path)?;
let hex = tokio::fs::read_to_string(&key_path).await?;
let bytes = hex::decode(hex.trim())?;
if bytes.len() != 32 {
anyhow::bail!("Invalid key length in {}", key_path.display());
}
let key = SecretKey::from_bytes(&bytes.try_into().unwrap());
let key = load_node_key_from_path(&key_path)?;
tracing::info!("Loaded key from {}", key_path.display());
return Ok(key);
}
@ -10266,75 +10269,17 @@ async fn load_or_create_key() -> Result<SecretKey> {
}
pub fn default_node_key_path() -> Result<std::path::PathBuf> {
let home =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?;
Ok(home.join(".mesh-llm").join("key"))
Ok(mesh_llm_identity::default_node_key_path()?)
}
pub fn load_node_key_from_path(path: &std::path::Path) -> Result<SecretKey> {
let hex = std::fs::read_to_string(path)?;
let bytes = hex::decode(hex.trim())?;
if bytes.len() != 32 {
anyhow::bail!("Invalid key length in {}", path.display());
}
Ok(SecretKey::from_bytes(&bytes.try_into().unwrap()))
Ok(SecretKey::from_bytes(
&mesh_llm_identity::load_node_key_bytes_from_path(path)?,
))
}
pub fn save_node_key_to_path(path: &std::path::Path, key: &SecretKey) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid node key path {}", path.display()))?;
ensure_private_node_key_dir(parent)?;
if path.exists() {
ensure_private_node_key_file(path)?;
}
crate::crypto::write_keystore_bytes_atomically(path, hex::encode(key.to_bytes()).as_bytes())?;
ensure_private_node_key_file(path)?;
Ok(())
}
#[cfg(unix)]
fn ensure_private_node_key_dir(dir: &std::path::Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::create_dir_all(dir)?;
let metadata = std::fs::metadata(dir)?;
let mut perms = metadata.permissions();
if perms.mode() & 0o077 != 0 {
perms.set_mode(0o700);
std::fs::set_permissions(dir, perms)?;
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_private_node_key_dir(dir: &std::path::Path) -> Result<()> {
std::fs::create_dir_all(dir)?;
Ok(())
}
#[cfg(unix)]
fn ensure_private_node_key_file(path: &std::path::Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::symlink_metadata(path)?;
if !metadata.file_type().is_file() {
anyhow::bail!("Node key path is not a regular file");
}
let mut perms = metadata.permissions();
if perms.mode() & 0o077 != 0 {
perms.set_mode(0o600);
std::fs::set_permissions(path, perms)?;
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_private_node_key_file(path: &std::path::Path) -> Result<()> {
let metadata = std::fs::symlink_metadata(path)?;
if !metadata.file_type().is_file() {
anyhow::bail!("Node key path is not a regular file");
}
mesh_llm_identity::save_node_key_bytes_to_path(path, &key.to_bytes())?;
Ok(())
}

View file

@ -3241,13 +3241,14 @@ fn incoming_peer_promoted_after_valid_gossip() {
"peer must NOT be admitted before gossip"
);
for &tunnel_stream in &[STREAM_TUNNEL, STREAM_TUNNEL_HTTP] {
assert!(
!stream_allowed_before_admission(tunnel_stream),
"stream {:#04x} must be gated until after admission — unadmitted peers must not reach tunnel paths",
tunnel_stream
);
}
assert!(
!stream_allowed_before_admission(STREAM_TUNNEL),
"raw tunnel streams must be gated until after admission"
);
assert!(
stream_allowed_before_admission(STREAM_TUNNEL_HTTP),
"HTTP tunnel streams must be allowed for passive SDK clients"
);
assert!(
stream_allowed_before_admission(STREAM_GOSSIP),
@ -3296,7 +3297,6 @@ fn incoming_peer_rejected_on_legacy_or_malformed_gossip() {
for stream_type in [
STREAM_TUNNEL,
STREAM_TUNNEL_HTTP,
STREAM_TUNNEL_MAP,
STREAM_PEER_DOWN,
STREAM_PEER_LEAVING,
@ -3319,6 +3319,10 @@ fn incoming_peer_rejected_on_legacy_or_malformed_gossip() {
stream_allowed_before_admission(STREAM_ROUTE_REQUEST),
"STREAM_ROUTE_REQUEST must bypass the gate (passive/client request-only path)"
);
assert!(
stream_allowed_before_admission(STREAM_TUNNEL_HTTP),
"STREAM_TUNNEL_HTTP must bypass the gate (passive/client inference path)"
);
let peer_id = EndpointId::from(SecretKey::from_bytes(&[0xcd; 32]).public());
let peers: HashMap<EndpointId, PeerInfo> = HashMap::new();
@ -3345,7 +3349,6 @@ fn passive_route_table_request_does_not_admit_peer() {
for &gated in &[
STREAM_TUNNEL,
STREAM_TUNNEL_HTTP,
STREAM_TUNNEL_MAP,
STREAM_PEER_DOWN,
STREAM_PEER_LEAVING,
@ -5186,18 +5189,16 @@ fn dead_peer_ttl_expires() {
/// Verifies that non-scope tunnel streams (0x02 STREAM_TUNNEL and 0x04
/// STREAM_TUNNEL_HTTP) are NOT subject to protobuf frame validation — they are
/// raw byte pass-throughs and must not be accidentally broken by the cut-over.
/// Also verifies they are correctly gated by admission policy.
/// Also verifies their admission policy.
#[test]
fn non_scope_tunnel_streams_pass_through_without_proto_validation() {
// 0x02 and 0x04 must NOT be allowed before admission (they are raw TCP tunnels,
// quarantined until the peer is admitted via gossip).
assert!(
!stream_allowed_before_admission(STREAM_TUNNEL),
"STREAM_TUNNEL (0x02) must be gated until after gossip admission"
);
assert!(
!stream_allowed_before_admission(STREAM_TUNNEL_HTTP),
"STREAM_TUNNEL_HTTP (0x04) must be gated until after gossip admission"
stream_allowed_before_admission(STREAM_TUNNEL_HTTP),
"STREAM_TUNNEL_HTTP (0x04) must be allowed for passive SDK inference"
);
// After admission these streams are live. Verify that the stream type constants
@ -5241,15 +5242,10 @@ fn non_scope_tunnel_streams_pass_through_without_proto_validation() {
err
);
// Verify that all admission-gated streams besides tunnels are also gated
// (completeness check for non-scope stream policy)
for stream in [STREAM_TUNNEL, STREAM_TUNNEL_HTTP] {
assert!(
!stream_allowed_before_admission(stream),
"stream {:#04x} must require admission (raw tunnel security boundary)",
stream
);
}
assert!(
!stream_allowed_before_admission(STREAM_TUNNEL),
"STREAM_TUNNEL must require admission (raw tunnel security boundary)"
);
}
/// Proves the behavioral contract introduced in the reconnect fix:

View file

@ -1,12 +1,12 @@
//! Managed model acquisition helpers.
use super::track_managed_model_usage;
use crate::cli::output::{ModelProgressStatus, OutputEvent, emit_event, interactive_tui_active};
use crate::cli::terminal_progress::{SpinnerHandle, start_spinner};
use anyhow::{Context, Result};
use hf_hub::progress::{DownloadEvent, Progress, ProgressEvent, ProgressHandler};
#[cfg(test)]
use hf_hub::progress::{FileProgress, FileStatus};
use mesh_llm_events::terminal_progress::{SpinnerHandle, start_spinner};
use mesh_llm_events::{ModelProgressStatus, OutputEvent, emit_event, interactive_tui_active};
#[cfg(test)]
use std::collections::HashMap;
use std::io::Write;

View file

@ -1,326 +1,25 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use model_hf::store::delete::DeleteModelCatalog;
use anyhow::{Context, Result, bail};
use hf_hub::cache::{CachedFileInfo, CachedRevisionInfo, HFCacheInfo};
use hf_hub::{RepoType, RepoTypeModel};
use crate::models::local::{
gguf_metadata_cache_path, huggingface_hub_cache_dir, huggingface_identity_for_path,
mesh_llm_cache_dir, scan_hf_cache_info, split_gguf_base_name,
};
use crate::models::resolve::{
DeleteModelRef, parse_delete_model_ref, resolve_huggingface_file_from_sibling_entries,
};
use crate::models::usage;
#[derive(Debug)]
pub struct DeleteResult {
pub deleted_paths: Vec<PathBuf>,
pub reclaimed_bytes: u64,
pub removed_metadata_files: usize,
pub removed_usage_records: usize,
pub removed_derived_cache_files: usize,
}
pub async fn resolve_model_identifier(identifier: &str) -> Result<Vec<PathBuf>> {
match parse_delete_model_ref(identifier).await? {
DeleteModelRef::LocalStem(stem) => {
let path = crate::models::find_model_path(&stem);
if !path.exists() {
bail!("Model not found: {}", identifier);
}
let mut resolved = BTreeSet::from([normalize_path(&path)]);
if let Some(cache_info) = scan_hf_cache_info(&huggingface_hub_cache_dir()) {
resolved.extend(find_related_hf_cache_paths(&cache_info, &path));
}
Ok(resolved.into_iter().collect())
}
DeleteModelRef::HuggingFace {
repo,
revision,
file,
} => resolve_cached_hf_ref(&repo, revision.as_deref(), &file)
.await
.with_context(|| format!("Resolve installed model ref {identifier}")),
}
}
fn normalized_gguf_stem(stem: &str) -> &str {
let stem = stem.strip_suffix(".gguf").unwrap_or(stem);
split_gguf_base_name(stem).unwrap_or(stem)
}
async fn resolve_cached_hf_ref(
repo_id: &str,
revision: Option<&str>,
file: &str,
) -> Result<Vec<PathBuf>> {
let cache_root = huggingface_hub_cache_dir();
let Some(cache_info) = scan_hf_cache_info(&cache_root) else {
bail!("Model not found: {repo_id}");
};
for repo in &cache_info.repos {
if repo.repo_type != RepoTypeModel.singular() || repo.repo_id != repo_id {
continue;
}
for cached_revision in &repo.revisions {
if revision.is_some_and(|requested| {
requested != cached_revision.commit_hash
&& !cached_revision.refs.iter().any(|r| r == requested)
}) {
continue;
}
if file.is_empty() && repo_id.ends_with("-layers") {
if cached_revision
.files
.iter()
.any(|file| is_layered_package_gguf_artifact(cached_revision, file))
{
let matches = layered_package_owned_paths(cached_revision);
return Ok(matches);
}
bail!("Delete only supports GGUF models: {repo_id}");
}
let sibling_entries: Vec<(String, Option<u64>)> = cached_revision
.files
.iter()
.map(|entry| {
let size = std::fs::metadata(&entry.file_path)
.ok()
.map(|meta| meta.len());
(entry.file_name.clone(), size)
})
.collect();
let resolved_file = resolve_huggingface_file_from_sibling_entries(
repo_id,
revision.or_else(|| cached_revision.refs.first().map(String::as_str)),
file,
&sibling_entries,
)
.await?;
if !resolved_file.ends_with(".gguf") {
bail!("Delete only supports GGUF models: {repo_id}");
}
let expected = normalized_gguf_stem(&resolved_file);
let mut matches: Vec<PathBuf> = cached_revision
.files
.iter()
.filter(|entry| entry.file_name.ends_with(".gguf"))
.filter(|entry| {
normalized_gguf_stem(&entry.file_name).eq_ignore_ascii_case(expected)
})
.map(|entry| entry.file_path.clone())
.collect();
if !matches.is_empty() {
matches.sort();
return Ok(matches);
}
}
}
bail!("Model not found: {repo_id}")
}
fn layered_package_owned_paths(revision: &CachedRevisionInfo) -> Vec<PathBuf> {
let mut matches: Vec<PathBuf> = revision
.files
.iter()
.map(|file| file.file_path.clone())
.collect();
matches.sort();
matches
}
fn is_layered_package_gguf_artifact(revision: &CachedRevisionInfo, file: &CachedFileInfo) -> bool {
let relative = file
.file_path
.strip_prefix(&revision.snapshot_path)
.unwrap_or(file.file_path.as_path())
.to_string_lossy()
.replace('\\', "/");
(relative.starts_with("shared/") || relative.starts_with("layers/"))
&& relative.ends_with(".gguf")
}
fn find_related_hf_cache_paths(cache_info: &HFCacheInfo, path: &Path) -> Vec<PathBuf> {
let mut results = BTreeSet::new();
let Some(identity) = huggingface_identity_for_path(path) else {
return Vec::new();
};
let Some(file_name) = Path::new(&identity.file)
.file_name()
.and_then(|value| value.to_str())
else {
return Vec::new();
};
let expected = normalized_gguf_stem(file_name);
for repo in &cache_info.repos {
if repo.repo_type != RepoTypeModel.singular() || repo.repo_id != identity.repo_id {
continue;
}
for revision in &repo.revisions {
if revision.commit_hash != identity.revision {
continue;
}
for file in &revision.files {
if !file.file_name.ends_with(".gguf") {
continue;
}
if normalized_gguf_stem(&file.file_name).eq_ignore_ascii_case(expected) {
results.insert(file.file_path.clone());
}
}
}
}
results.into_iter().collect()
}
pub fn collect_delete_paths(resolved_paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut to_delete: BTreeSet<PathBuf> = BTreeSet::new();
if resolved_paths.is_empty() {
return Ok(Vec::new());
}
for path in resolved_paths {
ensure_delete_path_allowed(path)?;
to_delete.insert(normalize_path(path));
}
let primary_path = &resolved_paths[0];
if let Some(record) = usage::load_model_usage_record_for_path(primary_path)
&& record.mesh_managed
&& !record.managed_paths.is_empty()
{
for p in &record.managed_paths {
to_delete.insert(normalize_path(p));
}
}
Ok(to_delete.into_iter().collect())
}
pub async fn delete_model_by_identifier(identifier: &str) -> Result<DeleteResult> {
let resolved_paths = resolve_model_identifier(identifier).await?;
if resolved_paths.is_empty() {
bail!("Model not found: {}", identifier);
}
let all_paths = collect_delete_paths(&resolved_paths)?;
if all_paths.is_empty() {
bail!(
"No GGUF files found at resolved path: {}",
resolved_paths[0].display()
);
}
let mut reclaimed_bytes: u64 = 0;
let mut removed_metadata_files: usize = 0;
let mut removed_usage_records: usize = 0;
let mut deleted_paths: Vec<PathBuf> = Vec::new();
let mut removed_record_paths = BTreeSet::new();
for path in &all_paths {
if path.exists() {
if let Ok(meta) = std::fs::metadata(path) {
reclaimed_bytes += meta.len();
}
std::fs::remove_file(path).with_context(|| format!("Remove {}", path.display()))?;
deleted_paths.push(path.clone());
if let Some(metadata_path) = gguf_metadata_cache_path(path)
&& metadata_path.exists()
{
std::fs::remove_file(&metadata_path).with_context(|| {
format!("Remove metadata cache {}", metadata_path.display())
})?;
removed_metadata_files += 1;
}
prune_empty_ancestors(path, &huggingface_hub_cache_dir());
}
}
for path in &all_paths {
if let Some(record) = load_model_usage_record_for_path(path) {
let usage_dir = usage::model_usage_cache_dir();
let record_path = usage::usage_record_path(&usage_dir, &record.lookup_key);
if removed_record_paths.insert(record_path.clone()) && record_path.exists() {
std::fs::remove_file(&record_path)
.with_context(|| format!("Remove usage record {}", record_path.display()))?;
removed_usage_records += 1;
}
}
}
Ok(DeleteResult {
deleted_paths,
reclaimed_bytes,
removed_metadata_files,
removed_usage_records,
removed_derived_cache_files: 0,
})
}
/// Load a model usage record for a given path.
fn load_model_usage_record_for_path(path: &std::path::Path) -> Option<usage::ModelUsageRecord> {
usage::load_model_usage_record_for_path(path)
}
fn ensure_delete_path_allowed(path: &Path) -> Result<()> {
let normalized = normalize_path(path);
let hf_root = normalize_path(&huggingface_hub_cache_dir());
let mesh_root = normalize_path(&mesh_llm_cache_dir());
if normalized.starts_with(&hf_root) || normalized.starts_with(&mesh_root) {
Ok(())
} else {
bail!(
"Deletion target outside known model roots: {}",
normalized.display()
);
}
}
/// Prune empty ancestor directories up to (but not including) stop_at.
fn prune_empty_ancestors(path: &std::path::Path, stop_at: &std::path::Path) {
let stop_at = normalize_path(stop_at);
let mut current = path.parent().map(normalize_path);
while let Some(dir) = current {
if dir == stop_at {
break;
}
let Ok(mut entries) = std::fs::read_dir(&dir) else {
break;
};
if entries.next().is_some() {
break;
}
if std::fs::remove_dir(&dir).is_err() {
break;
}
current = dir.parent().map(normalize_path);
}
}
fn normalize_path(path: &std::path::Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
pub use model_hf::store::delete::DeleteResult;
#[cfg(test)]
mod tests {
use super::*;
pub use model_hf::store::delete::resolve_huggingface_file_from_sibling_entries;
#[test]
fn normalized_gguf_stem_collapses_split_shards() {
assert_eq!(
normalized_gguf_stem("GLM-5-UD-IQ2_XXS-00001-of-00006.gguf"),
"GLM-5-UD-IQ2_XXS"
);
assert_eq!(normalized_gguf_stem("Qwen3-8B-Q4_K_M"), "Qwen3-8B-Q4_K_M");
struct HostDeleteCatalog;
impl DeleteModelCatalog for HostDeleteCatalog {
fn local_stem_for_identifier(&self, identifier: &str) -> Option<String> {
crate::models::remote_catalog::find_model_exact(identifier)
.map(|model| model.file.trim_end_matches(".gguf").to_string())
}
}
pub async fn resolve_model_identifier(identifier: &str) -> anyhow::Result<Vec<std::path::PathBuf>> {
model_hf::store::delete::resolve_model_identifier_with_catalog(identifier, &HostDeleteCatalog)
.await
}
pub async fn delete_model_by_identifier(identifier: &str) -> anyhow::Result<DeleteResult> {
model_hf::store::delete::delete_model_by_identifier_with_catalog(identifier, &HostDeleteCatalog)
.await
}

View file

@ -3,8 +3,10 @@ use std::path::{Path, PathBuf};
use serial_test::serial;
use crate::models::delete::{delete_model_by_identifier, resolve_model_identifier};
use crate::models::resolve::resolve_huggingface_file_from_sibling_entries;
use crate::models::delete::{
delete_model_by_identifier, resolve_huggingface_file_from_sibling_entries,
resolve_model_identifier,
};
fn unique_temp_dir(prefix: &str) -> PathBuf {
let stamp = std::time::SystemTime::now()

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
use super::{build_hf_api, huggingface_hub_cache_dir, run_hf_sync, short_revision};
use crate::cli::terminal_progress::{DeterminateProgressLine, clear_stderr_line};
use anyhow::{Context, Result};
use hf_hub::{RepoTypeModel, repository::ModelInfo};
use mesh_llm_events::terminal_progress::{DeterminateProgressLine, clear_stderr_line};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

View file

@ -8,15 +8,15 @@ use std::{
collections::HashSet,
fs,
path::{Component, Path, PathBuf},
sync::{Mutex, RwLock},
sync::{
Mutex, RwLock,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant, SystemTime},
};
#[cfg(test)]
use std::sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering},
};
use std::sync::{Arc, LazyLock};
use anyhow::{Context, Result, bail};
use model_resolver::{
@ -55,7 +55,6 @@ static CATALOG_REFRESH_BACKOFF_UNTIL: Mutex<Option<Instant>> = Mutex::new(None);
/// cached catalog is already available.
const CATALOG_REFRESH_BACKOFF: Duration = Duration::from_secs(5 * 60);
#[cfg(test)]
static CATALOG_ENTRIES_OVERRIDE_ACTIVE: AtomicBool = AtomicBool::new(false);
#[cfg(test)]
@ -65,8 +64,8 @@ type HfModelFileProbe = Arc<dyn Fn(&str, &str, &str) -> bool + Send + Sync>;
static HF_MODEL_FILE_PROBE_OVERRIDE: LazyLock<Mutex<Option<HfModelFileProbe>>> =
LazyLock::new(|| Mutex::new(None));
#[cfg(test)]
pub(crate) struct CatalogEntriesOverrideGuard {
#[doc(hidden)]
pub struct CatalogEntriesOverrideGuard {
previous_entries: Option<Vec<CatalogEntry>>,
previous_override_active: bool,
}
@ -76,10 +75,8 @@ pub(crate) struct HfModelFileProbeOverrideGuard {
previous_probe: Option<HfModelFileProbe>,
}
#[cfg(test)]
pub(crate) fn set_catalog_entries_for_test(
entries: Vec<CatalogEntry>,
) -> CatalogEntriesOverrideGuard {
#[doc(hidden)]
pub fn set_catalog_entries_for_test(entries: Vec<CatalogEntry>) -> CatalogEntriesOverrideGuard {
let previous_override_active = CATALOG_ENTRIES_OVERRIDE_ACTIVE.swap(true, Ordering::SeqCst);
let mut lock = CATALOG_ENTRIES.write().unwrap();
let previous = lock.replace(entries);
@ -89,7 +86,6 @@ pub(crate) fn set_catalog_entries_for_test(
}
}
#[cfg(test)]
impl Drop for CatalogEntriesOverrideGuard {
fn drop(&mut self) {
*CATALOG_ENTRIES.write().unwrap() = self.previous_entries.take();
@ -245,15 +241,12 @@ pub fn load_catalog_from_disk() -> Result<()> {
/// Ensures the catalog is loaded — refreshes if stale, otherwise loads from disk.
pub fn ensure_catalog() -> Result<()> {
#[cfg(test)]
{
if CATALOG_ENTRIES_OVERRIDE_ACTIVE.load(Ordering::SeqCst) {
let lock = CATALOG_ENTRIES
.read()
.map_err(|_| anyhow::anyhow!("catalog lock poisoned"))?;
if lock.is_some() {
return Ok(());
}
if CATALOG_ENTRIES_OVERRIDE_ACTIVE.load(Ordering::SeqCst) {
let lock = CATALOG_ENTRIES
.read()
.map_err(|_| anyhow::anyhow!("catalog lock poisoned"))?;
if lock.is_some() {
return Ok(());
}
}

View file

@ -4,9 +4,9 @@ use super::{
capabilities, catalog, find_model_path, format_size_bytes, huggingface_identity_for_path,
remote_catalog, track_model_usage,
};
use crate::cli::terminal_progress::start_spinner;
use crate::models::usage::ModelUsageRecord;
use anyhow::{Context, Result, bail};
use mesh_llm_events::terminal_progress::start_spinner;
use model_artifact::{ModelArtifactFile, select_primary_artifact_file};
use serde::Deserialize;
use std::cmp::Ordering;
@ -56,16 +56,6 @@ enum ExactModelRef {
},
}
#[derive(Clone, Debug)]
pub(crate) enum DeleteModelRef {
LocalStem(String),
HuggingFace {
repo: String,
revision: Option<String>,
file: String,
},
}
pub(super) fn merge_capabilities(
left: ModelCapabilities,
right: ModelCapabilities,
@ -452,50 +442,6 @@ pub fn installed_model_huggingface_ref(identity: &HuggingFaceModelIdentity) -> S
format_huggingface_display_ref(&identity.repo_id, None, &identity.file)
}
pub(crate) async fn parse_delete_model_ref(input: &str) -> Result<DeleteModelRef> {
if input.starts_with("http://") || input.starts_with("https://") {
bail!("Delete does not support direct URLs. Use a model stem or Hugging Face ref.");
}
if Path::new(input).is_absolute()
|| input.contains('\\')
|| input.starts_with("./")
|| input.starts_with("../")
|| input.starts_with("~/")
{
bail!("Delete does not support filesystem paths. Use a model stem or Hugging Face ref.");
}
if let Some(model) = find_remote_catalog_model_exact(input) {
let stem = model.file.trim_end_matches(".gguf");
if find_model_path(stem).exists() {
return Ok(DeleteModelRef::LocalStem(stem.to_string()));
}
}
if !input.contains('/') {
let installed_name = input.strip_suffix(".gguf").unwrap_or(input);
if find_model_path(installed_name).exists() {
return Ok(DeleteModelRef::LocalStem(installed_name.to_string()));
}
}
let canonical = canonicalize_model_ref_input(input).await?;
match parse_exact_model_ref(&canonical)? {
ExactModelRef::Catalog(model) => Ok(DeleteModelRef::LocalStem(
model.file.trim_end_matches(".gguf").to_string(),
)),
ExactModelRef::HuggingFace {
repo,
revision,
file,
} => Ok(DeleteModelRef::HuggingFace {
repo,
revision,
file,
}),
}
}
pub(super) fn matching_remote_catalog_model_for_huggingface(
repo: &str,
revision: Option<&str>,
@ -537,9 +483,6 @@ fn parse_huggingface_repo_url(input: &str) -> Option<(String, Option<String>, Op
}
fn parse_exact_model_ref(input: &str) -> Result<ExactModelRef> {
if let Some(model) = find_remote_catalog_model_exact(input) {
return Ok(ExactModelRef::Catalog(Box::new(model)));
}
if let Some((repo, revision, file)) = parse_huggingface_ref(input) {
return Ok(ExactModelRef::HuggingFace {
repo,
@ -561,6 +504,9 @@ fn parse_exact_model_ref(input: &str) -> Result<ExactModelRef> {
file: selector.unwrap_or_default(),
});
}
if let Some(model) = find_remote_catalog_model_exact(input) {
return Ok(ExactModelRef::Catalog(Box::new(model)));
}
bail!(
"Expected an exact model ref. Use a catalog id or a Hugging Face ref like org/repo, org/repo@rev:QUANT, org/repo/file.gguf, org/repo/file-stem for split GGUFs, org/repo/model.safetensors, or org/repo/model-00001-of-00048.safetensors."
)

View file

@ -1,968 +1 @@
use super::local::{
gguf_metadata_cache_path, huggingface_hub_cache_dir, huggingface_identity_for_path,
};
use anyhow::{Context, Result};
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct ModelUsageRecord {
pub lookup_key: String,
pub display_name: String,
pub model_ref: Option<String>,
pub source: String,
pub mesh_managed: bool,
pub primary_path: PathBuf,
pub managed_paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hf_repo_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hf_revision: Option<String>,
pub first_seen_at: String,
pub last_used_at: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct ModelCleanupCandidate {
pub display_name: String,
pub model_ref: Option<String>,
pub source: String,
pub primary_path: PathBuf,
pub mesh_managed: bool,
pub last_used_at: String,
pub file_count: usize,
pub total_bytes: u64,
pub stale_record_only: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct ModelCleanupPlan {
pub candidates: Vec<ModelCleanupCandidate>,
pub total_files: usize,
pub total_bytes: u64,
pub skipped_recent: usize,
pub stale_record_only: usize,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct ModelCleanupResult {
pub removed_candidates: usize,
pub removed_files: usize,
pub removed_records: usize,
pub removed_metadata_files: usize,
pub reclaimed_bytes: u64,
}
#[derive(Clone, Debug)]
struct CleanupEntry {
record: ModelUsageRecord,
record_path: PathBuf,
removable_paths: Vec<PathBuf>,
total_bytes: u64,
stale_record_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct RecordLocation {
lookup_key: String,
record_path: PathBuf,
record: Option<ModelUsageRecord>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct HuggingFaceRecordIdentity {
repo_id: String,
revision: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PathHuggingFaceIdentity {
repo_id: String,
revision: String,
canonical_ref: String,
}
pub fn model_usage_cache_dir() -> PathBuf {
super::mesh_llm_cache_dir().join("model-usage")
}
pub fn load_model_usage_record_for_path(path: &Path) -> Option<ModelUsageRecord> {
let usage_dir = model_usage_cache_dir();
let root = huggingface_hub_cache_dir();
let lookup_key = usage_lookup_key(path, &root)?;
resolve_record_location(&usage_dir, &lookup_key, &[normalize_path(path)]).record
}
pub fn track_model_usage(
path: &Path,
display_name: Option<&str>,
model_ref: Option<&str>,
source: Option<&str>,
) -> Result<()> {
let usage_dir = model_usage_cache_dir();
let root = huggingface_hub_cache_dir();
record_model_usage_in_dir(
&usage_dir,
&root,
path,
&[],
display_name,
model_ref,
source,
false,
)
}
pub fn track_managed_model_usage(
primary_path: &Path,
managed_paths: &[PathBuf],
display_name: &str,
model_ref: Option<&str>,
source: &str,
) -> Result<()> {
let usage_dir = model_usage_cache_dir();
let root = huggingface_hub_cache_dir();
record_model_usage_in_dir(
&usage_dir,
&root,
primary_path,
managed_paths,
Some(display_name),
model_ref,
Some(source),
true,
)
}
pub fn plan_model_cleanup(unused_since: Option<Duration>) -> Result<ModelCleanupPlan> {
let usage_dir = model_usage_cache_dir();
let root = huggingface_hub_cache_dir();
plan_model_cleanup_in_dir(&usage_dir, &root, unused_since)
}
pub fn execute_model_cleanup(unused_since: Option<Duration>) -> Result<ModelCleanupResult> {
let usage_dir = model_usage_cache_dir();
let root = huggingface_hub_cache_dir();
let records = load_model_usage_records_from_dir(&usage_dir);
let cutoff = unused_since
.map(ChronoDuration::from_std)
.transpose()?
.map(|age| Utc::now() - age);
let mut skipped_recent = 0usize;
let entries = plan_cleanup_entries(records, &usage_dir, &root, cutoff, &mut skipped_recent);
execute_model_cleanup_entries(entries)
}
fn load_model_usage_records_from_dir(dir: &Path) -> Vec<ModelUsageRecord> {
let mut records = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return records;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|value| value.to_str()) != Some("json") {
continue;
}
if let Some(record) = read_usage_record(&path) {
records.push(record);
}
}
records
}
fn read_usage_record(path: &Path) -> Option<ModelUsageRecord> {
let bytes = std::fs::read(path).ok()?;
serde_json::from_slice(&bytes).ok()
}
#[allow(clippy::too_many_arguments)]
fn record_model_usage_in_dir(
usage_dir: &Path,
track_root: &Path,
path: &Path,
managed_paths: &[PathBuf],
display_name: Option<&str>,
model_ref: Option<&str>,
source: Option<&str>,
mesh_managed: bool,
) -> Result<()> {
let Some(lookup_key) = usage_lookup_key(path, track_root) else {
return Ok(());
};
let now = Utc::now().to_rfc3339();
let primary_path = normalize_path(path);
let normalized_managed_paths = unique_paths(managed_paths.to_vec());
let candidate_paths = usage_record_candidate_paths(&primary_path, &normalized_managed_paths);
let location = resolve_record_location(usage_dir, &lookup_key, &candidate_paths);
let record_path = location.record_path;
let existing = location.record;
let existing_display_name = existing
.as_ref()
.map(|record| record.display_name.as_str())
.filter(|value| !value.is_empty());
let existing_source = existing
.as_ref()
.map(|record| record.source.as_str())
.filter(|value| !value.is_empty());
let existing_model_ref = existing
.as_ref()
.and_then(|record| record.model_ref.as_deref());
let mut merged_paths = existing
.as_ref()
.map(|record| record.managed_paths.clone())
.unwrap_or_default();
if mesh_managed {
if normalized_managed_paths.is_empty() {
merged_paths.push(primary_path.clone());
} else {
merged_paths.extend(normalized_managed_paths.iter().cloned());
}
}
merged_paths = unique_paths(merged_paths);
let hf_identity = infer_record_hf_identity(&primary_path, &merged_paths, track_root)
.or_else(|| existing.as_ref().and_then(record_hf_identity));
let record = ModelUsageRecord {
lookup_key: location.lookup_key,
display_name: display_name
.or(existing_display_name)
.map(str::to_string)
.unwrap_or_else(|| default_display_name(&primary_path)),
model_ref: model_ref
.or(existing_model_ref)
.map(str::to_string)
.or_else(|| default_model_ref(&primary_path)),
source: source
.or(existing_source)
.map(str::to_string)
.unwrap_or_else(|| default_source(&primary_path)),
mesh_managed: mesh_managed || existing.as_ref().is_some_and(|record| record.mesh_managed),
primary_path,
managed_paths: merged_paths,
hf_repo_id: hf_identity
.as_ref()
.map(|identity| identity.repo_id.clone()),
hf_revision: hf_identity
.as_ref()
.map(|identity| identity.revision.clone()),
first_seen_at: existing
.as_ref()
.map(|record| record.first_seen_at.clone())
.unwrap_or_else(|| now.clone()),
last_used_at: now,
};
std::fs::create_dir_all(usage_dir)
.with_context(|| format!("Create {}", usage_dir.display()))?;
let bytes = serde_json::to_vec_pretty(&record)?;
std::fs::write(&record_path, bytes)
.with_context(|| format!("Write {}", record_path.display()))?;
Ok(())
}
fn plan_model_cleanup_in_dir(
usage_dir: &Path,
track_root: &Path,
unused_since: Option<Duration>,
) -> Result<ModelCleanupPlan> {
let records = load_model_usage_records_from_dir(usage_dir);
let cutoff = unused_since
.map(ChronoDuration::from_std)
.transpose()?
.map(|age| Utc::now() - age);
let mut skipped_recent = 0usize;
let entries = plan_cleanup_entries(records, usage_dir, track_root, cutoff, &mut skipped_recent);
let mut plan = ModelCleanupPlan {
skipped_recent,
..Default::default()
};
for entry in entries {
if entry.stale_record_only {
plan.stale_record_only += 1;
}
plan.total_files += entry.removable_paths.len();
plan.total_bytes += entry.total_bytes;
plan.candidates.push(ModelCleanupCandidate {
display_name: entry.record.display_name,
model_ref: entry.record.model_ref,
source: entry.record.source,
primary_path: entry.record.primary_path,
mesh_managed: entry.record.mesh_managed,
last_used_at: entry.record.last_used_at,
file_count: entry.removable_paths.len(),
total_bytes: entry.total_bytes,
stale_record_only: entry.stale_record_only,
});
}
plan.candidates.sort_by(|left, right| {
left.last_used_at
.cmp(&right.last_used_at)
.then_with(|| left.display_name.cmp(&right.display_name))
});
Ok(plan)
}
fn plan_cleanup_entries(
records: Vec<ModelUsageRecord>,
usage_dir: &Path,
track_root: &Path,
cutoff: Option<DateTime<Utc>>,
skipped_recent: &mut usize,
) -> Vec<CleanupEntry> {
let mut entries = Vec::new();
for record in records {
if !record.mesh_managed {
continue;
}
let last_used =
parse_timestamp(&record.last_used_at).unwrap_or(DateTime::<Utc>::UNIX_EPOCH);
if let Some(cutoff) = cutoff
&& last_used > cutoff
{
*skipped_recent += 1;
continue;
}
let removable_paths: Vec<PathBuf> = unique_paths(record.managed_paths.clone())
.into_iter()
.filter(|path| is_trackable_path(path, track_root))
.filter(|path| path_matches_record_identity(&record, path, track_root))
.filter(|path| path.exists())
.collect();
let total_bytes = removable_paths
.iter()
.filter_map(|path| std::fs::metadata(path).ok().map(|meta| meta.len()))
.sum();
let stale_record_only = removable_paths.is_empty();
let record_path = usage_record_path(usage_dir, &record.lookup_key);
entries.push(CleanupEntry {
record,
record_path,
removable_paths,
total_bytes,
stale_record_only,
});
}
entries.sort_by(|left, right| {
left.record
.last_used_at
.cmp(&right.record.last_used_at)
.then_with(|| left.record.display_name.cmp(&right.record.display_name))
});
entries
}
fn execute_model_cleanup_entries(entries: Vec<CleanupEntry>) -> Result<ModelCleanupResult> {
let mut result = ModelCleanupResult::default();
for entry in entries {
for path in &entry.removable_paths {
if let Ok(meta) = std::fs::metadata(path) {
result.reclaimed_bytes += meta.len();
}
if path.exists() {
std::fs::remove_file(path).with_context(|| format!("Remove {}", path.display()))?;
result.removed_files += 1;
}
if let Some(cache_path) = gguf_metadata_cache_path(path)
&& cache_path.exists()
{
std::fs::remove_file(&cache_path)
.with_context(|| format!("Remove metadata cache {}", cache_path.display()))?;
result.removed_metadata_files += 1;
}
prune_empty_ancestors(path, &huggingface_hub_cache_dir());
}
if entry.record_path.exists() {
std::fs::remove_file(&entry.record_path)
.with_context(|| format!("Remove {}", entry.record_path.display()))?;
result.removed_records += 1;
}
result.removed_candidates += 1;
}
Ok(result)
}
fn usage_lookup_key(path: &Path, track_root: &Path) -> Option<String> {
if !is_trackable_path(path, track_root) {
return None;
}
if let Some(identity) = hf_identity_for_path_in_root(path, track_root) {
return Some(format!("hf:{}", identity.canonical_ref));
}
if let Some(identity) = huggingface_identity_for_path(path) {
return Some(format!("hf:{}", identity.canonical_ref));
}
Some(format!(
"path:{}",
normalize_path(path).to_string_lossy().replace('\\', "/")
))
}
fn resolve_record_location(
usage_dir: &Path,
direct_lookup_key: &str,
candidate_paths: &[PathBuf],
) -> RecordLocation {
let direct_record_path = usage_record_path(usage_dir, direct_lookup_key);
if let Some(record) = read_usage_record(&direct_record_path) {
return RecordLocation {
lookup_key: direct_lookup_key.to_string(),
record_path: direct_record_path,
record: Some(record),
};
}
let Some(existing) = find_usage_record_by_paths(usage_dir, candidate_paths) else {
return RecordLocation {
lookup_key: direct_lookup_key.to_string(),
record_path: direct_record_path,
record: None,
};
};
let record_path = usage_record_path(usage_dir, &existing.lookup_key);
RecordLocation {
lookup_key: existing.lookup_key.clone(),
record_path,
record: Some(existing),
}
}
fn find_usage_record_by_paths(
usage_dir: &Path,
candidate_paths: &[PathBuf],
) -> Option<ModelUsageRecord> {
let candidate_paths = unique_paths(candidate_paths.to_vec());
if candidate_paths.is_empty() {
return None;
}
let candidate_set: HashSet<PathBuf> = candidate_paths.iter().cloned().collect();
load_model_usage_records_from_dir(usage_dir)
.into_iter()
.find(|record| record_matches_any_path(record, &candidate_set))
}
fn record_matches_any_path(record: &ModelUsageRecord, candidate_paths: &HashSet<PathBuf>) -> bool {
let primary_path = normalize_path(&record.primary_path);
if candidate_paths.contains(&primary_path) {
return true;
}
if record
.managed_paths
.iter()
.map(|path| normalize_path(path))
.any(|path| candidate_paths.contains(&path))
{
return true;
}
false
}
fn usage_record_candidate_paths(primary_path: &Path, managed_paths: &[PathBuf]) -> Vec<PathBuf> {
let mut paths = vec![primary_path.to_path_buf()];
paths.extend(managed_paths.iter().cloned());
unique_paths(paths)
}
fn infer_record_hf_identity(
primary_path: &Path,
managed_paths: &[PathBuf],
track_root: &Path,
) -> Option<HuggingFaceRecordIdentity> {
let mut paths = usage_record_candidate_paths(primary_path, managed_paths).into_iter();
let first = paths
.find_map(|path| hf_identity_for_path_in_root(&path, track_root))
.map(|identity| HuggingFaceRecordIdentity {
repo_id: identity.repo_id,
revision: identity.revision,
})?;
for path in usage_record_candidate_paths(primary_path, managed_paths) {
let Some(identity) = hf_identity_for_path_in_root(&path, track_root) else {
continue;
};
if identity.repo_id != first.repo_id || identity.revision != first.revision {
return None;
}
}
Some(first)
}
fn record_hf_identity(record: &ModelUsageRecord) -> Option<HuggingFaceRecordIdentity> {
Some(HuggingFaceRecordIdentity {
repo_id: record.hf_repo_id.clone()?,
revision: record.hf_revision.clone()?,
})
}
fn path_matches_record_identity(record: &ModelUsageRecord, path: &Path, track_root: &Path) -> bool {
let Some(record_identity) = record_hf_identity(record) else {
return true;
};
hf_identity_for_path_in_root(path, track_root).is_some_and(|identity| {
identity.repo_id == record_identity.repo_id && identity.revision == record_identity.revision
})
}
fn hf_identity_for_path_in_root(path: &Path, track_root: &Path) -> Option<PathHuggingFaceIdentity> {
let path = normalize_path(path);
let root = normalize_path(track_root);
let relative = path.strip_prefix(&root).ok()?;
let mut components = relative.components();
let repo_dir = components.next()?.as_os_str().to_str()?;
let repo_id = repo_dir.strip_prefix("models--")?.replace("--", "/");
if components.next()?.as_os_str() != "snapshots" {
return None;
}
let revision = components.next()?.as_os_str().to_str()?.to_string();
let relative_file = components
.map(|component| component.as_os_str().to_str())
.collect::<Option<Vec<_>>>()?
.join("/");
if relative_file.is_empty() {
return None;
}
Some(PathHuggingFaceIdentity {
repo_id: repo_id.clone(),
revision: revision.clone(),
canonical_ref: format!("{repo_id}@{revision}/{relative_file}"),
})
}
pub(crate) fn usage_record_path(usage_dir: &Path, lookup_key: &str) -> PathBuf {
let digest = Sha256::digest(lookup_key.as_bytes());
usage_dir.join(format!("{digest:x}.json"))
}
fn normalize_path(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn unique_paths(paths: Vec<PathBuf>) -> Vec<PathBuf> {
let mut seen = HashSet::new();
let mut unique = Vec::new();
for path in paths {
let normalized = normalize_path(&path);
if seen.insert(normalized.clone()) {
unique.push(normalized);
}
}
unique.sort();
unique
}
fn parse_timestamp(value: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_rfc3339(value)
.ok()
.map(|parsed| parsed.with_timezone(&Utc))
}
fn default_display_name(path: &Path) -> String {
path.file_stem()
.and_then(|value| value.to_str())
.or_else(|| path.file_name().and_then(|value| value.to_str()))
.unwrap_or("model")
.to_string()
}
fn default_model_ref(path: &Path) -> Option<String> {
huggingface_identity_for_path(path).map(|identity| identity.canonical_ref)
}
fn default_source(path: &Path) -> String {
if huggingface_identity_for_path(path).is_some() {
"huggingface-cache".to_string()
} else {
"local-cache".to_string()
}
}
fn is_trackable_path(path: &Path, track_root: &Path) -> bool {
let path = normalize_path(path);
let root = normalize_path(track_root);
path.starts_with(&root)
}
fn prune_empty_ancestors(path: &Path, stop_at: &Path) {
let stop_at = normalize_path(stop_at);
let mut current = path.parent().map(normalize_path);
while let Some(dir) = current {
if dir == stop_at {
break;
}
let Ok(mut entries) = std::fs::read_dir(&dir) else {
break;
};
if entries.next().is_some() {
break;
}
if std::fs::remove_dir(&dir).is_err() {
break;
}
current = dir.parent().map(normalize_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_dir(prefix: &str) -> PathBuf {
let sequence = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"{prefix}-{}-{}-{}",
std::process::id(),
sequence,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos()
))
}
fn write_record(dir: &Path, record: &ModelUsageRecord) {
std::fs::create_dir_all(dir).expect("usage dir should be created");
let path = usage_record_path(dir, &record.lookup_key);
std::fs::write(
path,
serde_json::to_vec_pretty(record).expect("record JSON should serialize"),
)
.expect("record should be written");
}
#[test]
fn record_model_usage_merges_managed_paths() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let primary = cache_root
.join("models--Org--Demo")
.join("snapshots")
.join("rev1")
.join("Demo-Q4_K_M.gguf");
let shard = cache_root
.join("models--Org--Demo")
.join("snapshots")
.join("rev1")
.join("Demo-Q4_K_M-00002-of-00002.gguf");
std::fs::create_dir_all(primary.parent().expect("primary path should have parent"))
.expect("primary parent should exist");
std::fs::write(&primary, b"primary").expect("primary model should be written");
std::fs::write(&shard, b"shard").expect("shard model should be written");
record_model_usage_in_dir(
&usage_dir,
&cache_root,
&primary,
&[primary.clone(), shard.clone()],
Some("Demo-Q4_K_M"),
Some("Org/Demo@rev1/Demo-Q4_K_M.gguf"),
Some("catalog"),
true,
)
.expect("managed usage should be recorded");
let records = load_model_usage_records_from_dir(&usage_dir);
assert_eq!(records.len(), 1);
assert!(records[0].mesh_managed);
assert_eq!(records[0].managed_paths.len(), 2);
assert_eq!(records[0].display_name, "Demo-Q4_K_M");
assert_eq!(records[0].hf_repo_id.as_deref(), Some("Org/Demo"));
assert_eq!(records[0].hf_revision.as_deref(), Some("rev1"));
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
#[test]
fn load_model_usage_record_for_split_shard_returns_bundle_record() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let primary = cache_root
.join("models--Org--Bundle")
.join("snapshots")
.join("rev1")
.join("Bundle-Q4_K_M-00001-of-00002.gguf");
let shard = cache_root
.join("models--Org--Bundle")
.join("snapshots")
.join("rev1")
.join("Bundle-Q4_K_M-00002-of-00002.gguf");
std::fs::create_dir_all(primary.parent().expect("primary path should have parent"))
.expect("primary parent should exist");
std::fs::write(&primary, b"primary").expect("primary model should be written");
std::fs::write(&shard, b"shard").expect("shard model should be written");
record_model_usage_in_dir(
&usage_dir,
&cache_root,
&primary,
&[primary.clone(), shard.clone()],
Some("Bundle-Q4_K_M"),
Some("Org/Bundle@rev1/Bundle-Q4_K_M-00001-of-00002.gguf"),
Some("catalog"),
true,
)
.expect("managed usage should be recorded");
let record = find_usage_record_by_paths(&usage_dir, std::slice::from_ref(&shard))
.expect("split shard should resolve back to the bundle record");
assert_eq!(
record.lookup_key,
usage_lookup_key(&primary, &cache_root).expect("primary path should key")
);
assert_eq!(record.managed_paths.len(), 2);
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
#[test]
fn record_model_usage_updates_last_used_at_by_managed_identity() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let primary = cache_root
.join("models--Org--Bundle")
.join("snapshots")
.join("rev1")
.join("Bundle-Q4_K_M-00001-of-00002.gguf");
let shard = cache_root
.join("models--Org--Bundle")
.join("snapshots")
.join("rev1")
.join("Bundle-Q4_K_M-00002-of-00002.gguf");
std::fs::create_dir_all(primary.parent().expect("primary path should have parent"))
.expect("primary parent should exist");
std::fs::write(&primary, b"primary").expect("primary model should be written");
std::fs::write(&shard, b"shard").expect("shard model should be written");
let lookup_key = usage_lookup_key(&primary, &cache_root).expect("primary path should key");
write_record(
&usage_dir,
&ModelUsageRecord {
lookup_key: lookup_key.clone(),
display_name: "Bundle-Q4_K_M".to_string(),
model_ref: Some("Org/Bundle@rev1/Bundle-Q4_K_M-00001-of-00002.gguf".to_string()),
source: "catalog".to_string(),
mesh_managed: true,
primary_path: primary.clone(),
managed_paths: vec![primary.clone(), shard.clone()],
hf_repo_id: Some("Org/Bundle".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: "2026-04-01T00:00:00Z".to_string(),
last_used_at: "2026-04-01T00:00:00Z".to_string(),
},
);
record_model_usage_in_dir(
&usage_dir,
&cache_root,
&shard,
&[],
None,
None,
Some("resolve"),
false,
)
.expect("usage refresh should succeed");
let records = load_model_usage_records_from_dir(&usage_dir);
assert_eq!(records.len(), 1);
assert_eq!(records[0].lookup_key, lookup_key);
assert_eq!(records[0].managed_paths.len(), 2);
assert_ne!(records[0].last_used_at, "2026-04-01T00:00:00Z");
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
#[test]
fn cleanup_plan_filters_recent_and_external_records() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let old_path = cache_root
.join("models--Org--Old")
.join("snapshots")
.join("rev1")
.join("Old-Q4_K_M.gguf");
let recent_path = cache_root
.join("models--Org--Recent")
.join("snapshots")
.join("rev1")
.join("Recent-Q4_K_M.gguf");
let external_path = cache_root
.join("models--Org--External")
.join("snapshots")
.join("rev1")
.join("External-Q4_K_M.gguf");
for path in [&old_path, &recent_path, &external_path] {
std::fs::create_dir_all(path.parent().expect("test path should have parent"))
.expect("test parent should exist");
std::fs::write(path, vec![0_u8; 16]).expect("test model should be written");
}
write_record(
&usage_dir,
&ModelUsageRecord {
lookup_key: usage_lookup_key(&old_path, &cache_root).expect("old path should key"),
display_name: "Old".to_string(),
model_ref: None,
source: "catalog".to_string(),
mesh_managed: true,
primary_path: old_path.clone(),
managed_paths: vec![old_path.clone()],
hf_repo_id: Some("Org/Old".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: "2026-04-01T00:00:00Z".to_string(),
last_used_at: "2026-04-01T00:00:00Z".to_string(),
},
);
write_record(
&usage_dir,
&ModelUsageRecord {
lookup_key: usage_lookup_key(&recent_path, &cache_root)
.expect("recent path should key"),
display_name: "Recent".to_string(),
model_ref: None,
source: "catalog".to_string(),
mesh_managed: true,
primary_path: recent_path.clone(),
managed_paths: vec![recent_path.clone()],
hf_repo_id: Some("Org/Recent".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: Utc::now().to_rfc3339(),
last_used_at: Utc::now().to_rfc3339(),
},
);
write_record(
&usage_dir,
&ModelUsageRecord {
lookup_key: usage_lookup_key(&external_path, &cache_root)
.expect("external path should key"),
display_name: "External".to_string(),
model_ref: None,
source: "local-cache".to_string(),
mesh_managed: false,
primary_path: external_path.clone(),
managed_paths: vec![],
hf_repo_id: Some("Org/External".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: "2026-04-01T00:00:00Z".to_string(),
last_used_at: "2026-04-01T00:00:00Z".to_string(),
},
);
let plan =
plan_model_cleanup_in_dir(&usage_dir, &cache_root, Some(Duration::from_secs(60)))
.expect("cleanup plan should succeed");
assert_eq!(plan.candidates.len(), 1);
assert_eq!(plan.candidates[0].display_name, "Old");
assert_eq!(plan.skipped_recent, 1);
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
#[test]
fn execute_cleanup_removes_files_and_records() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let primary = cache_root
.join("models--Org--Cleanup")
.join("snapshots")
.join("rev1")
.join("Cleanup-Q4_K_M.gguf");
std::fs::create_dir_all(primary.parent().expect("cleanup path should have parent"))
.expect("cleanup parent should exist");
std::fs::write(&primary, vec![0_u8; 32]).expect("cleanup model should be written");
let record = ModelUsageRecord {
lookup_key: usage_lookup_key(&primary, &cache_root).expect("cleanup path should key"),
display_name: "Cleanup".to_string(),
model_ref: Some("Org/Cleanup@rev1/Cleanup-Q4_K_M.gguf".to_string()),
source: "catalog".to_string(),
mesh_managed: true,
primary_path: primary.clone(),
managed_paths: vec![primary.clone()],
hf_repo_id: Some("Org/Cleanup".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: "2026-04-01T00:00:00Z".to_string(),
last_used_at: "2026-04-01T00:00:00Z".to_string(),
};
write_record(&usage_dir, &record);
let mut skipped_recent = 0usize;
let entries = plan_cleanup_entries(
vec![record],
&usage_dir,
&cache_root,
None,
&mut skipped_recent,
);
let result = execute_model_cleanup_entries(entries).expect("cleanup should succeed");
assert_eq!(result.removed_candidates, 1);
assert_eq!(result.removed_files, 1);
assert_eq!(result.removed_records, 1);
assert!(!primary.exists());
assert!(load_model_usage_records_from_dir(&usage_dir).is_empty());
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
#[test]
fn cleanup_skips_paths_that_no_longer_match_record_identity() {
let usage_dir = temp_dir("mesh-llm-usage-dir");
let cache_root = temp_dir("mesh-llm-hf-cache");
let old_path = cache_root
.join("models--Org--Actual")
.join("snapshots")
.join("rev2")
.join("Actual-Q4_K_M.gguf");
std::fs::create_dir_all(old_path.parent().expect("cleanup path should have parent"))
.expect("cleanup parent should exist");
std::fs::write(&old_path, vec![0_u8; 32]).expect("cleanup model should be written");
let record = ModelUsageRecord {
lookup_key: "hf:Org/Expected@rev1/Expected-Q4_K_M.gguf".to_string(),
display_name: "Expected".to_string(),
model_ref: Some("Org/Expected@rev1/Expected-Q4_K_M.gguf".to_string()),
source: "catalog".to_string(),
mesh_managed: true,
primary_path: old_path.clone(),
managed_paths: vec![old_path.clone()],
hf_repo_id: Some("Org/Expected".to_string()),
hf_revision: Some("rev1".to_string()),
first_seen_at: "2026-04-01T00:00:00Z".to_string(),
last_used_at: "2026-04-01T00:00:00Z".to_string(),
};
write_record(&usage_dir, &record);
let mut skipped_recent = 0usize;
let entries = plan_cleanup_entries(
vec![record],
&usage_dir,
&cache_root,
None,
&mut skipped_recent,
);
assert_eq!(entries.len(), 1);
assert!(entries[0].removable_paths.is_empty());
assert!(entries[0].stale_record_only);
assert!(old_path.exists());
let _ = std::fs::remove_dir_all(&usage_dir);
let _ = std::fs::remove_dir_all(&cache_root);
}
}
pub use model_hf::store::usage::*;

View file

@ -1,14 +1,14 @@
use anyhow::{Context, Result};
use clap::ValueEnum;
use mdns_sd::{DaemonStatus, ResolvedService, ServiceDaemon, ServiceEvent, ServiceInfo};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::time::Duration;
pub(crate) use crate::discovery::{DiscoveryScope, MeshDiscoveryMode};
use crate::network::nostr;
pub(crate) const LAN_SERVICE_TYPE: &str = "_mesh-llm._tcp.local.";
pub const LAN_SERVICE_TYPE: &str = "_mesh-llm._tcp.local.";
pub(crate) const LAN_DETAILS_PATH: &str = "/api/discovery/lan-details";
const TXT_SCHEMA_VERSION: u8 = 1;
const TXT_LIST_SEPARATOR: char = '|';
@ -19,55 +19,9 @@ const LAN_DETAILS_CHALLENGE_DOMAIN: &[u8] = b"mesh-llm-lan-details-challenge-v1\
const LAN_DETAILS_TOKEN_PROOF_DOMAIN: &[u8] = b"mesh-llm-lan-details-proof-v1\0";
const DAEMON_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub(crate) enum MeshDiscoveryMode {
#[default]
Nostr,
Mdns,
}
impl MeshDiscoveryMode {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Nostr => "nostr",
Self::Mdns => "mdns",
}
}
pub(crate) fn source(self) -> &'static str {
match self {
Self::Nostr => "nostr-relay",
Self::Mdns => "mdns-sd",
}
}
pub(crate) fn scope(self) -> DiscoveryScope {
match self {
Self::Nostr => DiscoveryScope::Public,
Self::Mdns => DiscoveryScope::Lan,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DiscoveryScope {
Public,
Lan,
}
impl DiscoveryScope {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::Lan => "lan",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum LanJoinMaterial {
pub enum LanJoinMaterial {
RequiresSuppliedToken,
}
@ -83,11 +37,11 @@ pub(crate) struct LanMeshAdvertisement {
pub(crate) node_count: usize,
pub(crate) client_count: usize,
pub(crate) max_clients: usize,
pub(crate) token_fingerprint: Option<String>,
pub token_fingerprint: Option<String>,
pub(crate) details_path: Option<String>,
pub(crate) proof_challenge: Option<String>,
pub(crate) app_version: Option<String>,
pub(crate) join_material: LanJoinMaterial,
pub join_material: LanJoinMaterial,
}
impl LanMeshAdvertisement {
@ -234,29 +188,29 @@ impl LanMeshAdvertisement {
}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct LanDiscoveredMesh {
pub(crate) mode: &'static str,
pub(crate) scope: DiscoveryScope,
pub(crate) source: &'static str,
pub(crate) service_type: &'static str,
pub(crate) instance_name: String,
pub(crate) host: String,
pub(crate) port: u16,
pub(crate) addresses: Vec<String>,
pub(crate) listing: nostr::MeshListing,
pub struct LanDiscoveredMesh {
pub mode: &'static str,
pub scope: DiscoveryScope,
pub source: &'static str,
pub service_type: &'static str,
pub instance_name: String,
pub host: String,
pub port: u16,
pub addresses: Vec<String>,
pub listing: nostr::MeshListing,
pub(crate) token_fingerprint: Option<String>,
pub(crate) details_path: Option<String>,
pub(crate) proof_challenge: Option<String>,
pub(crate) join_material: LanJoinMaterial,
pub(crate) joinable_with_supplied_token: bool,
pub(crate) published_version: Option<String>,
pub(crate) discovered_at: u64,
pub joinable_with_supplied_token: bool,
pub published_version: Option<String>,
pub discovered_at: u64,
#[serde(skip)]
join_token: Option<String>,
}
impl LanDiscoveredMesh {
pub(crate) fn join_token(&self) -> Option<&str> {
pub fn join_token(&self) -> Option<&str> {
self.join_token.as_deref()
}
@ -465,7 +419,7 @@ fn register_lan_service(
}
}
pub(crate) async fn discover_lan(
pub async fn discover_lan(
filter: &nostr::MeshFilter,
supplied_invite_token: Option<&str>,
timeout: Duration,

View file

@ -1,11 +1,11 @@
use crate::api;
use crate::cli::output::{OutputEvent, emit_event};
use crate::inference::{election, pipeline};
use crate::mesh;
use crate::network::affinity;
use crate::network::openai::auto_route;
use crate::network::openai::transport as proxy;
use crate::network::router;
use mesh_llm_events::{OutputEvent, emit_event};
use mesh_llm_node::serving::{UnloadOptions, UnloadTarget};
use mesh_mixture_of_agents as moa;

View file

@ -505,6 +505,7 @@ pub struct RoutingCandidate<'a> {
impl<'a> RoutingCandidate<'a> {
/// Build a candidate without any throughput hint. Useful for
/// pre-startup paths or test fixtures.
#[cfg(test)]
pub fn unscored(name: &'a str, caps: crate::models::ModelCapabilities) -> Self {
Self {
name,

View file

@ -296,7 +296,10 @@ mod tests {
include_str!("../../tests/fixtures/skippy_full_surface_invalid.toml");
fn documented_matrix_key_paths() -> BTreeSet<String> {
let matrix = include_str!("../../../../docs/skippy/CONFIGURATION.md");
let matrix = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/skippy/CONFIGURATION.md"
));
matrix
.lines()
.filter(|line| line.starts_with('|'))
@ -1416,7 +1419,10 @@ mmproj = "multimodal.gguf"
"omitted per-model request defaults should stay absent"
);
let matrix = include_str!("../../../../docs/skippy/CONFIGURATION.md");
let matrix = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/skippy/CONFIGURATION.md"
));
let matrix_keys = documented_matrix_key_paths();
assert!(
matrix_keys.len() >= 100,
@ -1437,9 +1443,10 @@ mmproj = "multimodal.gguf"
assert!(matrix.contains(key), "missing matrix doc entry {key}");
}
let docs_readme = include_str!("../../../../docs/README.md");
let usage = include_str!("../../../../docs/USAGE.md");
let cli = include_str!("../../../../docs/CLI.md");
let docs_readme =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/README.md"));
let usage = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/USAGE.md"));
let cli = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/CLI.md"));
assert!(docs_readme.contains("[skippy/CONFIGURATION.md](skippy/CONFIGURATION.md)"));
assert!(usage.contains("request payload values still win"));
assert!(cli.contains("Request defaults only fill absent or null request fields"));

View file

@ -590,7 +590,7 @@ impl PluginManager {
summaries
}
pub(crate) async fn shutdown(&self) {
pub async fn shutdown(&self) {
self.inner.shutting_down.store(true, Ordering::SeqCst);
for plugin in self.inner.plugins.values() {
plugin.shutdown().await;
@ -1150,7 +1150,7 @@ impl PluginManager {
}
#[cfg(test)]
pub async fn set_test_stream_handler<F>(&self, plugin_name: &str, handler: F)
pub(crate) async fn set_test_stream_handler<F>(&self, plugin_name: &str, handler: F)
where
F: Fn(proto::OpenStreamRequest) -> TestStreamFuture + Send + Sync + 'static,
{

View file

@ -1,8 +1,7 @@
use crate::cli::Cli;
use crate::cli::output::{OutputEvent, emit_event};
use crate::mesh;
use crate::network::{discovery as mesh_discovery, nostr, router};
use anyhow::{Context, Result};
use crate::network::{discovery as mesh_discovery, nostr};
use crate::runtime::RuntimeOptions;
use mesh_llm_events::{OutputEvent, emit_event};
use std::cmp::Reverse;
/// Health probe: try QUIC connect to the mesh's bootstrap node.
@ -402,21 +401,21 @@ fn current_unix_secs() -> u64 {
/// Helper for StartNew path — configure CLI to start a new mesh.
pub(super) fn start_new_mesh(
cli: &mut Cli,
options: &mut RuntimeOptions,
models: &[String],
my_vram_gb: f64,
has_startup_models: bool,
) {
let primary = models.first().cloned().unwrap_or_default();
if !has_startup_models && cli.model.is_empty() {
cli.model.push(primary.clone().into());
if !has_startup_models && options.model.is_empty() {
options.model.push(primary.clone().into());
}
let detail = if has_startup_models {
"using configured startup models".to_string()
} else {
format!("serving: {primary}")
};
let discovery = if cli.publish {
let discovery = if options.publish {
"publishing for discovery"
} else {
"mesh is private — add --publish to advertise it for discovery"
@ -430,7 +429,7 @@ pub(super) fn start_new_mesh(
});
}
pub(crate) fn nostr_relays(cli_relays: &[String]) -> Vec<String> {
pub fn nostr_relays(cli_relays: &[String]) -> Vec<String> {
if cli_relays.is_empty() {
nostr::DEFAULT_RELAYS
.iter()
@ -441,113 +440,6 @@ pub(crate) fn nostr_relays(cli_relays: &[String]) -> Vec<String> {
}
}
/// Ensure mesh-llm is running on `port`, then return (available_models, chosen_model, spawned_child).
///
/// Launcher behavior: if nothing is listening yet, auto-start `mesh-llm client --auto`
/// (client node — tunnels to mesh peers without publishing to Nostr).
/// Returns the child process handle if we spawned one, so callers can clean up on exit.
pub(crate) async fn check_mesh(
client: &reqwest::Client,
port: u16,
model: &Option<String>,
) -> Result<(Vec<String>, String, Option<std::process::Child>)> {
let url = format!("http://127.0.0.1:{port}/v1/models");
let mut child: Option<std::process::Child> = None;
if client.get(&url).send().await.is_err() {
let _ = emit_event(OutputEvent::Info {
message: format!("No mesh-llm on port {port} — starting background auto-join node"),
context: None,
});
let exe = std::env::current_exe().unwrap_or_else(|_| "mesh-llm".into());
child = Some(
std::process::Command::new(&exe)
.args(["client", "--auto", "--port", &port.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.context("Failed to start mesh-llm node")?,
);
}
let mut models: Vec<String> = Vec::new();
for i in 0..40 {
if let Ok(resp) = client.get(&url).send().await
&& let Ok(body) = resp.json::<serde_json::Value>().await
{
models = body["data"]
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|m| m["id"].as_str().map(String::from))
.collect();
if !models.is_empty() {
break;
}
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
if i % 5 == 4 {
let _ = emit_event(OutputEvent::Info {
message: format!("Waiting for mesh/models... ({:.0}s)", (i + 1) as f64 * 3.0),
context: Some(format!("port={port}")),
});
}
}
if models.is_empty() {
if let Some(mut c) = child {
let _ = c.kill();
}
anyhow::bail!(
"mesh-llm on port {port} has no models yet (or could not be reached).\n\
Ensure at least one serving peer is available on the mesh."
);
}
let chosen = if let Some(m) = model {
if !models.iter().any(|n| n == m) {
if let Some(mut c) = child {
let _ = c.kill();
let _ = c.wait();
}
anyhow::bail!(
"Model '{}' not available. Available: {}",
m,
models.join(", ")
);
}
m.clone()
} else {
// Pre-startup path: no live routing metrics yet, so candidates
// are scored as cold (uniform weight).
let available: Vec<router::RoutingCandidate<'_>> = models
.iter()
.map(|n| {
let caps = crate::models::installed_model_capabilities(n);
router::RoutingCandidate::unscored(n.as_str(), caps)
})
.collect();
let agentic = router::Classification {
category: router::Category::Code,
complexity: router::Complexity::Deep,
needs_tools: true,
has_media_inputs: false,
};
router::pick_model_classified(&agentic, &available)
.map(|s| s.to_string())
.unwrap_or_else(|| models[0].clone())
};
let _ = emit_event(OutputEvent::Info {
message: format!("Models: {}", models.join(", ")),
context: Some(format!("port={port}")),
});
let _ = emit_event(OutputEvent::Info {
message: format!("Using: {chosen}"),
context: Some(format!("port={port}")),
});
Ok((models, chosen, child))
}
#[cfg(test)]
mod tests {
use super::*;

Some files were not shown because too many files have changed in this diff Show more