diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 03f3ece17..732fa7be9 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -462,6 +462,20 @@ pub async fn mesh_node_status(state: State<'_, AppState>) -> CmdResult, +) -> CmdResult { + let runtime = state.mesh_llm_runtime.lock().await; + match runtime.as_ref() { + Some(runtime) => runtime.serving_usage().await.map_err(|e| e.to_string()), + None => Ok(mesh_llm::MeshServingUsage::default()), + } +} + #[tauri::command] pub async fn mesh_installed_models( state: State<'_, AppState>, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 9017356ab..af2bb3226 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -797,6 +797,7 @@ pub fn run() { mesh_start_node, mesh_stop_node, mesh_node_status, + mesh_serving_usage, mesh_installed_models, mesh_model_catalog, update_managed_agent, diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index 9b01d185c..acece498c 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -196,6 +196,80 @@ pub struct MeshNodeStatus { pub device_name: Option, } +/// Host-side "who is using the compute I'm sharing" snapshot. +/// +/// Read-only projection of the serving node's own runtime metrics (the same +/// `routing_metrics` / `inflight_requests` the SDK already exposes on the local +/// console). No new trust surface: it reads the node's own status payload. +/// +/// The local/remote/endpoint attempt split is what distinguishes *my own* +/// agent (local) from *another member consuming my compute* (remote/endpoint). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct MeshServingUsage { + /// Requests being served right now. + pub inflight: u64, + /// Highest concurrent in-flight seen this session. + pub peak_inflight: u64, + /// Total requests routed through this node. + pub requests_served: u64, + /// Completion tokens produced. + pub tokens_served: u64, + /// Recent decode throughput. + pub tokens_per_second: f64, + /// Requests served for this machine's own agents. + pub local_attempts: u64, + /// Requests served for a remote peer (someone else consuming my compute). + pub remote_attempts: u64, + /// Requests served via an advertised endpoint (also a remote consumer). + pub endpoint_attempts: u64, + /// Other nodes currently visible as peers. + pub peers: u64, +} + +impl MeshServingUsage { + /// True when at least one request has been served for a non-local consumer. + pub fn has_remote_consumers(&self) -> bool { + self.remote_attempts > 0 || self.endpoint_attempts > 0 + } +} + +/// Pure extractor: project a raw SDK status payload into [`MeshServingUsage`]. +/// +/// Every field is read defensively (missing → 0) so an SDK shape change +/// degrades to "no usage shown" rather than an error. Kept pure so it can be +/// unit-tested against a captured payload without a live runtime. +pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsage { + let u64_at = |v: &serde_json::Value| v.as_u64().unwrap_or(0); + let rm = payload.get("routing_metrics"); + let local = rm.and_then(|m| m.get("local_node")); + let get_u64 = |obj: Option<&serde_json::Value>, key: &str| { + obj.and_then(|o| o.get(key)).map(u64_at).unwrap_or(0) + }; + MeshServingUsage { + inflight: local + .and_then(|l| l.get("current_inflight_requests")) + .map(u64_at) + .or_else(|| payload.get("inflight_requests").map(u64_at)) + .unwrap_or(0), + peak_inflight: get_u64(local, "peak_inflight_requests"), + requests_served: get_u64(rm, "request_count"), + tokens_served: get_u64(rm, "completion_tokens_observed"), + tokens_per_second: rm + .and_then(|m| m.get("avg_tokens_per_second")) + .and_then(serde_json::Value::as_f64) + .unwrap_or(0.0), + local_attempts: get_u64(local, "local_attempt_count"), + remote_attempts: get_u64(local, "remote_attempt_count"), + endpoint_attempts: get_u64(local, "endpoint_attempt_count"), + peers: payload + .get("peers") + .and_then(serde_json::Value::as_array) + .map(|a| a.len() as u64) + .unwrap_or(0), + } +} + pub fn stopped_status() -> MeshNodeStatus { MeshNodeStatus { state: MeshNodeState::Off, @@ -428,6 +502,12 @@ impl DesktopMeshRuntime { Ok(payload) } + /// Read-only host-side usage snapshot from the node's own runtime metrics. + pub async fn serving_usage(&self) -> anyhow::Result { + let status = self.handle.status().await?; + Ok(serving_usage_from_payload(&status.payload)) + } + pub async fn dial_endpoint_addr(&self, endpoint_addr: impl Into) -> anyhow::Result<()> { let endpoint_addr = endpoint_addr.into(); let validated = validate_advertised_endpoint(&endpoint_addr)?; diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index 4c0c835ef..3f7f4cb70 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -597,3 +597,63 @@ fn owner_roster_without_membership_list_fails_closed() { assert!(super::owner_ids_from_events(&events).is_empty()); } + +#[test] +fn serving_usage_extracts_local_and_remote_attempts() { + // Captured shape from a live serving node's status payload. The local vs + // remote/endpoint split is what tells "my own agent" apart from "a peer + // consuming my compute". + let payload = json!({ + "inflight_requests": 0, + "peers": [], + "routing_metrics": { + "request_count": 5, + "completion_tokens_observed": 764, + "avg_tokens_per_second": 29.651, + "local_node": { + "current_inflight_requests": 1, + "peak_inflight_requests": 2, + "local_attempt_count": 4, + "remote_attempt_count": 0, + "endpoint_attempt_count": 0 + } + } + }); + let usage = super::serving_usage_from_payload(&payload); + assert_eq!(usage.inflight, 1); + assert_eq!(usage.peak_inflight, 2); + assert_eq!(usage.requests_served, 5); + assert_eq!(usage.tokens_served, 764); + assert_eq!(usage.local_attempts, 4); + assert_eq!(usage.remote_attempts, 0); + assert_eq!(usage.endpoint_attempts, 0); + assert!(!usage.has_remote_consumers(), "all-local traffic is not a remote consumer"); +} + +#[test] +fn serving_usage_flags_remote_consumer() { + let payload = json!({ + "peers": [{"id": "a"}, {"id": "b"}], + "routing_metrics": { + "request_count": 10, + "local_node": { + "local_attempt_count": 3, + "remote_attempt_count": 6, + "endpoint_attempt_count": 1 + } + } + }); + let usage = super::serving_usage_from_payload(&payload); + assert_eq!(usage.remote_attempts, 6); + assert_eq!(usage.endpoint_attempts, 1); + assert_eq!(usage.peers, 2); + assert!(usage.has_remote_consumers(), "remote/endpoint attempts mean someone else is using my compute"); +} + +#[test] +fn serving_usage_defaults_to_zero_on_missing_fields() { + // SDK shape drift must degrade to "no usage" not panic. + let usage = super::serving_usage_from_payload(&json!({})); + assert_eq!(usage, super::MeshServingUsage::default()); + assert!(!usage.has_remote_consumers()); +} diff --git a/desktop/src-tauri/src/mesh_llm_stubs.rs b/desktop/src-tauri/src/mesh_llm_stubs.rs index 779f7dbdd..e8c13f48e 100644 --- a/desktop/src-tauri/src/mesh_llm_stubs.rs +++ b/desktop/src-tauri/src/mesh_llm_stubs.rs @@ -26,6 +26,11 @@ pub async fn mesh_node_status(_state: State<'_, AppState>) -> CmdResult) -> CmdResult { + Err("mesh-llm feature not enabled".to_string()) +} + #[tauri::command] pub async fn mesh_installed_models( _state: State<'_, AppState>, diff --git a/desktop/src/features/mesh-compute/hooks/useMeshServingUsage.ts b/desktop/src/features/mesh-compute/hooks/useMeshServingUsage.ts new file mode 100644 index 000000000..a7e28be5d --- /dev/null +++ b/desktop/src/features/mesh-compute/hooks/useMeshServingUsage.ts @@ -0,0 +1,43 @@ +import * as React from "react"; + +import { meshServingUsage } from "@/shared/api/tauriMesh"; +import type { MeshServingUsage } from "@/shared/api/tauriMesh"; + +/** + * Polls host-side serving usage while this machine is sharing compute. + * + * Only polls when `enabled` (the card passes `isSharing`) so a machine that + * isn't serving does no work. Cadence is a plain 4s — usage is informational, + * not a lifecycle transition, so it doesn't need the adaptive stepping that + * `useMeshNodeStatus` uses. Returns `null` until the first successful fetch. + */ +export function useMeshServingUsage(enabled: boolean): MeshServingUsage | null { + const [usage, setUsage] = React.useState(null); + + React.useEffect(() => { + if (!enabled) { + setUsage(null); + return; + } + let cancelled = false; + const fetchOnce = () => { + (async () => { + try { + const value = await meshServingUsage(); + if (!cancelled) setUsage(value); + } catch { + // Usage is best-effort; a failed poll leaves the last value in place + // rather than flapping the indicator. + } + })(); + }; + fetchOnce(); + const handle = window.setInterval(fetchOnce, 4000); + return () => { + cancelled = true; + window.clearInterval(handle); + }; + }, [enabled]); + + return usage; +} diff --git a/desktop/src/features/mesh-compute/servingUsage.test.mjs b/desktop/src/features/mesh-compute/servingUsage.test.mjs new file mode 100644 index 000000000..bde42e02c --- /dev/null +++ b/desktop/src/features/mesh-compute/servingUsage.test.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { deriveServingIndicator } from "./servingUsage.ts"; + +function usage(overrides = {}) { + return { + inflight: 0, + peakInflight: 0, + requestsServed: 0, + tokensServed: 0, + tokensPerSecond: 0, + localAttempts: 0, + remoteAttempts: 0, + endpointAttempts: 0, + peers: 0, + ...overrides, + }; +} + +test("hidden when not sharing", () => { + const i = deriveServingIndicator(usage({ requestsServed: 5 }), false); + assert.equal(i.show, false); +}); + +test("hidden when usage not yet fetched", () => { + const i = deriveServingIndicator(null, true); + assert.equal(i.show, false); +}); + +test("sharing but nothing served yet -> idle, no detail", () => { + const i = deriveServingIndicator(usage(), true); + assert.equal(i.show, true); + assert.equal(i.active, false); + assert.equal(i.hasRemoteConsumers, false); + assert.match(i.label, /Idle/); +}); + +test("only local agent traffic -> not a remote consumer", () => { + const i = deriveServingIndicator( + usage({ requestsServed: 4, localAttempts: 4, tokensPerSecond: 30 }), + true, + ); + assert.equal(i.hasRemoteConsumers, false); + assert.match(i.label, /Idle/); // served earlier, none live now + assert.match(i.detail, /4 requests served/); +}); + +test("local agent live now -> serving your agent", () => { + const i = deriveServingIndicator( + usage({ inflight: 1, localAttempts: 2, tokensPerSecond: 28 }), + true, + ); + assert.equal(i.active, true); + assert.equal(i.hasRemoteConsumers, false); + assert.match(i.label, /Serving your agent/); + assert.match(i.label, /1 live/); +}); + +test("remote consumer, not live -> used by another member (headline case)", () => { + const i = deriveServingIndicator( + usage({ + requestsServed: 7, + remoteAttempts: 6, + endpointAttempts: 1, + peers: 2, + }), + true, + ); + assert.equal(i.hasRemoteConsumers, true); + assert.equal(i.active, false); + assert.match(i.label, /another member/); + assert.match(i.label, /7 requests/); // remote+endpoint = 7 + assert.match(i.detail, /2 peers/); +}); + +test("remote consumer live now -> in use now, singular peer/request grammar", () => { + const i = deriveServingIndicator( + usage({ inflight: 1, remoteAttempts: 1, peers: 1, tokensPerSecond: 31 }), + true, + ); + assert.equal(i.active, true); + assert.equal(i.hasRemoteConsumers, true); + assert.match(i.label, /In use now by another member/); + assert.match(i.detail, /1 peer\b/); // singular +}); diff --git a/desktop/src/features/mesh-compute/servingUsage.ts b/desktop/src/features/mesh-compute/servingUsage.ts new file mode 100644 index 000000000..fff7584af --- /dev/null +++ b/desktop/src/features/mesh-compute/servingUsage.ts @@ -0,0 +1,101 @@ +import type { MeshServingUsage } from "@/shared/api/tauriMesh"; + +/** + * Pure projection of host-side serving usage into a small, politely-worded + * indicator model for the Share compute card. + * + * Single source of truth for "who is using the compute I'm sharing" copy, so + * the component and its tests agree. Kept pure/total (accepts null = not yet + * fetched) and defensive (all fields optional-safe via the Rust extractor). + * + * Distinctions that matter: + * - `localAttempts` = this machine's OWN agents using the local model. Not a + * "someone else is here" signal — surfaced softly as activity, not as a peer. + * - `remoteAttempts` / `endpointAttempts` = another member consuming this + * machine's compute. THIS is the "someone connected to what I'm sharing" + * signal. + */ +export type MeshServingIndicator = { + /** Whether to show anything at all (only while actively sharing). */ + show: boolean; + /** Someone is being served right now. */ + active: boolean; + /** A non-local member is (or has been) consuming this machine's compute. */ + hasRemoteConsumers: boolean; + /** One-line status suitable for the card. */ + label: string; + /** Longer detail for a tooltip / secondary line. */ + detail: string | null; +}; + +function plural(n: number, one: string, many = `${one}s`): string { + return n === 1 ? one : many; +} + +/** + * @param usage latest snapshot from `meshServingUsage`, or null if not fetched + * @param isSharing whether this machine is currently in serve mode (card owns + * this from the toggle model). Usage is only meaningful while + * sharing. + */ +export function deriveServingIndicator( + usage: MeshServingUsage | null, + isSharing: boolean, +): MeshServingIndicator { + const hidden: MeshServingIndicator = { + show: false, + active: false, + hasRemoteConsumers: false, + label: "", + detail: null, + }; + if (!isSharing || !usage) { + return hidden; + } + + const hasRemoteConsumers = + usage.remoteAttempts > 0 || usage.endpointAttempts > 0; + const active = usage.inflight > 0; + + // Remote consumer present (or seen) — the headline case the user asked for. + if (hasRemoteConsumers) { + const remote = usage.remoteAttempts + usage.endpointAttempts; + const label = active + ? `In use now by another member · ${usage.inflight} live` + : `Used by another member · ${remote} ${plural(remote, "request")}`; + const detail = + usage.peers > 0 + ? `${usage.peers} ${plural(usage.peers, "peer")} on the mesh · ${Math.round(usage.tokensPerSecond)} tok/s` + : `${Math.round(usage.tokensPerSecond)} tok/s`; + return { show: true, active, hasRemoteConsumers: true, label, detail }; + } + + // Only local (this machine's own agents) — show softly as activity. + if (active) { + return { + show: true, + active: true, + hasRemoteConsumers: false, + label: `Serving your agent · ${usage.inflight} live`, + detail: `${Math.round(usage.tokensPerSecond)} tok/s`, + }; + } + if (usage.requestsServed > 0) { + return { + show: true, + active: false, + hasRemoteConsumers: false, + label: "Idle · no one using it right now", + detail: `${usage.requestsServed} ${plural(usage.requestsServed, "request")} served this session`, + }; + } + + // Sharing but nothing served yet. + return { + show: true, + active: false, + hasRemoteConsumers: false, + label: "Idle · no one using it yet", + detail: null, + }; +} diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx index aef4351bd..1ca81c43b 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx @@ -29,7 +29,9 @@ import { useMeshDownloadProgress, } from "../hooks/useMeshDownloadProgress"; import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus"; +import { useMeshServingUsage } from "../hooks/useMeshServingUsage"; import { deriveMeshShareToggle } from "../shareToggleState"; +import { deriveServingIndicator } from "../servingUsage"; const MODEL_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.model.v1"; const MAX_VRAM_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.max-vram-gb.v1"; @@ -137,6 +139,10 @@ export function MeshComputeSettingsCard() { // toggling off must never tear down that unrelated consume session. const { isSharing, isConsuming, slotOccupied } = deriveMeshShareToggle(status); + // Host-side "who is using the compute I'm sharing" — only polled while + // actively sharing (serve mode). Read-only; reads the node's own metrics. + const servingUsage = useMeshServingUsage(isSharing); + const servingIndicator = deriveServingIndicator(servingUsage, isSharing); // Any occupying runtime (serve or client, healthy or failed) locks the model // inputs and blocks a fresh start — stop it before reconfiguring. const controlsDisabled = slotOccupied || actionInFlight; @@ -222,6 +228,25 @@ export function MeshComputeSettingsCard() { pendingAction={pendingAction} status={status} /> + {servingIndicator.show ? ( +

+ {servingIndicator.label} + {servingIndicator.detail ? ( + + {" "} + · {servingIndicator.detail} + + ) : null} +

+ ) : null} { return await invokeTauri("mesh_node_status"); } +/** + * Host-side usage of the compute this machine is sharing. The + * local/remote/endpoint attempt split distinguishes this machine's own agents + * (local) from another member consuming this machine's compute (remote/endpoint). + */ +export type MeshServingUsage = { + inflight: number; + peakInflight: number; + requestsServed: number; + tokensServed: number; + tokensPerSecond: number; + localAttempts: number; + remoteAttempts: number; + endpointAttempts: number; + peers: number; +}; + +export async function meshServingUsage(): Promise { + return await invokeTauri("mesh_serving_usage"); +} + export async function meshInstalledModels(): Promise { return await invokeTauri("mesh_installed_models"); } diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 507b4e399..64d40191c 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1029,6 +1029,18 @@ declare global { * so the Share toggle must stay off. Drives the toggle-on regression test. */ nodeMode?: "serve" | "client" | null; + /** Seed host-side serving usage to exercise the "who's using my compute" indicator. */ + servingUsage?: Partial<{ + inflight: number; + peakInflight: number; + requestsServed: number; + tokensServed: number; + tokensPerSecond: number; + localAttempts: number; + remoteAttempts: number; + endpointAttempts: number; + peers: number; + }>; }) => void; __BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: { agentPubkey: string; @@ -2742,12 +2754,37 @@ function resetMockSaveSubscriptions(config: E2eConfig | undefined) { // in a browser. They deliberately do NOT model real admission, real inference, // or real mesh routing — those are proven by the Rust layer-2 tests and the // on-hardware layer-1 example. Do not port any of this into production code. +type MockServingUsage = { + inflight: number; + peakInflight: number; + requestsServed: number; + tokensServed: number; + tokensPerSecond: number; + localAttempts: number; + remoteAttempts: number; + endpointAttempts: number; + peers: number; +}; + +const ZERO_SERVING_USAGE: MockServingUsage = { + inflight: 0, + peakInflight: 0, + requestsServed: 0, + tokensServed: 0, + tokensPerSecond: 0, + localAttempts: 0, + remoteAttempts: 0, + endpointAttempts: 0, + peers: 0, +}; + const mockMeshState: { admitted: boolean; models: Array<{ id: string; name: string | null }>; denyReason: string; nodeState: "off" | "running"; nodeMode: "serve" | "client" | null; + servingUsage: MockServingUsage; } = { admitted: true, models: [ @@ -2756,6 +2793,7 @@ const mockMeshState: { denyReason: "not a relay member", nodeState: "off", nodeMode: null, + servingUsage: { ...ZERO_SERVING_USAGE }, }; function resetMockMesh() { @@ -2766,6 +2804,7 @@ function resetMockMesh() { mockMeshState.denyReason = "not a relay member"; mockMeshState.nodeState = "off"; mockMeshState.nodeMode = null; + mockMeshState.servingUsage = { ...ZERO_SERVING_USAGE }; } let mockPersonas: RawPersona[] = []; let mockTeams: RawTeam[] = []; @@ -9076,6 +9115,11 @@ export function maybeInstallE2eTauriMocks() { mockMeshState.denyReason = mesh.denyReason; if (mesh.nodeState !== undefined) mockMeshState.nodeState = mesh.nodeState; if (mesh.nodeMode !== undefined) mockMeshState.nodeMode = mesh.nodeMode; + if (mesh.servingUsage !== undefined) + mockMeshState.servingUsage = { + ...mockMeshState.servingUsage, + ...mesh.servingUsage, + }; }; let seedTurnSeq = Date.now(); window.__BUZZ_E2E_SEED_ACTIVE_TURNS__ = ({ @@ -9195,6 +9239,8 @@ export function maybeInstallE2eTauriMocks() { return mockMeshState.models; case "mesh_node_status": return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode); + case "mesh_serving_usage": + return mockMeshState.servingUsage; case "mesh_start_node": { const req = ( payload as { request?: { mode?: "serve" | "client" } } | null