From bce957fe61246af61e4f4bde96a576eab827156b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 2 Aug 2026 18:04:25 +0800 Subject: [PATCH] fix(llama-cpp): clarify local model setup --- CHANGELOG.md | 1 + extensions/llama-cpp/index.test.ts | 2 +- extensions/llama-cpp/index.ts | 8 +++---- extensions/llama-cpp/openclaw.plugin.json | 6 ++--- extensions/llama-cpp/src/defaults.ts | 2 +- extensions/llama-cpp/src/setup.test.ts | 10 ++++---- extensions/llama-cpp/src/setup.ts | 7 +++--- ui/src/components/wizard-step-controls.ts | 2 +- ui/src/e2e/model-setup-llamacpp.e2e.test.ts | 16 ++++++------- ui/src/e2e/model-setup-lmstudio.e2e.test.ts | 2 +- ui/src/e2e/model-setup.e2e.test.ts | 8 +++---- .../model-setup/configured-model.test.ts | 5 ++-- .../model-setup/model-setup-page.test.ts | 6 ++--- ui/src/pages/model-setup/view.test.ts | 8 +++---- ui/src/pages/model-setup/wizard-view.ts | 24 +++++++++++++++---- 15 files changed, 63 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f6415911943..53e92a908686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - **Browser extension relay CDP compat:** answer `Target.getBrowserContexts` so Puppeteer-based clients (chrome-devtools-mcp) can drive the paired Chrome without the remote-debugging permission prompt, serve DevTools-style `/json/list` target descriptors, and add `openclaw browser extension cdp` to print the relay endpoint plus auth header for external CDP clients. - **Local model setup:** advertise provider-owned Ollama, llama.cpp, and LM Studio setup choices to Control UI and macOS, retry unavailable LM Studio services in place, and verify the exact prepared model before showing success. +- **llama.cpp setup:** present llama.cpp as an in-Gateway GGUF runtime, use one clear setup action, and state the recommended model download at the confirmation step instead of making it look like a remote connection. - **Control UI first-run setup:** continue verified model setup into Custodian, explain that the web app is ready without a channel, and offer an optional dismissible path to Channels. - **Fish Audio speech:** add hosted S2.1 synthesis with streaming, voice notes, voice discovery, and telephony, plus local Fish S2 Pro reference-voice streaming in native macOS Talk. Thanks @Conan-Scott for the earlier community-plugin implementation. - **Control UI cloud workspace conflicts:** surface staged-ref guidance, bounded conflicted paths, structured transcript events, and sidebar attention for cloud worker results that kept local versions. diff --git a/extensions/llama-cpp/index.test.ts b/extensions/llama-cpp/index.test.ts index 416777893918..fd2af630df84 100644 --- a/extensions/llama-cpp/index.test.ts +++ b/extensions/llama-cpp/index.test.ts @@ -75,7 +75,7 @@ describe("llama.cpp provider plugin", () => { expect(registerProvider).toHaveBeenCalledWith( expect.objectContaining({ id: "llama-cpp", - label: "Local model (llama.cpp)", + label: "llama.cpp", createStreamFn: expect.any(Function), normalizeToolSchemas: expect.any(Function), inspectToolSchemas: expect.any(Function), diff --git a/extensions/llama-cpp/index.ts b/extensions/llama-cpp/index.ts index da88241db062..368bbab132dc 100644 --- a/extensions/llama-cpp/index.ts +++ b/extensions/llama-cpp/index.ts @@ -24,7 +24,7 @@ export default definePluginEntry({ { id: "local", label: LLAMA_CPP_PROVIDER_LABEL, - hint: "In-process local GGUF model (about 5.0 GB download; requires 16 GB RAM)", + hint: "Run one private GGUF model directly inside this Gateway", kind: "custom", appGuidedSetup: { detect: detectLlamaCppSetup, @@ -59,15 +59,15 @@ export default definePluginEntry({ setup: { choiceId: LLAMA_CPP_PROVIDER_ID, choiceLabel: LLAMA_CPP_PROVIDER_LABEL, - choiceHint: "In-process local model (about 5.0 GB download; requires 16 GB RAM)", + choiceHint: "Run one private GGUF model directly inside this Gateway", groupId: LLAMA_CPP_PROVIDER_ID, groupLabel: "Local llama.cpp", groupHint: "No API key required", methodId: "local", }, modelPicker: { - label: "llama.cpp (local GGUF)", - hint: "Run a GGUF model in the OpenClaw process", + label: "llama.cpp", + hint: "Run a GGUF model directly inside OpenClaw", methodId: "local", }, }, diff --git a/extensions/llama-cpp/openclaw.plugin.json b/extensions/llama-cpp/openclaw.plugin.json index 224016d19ca4..fe2ae13753f2 100644 --- a/extensions/llama-cpp/openclaw.plugin.json +++ b/extensions/llama-cpp/openclaw.plugin.json @@ -29,9 +29,9 @@ "method": "local", "choiceId": "llama-cpp", "appGuidedDiscovery": true, - "appGuidedActionLabel": "Review download", - "choiceLabel": "Local model (llama.cpp)", - "choiceHint": "Downloads an approximately 5.0 GB local model; requires 16 GB RAM", + "appGuidedActionLabel": "Set up model", + "choiceLabel": "llama.cpp", + "choiceHint": "Run one private GGUF model directly inside this Gateway", "groupId": "llama-cpp", "groupLabel": "Local llama.cpp", "groupHint": "No API key required" diff --git a/extensions/llama-cpp/src/defaults.ts b/extensions/llama-cpp/src/defaults.ts index 349a269f148a..c43b7e5da99c 100644 --- a/extensions/llama-cpp/src/defaults.ts +++ b/extensions/llama-cpp/src/defaults.ts @@ -6,7 +6,7 @@ import type { } from "openclaw/plugin-sdk/provider-model-shared"; export const LLAMA_CPP_PROVIDER_ID = "llama-cpp"; -export const LLAMA_CPP_PROVIDER_LABEL = "Local model (llama.cpp)"; +export const LLAMA_CPP_PROVIDER_LABEL = "llama.cpp"; const LLAMA_CPP_LOCAL_AUTH_MARKER = "llama-cpp-local"; const LLAMA_CPP_LOCAL_BASE_URL = "local://llama-cpp"; diff --git a/extensions/llama-cpp/src/setup.test.ts b/extensions/llama-cpp/src/setup.test.ts index b8c5a5fcd4d6..7cd0ec3d35d4 100644 --- a/extensions/llama-cpp/src/setup.test.ts +++ b/extensions/llama-cpp/src/setup.test.ts @@ -125,7 +125,7 @@ describe("llama.cpp setup", () => { await expect(detectLlamaCppSetup({ config: configWithCache(), env: {} })).resolves.toEqual({ modelRef: DEFAULT_LLAMA_CPP_MODEL_REF, - detail: "gemma-4-e4b-it-q4_k_m (downloaded)", + detail: "Ready locally", }); expect(nodeLlamaMocks.createModelDownloader).not.toHaveBeenCalled(); expect(nodeLlamaMocks.resolveModelFile).toHaveBeenCalledWith( @@ -157,7 +157,7 @@ describe("llama.cpp setup", () => { await expect(detectLlamaCppSetup({ config, env: {} })).resolves.toEqual({ modelRef: "llama-cpp/custom", - detail: "custom (downloaded)", + detail: "Ready locally", }); expect(nodeLlamaMocks.resolveModelFile).toHaveBeenCalledWith( "hf:org/repo/model.gguf#release", @@ -215,7 +215,7 @@ describe("llama.cpp setup", () => { expect(ctx.prompter.confirm).not.toHaveBeenCalled(); expect(ctx.prompter.note).toHaveBeenCalledWith( - "This machine has 8 GB RAM; the bundled local model needs 16 GB+. Use Ollama/LM Studio with a smaller model, or a cloud provider.", + "This Gateway has 8 GB RAM; the recommended model needs 16 GB+. Use Ollama or LM Studio with a smaller model, configure an existing GGUF, or choose a cloud provider.", "Setup skipped", ); expect(nodeLlamaMocks.createModelDownloader).not.toHaveBeenCalled(); @@ -240,7 +240,9 @@ describe("llama.cpp setup", () => { await expect(runLlamaCppSetup(ctx)).resolves.toEqual({ profiles: [] }); expect(ctx.prompter.confirm).toHaveBeenCalledWith( - expect.objectContaining({ message: expect.stringContaining("about 5.0 GB") }), + expect.objectContaining({ + message: expect.stringContaining("run it directly inside this Gateway"), + }), ); expect(nodeLlamaMocks.createModelDownloader).not.toHaveBeenCalled(); }); diff --git a/extensions/llama-cpp/src/setup.ts b/extensions/llama-cpp/src/setup.ts index f25139196d36..71b65965ac3e 100644 --- a/extensions/llama-cpp/src/setup.ts +++ b/extensions/llama-cpp/src/setup.ts @@ -98,7 +98,7 @@ export async function detectLlamaCppSetup(ctx: ProviderAppGuidedSetupContext) { } return { modelRef: `${LLAMA_CPP_PROVIDER_ID}/${candidate.model.id}`, - detail: `${candidate.model.id} (downloaded)`, + detail: "Ready locally", }; } catch { // Discovery is read-only: a missing model or native module is not a setup error. @@ -148,13 +148,14 @@ export async function runLlamaCppSetup(ctx: ProviderAuthContext): Promise props.onAnswer(answer)} > - ${t(answer ? "common.yes" : "common.no")} + ${answer ? (props.answerLabel ?? t("common.yes")) : t("common.no")} `, )} diff --git a/ui/src/e2e/model-setup-llamacpp.e2e.test.ts b/ui/src/e2e/model-setup-llamacpp.e2e.test.ts index e2b7c05b8c51..69cf7ce6b45f 100644 --- a/ui/src/e2e/model-setup-llamacpp.e2e.test.ts +++ b/ui/src/e2e/model-setup-llamacpp.e2e.test.ts @@ -27,9 +27,9 @@ const prepareOptions = [ { id: "llama-cpp", brandId: "llama-cpp", - label: "Local model (llama.cpp)", - hint: "Download and run a private GGUF model", - actionLabel: "Review download", + label: "llama.cpp", + hint: "Run one private GGUF model directly inside this Gateway", + actionLabel: "Set up model", }, { id: "lmstudio", @@ -106,7 +106,7 @@ describeControlUiE2e("Control UI llama.cpp setup mocked Gateway E2E", () => { id: "llama-cpp-consent", type: "confirm", message: - "Download Gemma 4 E4B IT Q4_K_M (about 5.0 GB) for local llama.cpp inference?", + "OpenClaw will download Gemma 4 E4B IT Q4_K_M (about 5.0 GB) and run it directly inside this Gateway. Continue?", initialValue: false, }, }, @@ -140,7 +140,7 @@ describeControlUiE2e("Control UI llama.cpp setup mocked Gateway E2E", () => { const response = await page.goto(`${server.baseUrl}settings/model-setup`); expect(response?.status()).toBe(200); const llamaCppRow = page.locator('[data-prepare-choice="llama-cpp"]'); - await llamaCppRow.getByRole("button", { name: "Review download" }).waitFor(); + await llamaCppRow.getByRole("button", { name: "Set up model" }).waitFor(); await expect .poll(() => llamaCppRow.locator('[data-provider-icon="llamacpp"]').count()) .toBe(1); @@ -157,11 +157,11 @@ describeControlUiE2e("Control UI llama.cpp setup mocked Gateway E2E", () => { }); } - await llamaCppRow.getByRole("button", { name: "Review download" }).click(); + await llamaCppRow.getByRole("button", { name: "Set up model" }).click(); const start = await gateway.waitForRequest("openclaw.setup.prepare.start"); expect(start.params).toMatchObject({ authChoice: "llama-cpp" }); await page.getByRole("heading", { name: "Set up a local model" }).waitFor(); - await page.getByText("Download Gemma 4 E4B IT Q4_K_M").waitFor(); + await page.getByText("OpenClaw will download Gemma 4 E4B IT Q4_K_M").waitFor(); if (artifactDir) { await page.screenshot({ @@ -185,7 +185,7 @@ describeControlUiE2e("Control UI llama.cpp setup mocked Gateway E2E", () => { }, ], }); - await page.getByRole("button", { name: "Yes" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("heading", { name: "Connection verified" }).waitFor(); await expect .poll(() => page.locator(".model-setup-success").textContent()) diff --git a/ui/src/e2e/model-setup-lmstudio.e2e.test.ts b/ui/src/e2e/model-setup-lmstudio.e2e.test.ts index 51b6c19baba4..5e516e6333f3 100644 --- a/ui/src/e2e/model-setup-lmstudio.e2e.test.ts +++ b/ui/src/e2e/model-setup-lmstudio.e2e.test.ts @@ -188,7 +188,7 @@ describeControlUiE2e("Control UI LM Studio setup mocked Gateway E2E", () => { }, ], }); - await page.getByRole("button", { name: "Yes" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("heading", { name: "Connection verified" }).waitFor(); await expect .poll(() => page.locator(".model-setup-success").textContent()) diff --git a/ui/src/e2e/model-setup.e2e.test.ts b/ui/src/e2e/model-setup.e2e.test.ts index 0b0a7d56fd0c..0becaf22ba4f 100644 --- a/ui/src/e2e/model-setup.e2e.test.ts +++ b/ui/src/e2e/model-setup.e2e.test.ts @@ -27,9 +27,9 @@ const localPrepareOptions = [ { id: "llama-cpp", brandId: "llama-cpp", - label: "Local model (llama.cpp)", - hint: "Download and run a private GGUF model", - actionLabel: "Review download", + label: "llama.cpp", + hint: "Run one private GGUF model directly inside this Gateway", + actionLabel: "Set up model", }, { id: "lmstudio", @@ -447,7 +447,7 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { ], recommendedInstalls: [], }); - await page.getByRole("button", { name: "Yes" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("heading", { name: "Connection verified" }).waitFor(); await expect .poll(() => page.locator(".model-setup-success").textContent()) diff --git a/ui/src/pages/model-setup/configured-model.test.ts b/ui/src/pages/model-setup/configured-model.test.ts index d4850d9e578b..44171ba66b35 100644 --- a/ui/src/pages/model-setup/configured-model.test.ts +++ b/ui/src/pages/model-setup/configured-model.test.ts @@ -53,7 +53,7 @@ describe("renderConfiguredModel", () => { }, { brandId: "llama-cpp", - detail: "gemma-4-e4b-it-q4_k_m (downloaded)", + detail: "Ready locally", kind: "provider-auto:llama-cpp", label: "llama.cpp", modelRef: "llama-cpp/gemma-4-e4b-it-q4_k_m", @@ -86,7 +86,8 @@ describe("renderConfiguredModel", () => { }; const { container, onVerify } = mount(result); - expect(text(container)).toContain(`Selected model ${fixture.label} ${fixture.detail}`); + expect(text(container)).toContain(`Selected model ${fixture.label}`); + expect(text(container)).toContain(fixture.detail); expect(text(container)).toContain(`${fixture.label} isn’t responding.`); expect(text(container)).not.toContain("Change connection"); const button = container.querySelector("button"); diff --git a/ui/src/pages/model-setup/model-setup-page.test.ts b/ui/src/pages/model-setup/model-setup-page.test.ts index 1cf09bc15ed0..f2a47aa795cb 100644 --- a/ui/src/pages/model-setup/model-setup-page.test.ts +++ b/ui/src/pages/model-setup/model-setup-page.test.ts @@ -36,8 +36,8 @@ const detection: SystemAgentSetupDetectResult = { { id: "llama-cpp", brandId: "llama-cpp", - label: "Local model (llama.cpp)", - hint: "Download and run a private GGUF model", + label: "llama.cpp", + hint: "Run one private GGUF model directly inside this Gateway", }, { id: "lmstudio", @@ -400,7 +400,7 @@ describe("ModelSetupPage catalog icons", () => { await vi.waitFor(() => { expect(page.textContent).toContain( - "Local model (llama.cpp) did not expose a usable local model. Review the setup result, then retry.", + "llama.cpp did not expose a usable local model. Review the setup result, then retry.", ); }); expect(page.textContent).not.toContain("llama-cpp/persisted-before-verification"); diff --git a/ui/src/pages/model-setup/view.test.ts b/ui/src/pages/model-setup/view.test.ts index 20672e121ca2..4d6aa20eeeb4 100644 --- a/ui/src/pages/model-setup/view.test.ts +++ b/ui/src/pages/model-setup/view.test.ts @@ -85,9 +85,9 @@ const detected: SystemAgentSetupDetectResult = { { id: "llama-cpp", brandId: "llama-cpp", - label: "Local model (llama.cpp)", - hint: "Download and run a private GGUF model", - actionLabel: "Review download", + label: "llama.cpp", + hint: "Run one private GGUF model directly inside this Gateway", + actionLabel: "Set up model", }, ], recommendedInstalls: [ @@ -502,7 +502,7 @@ describe("renderModelSetup", () => { '[data-prepare-choice="llama-cpp"] button', ); expect(ollama?.textContent).toContain("Choose connection"); - expect(llamaCpp?.textContent).toContain("Review download"); + expect(llamaCpp?.textContent).toContain("Set up model"); expect( container.querySelector('[data-prepare-choice="lmstudio"] button') ?.textContent, diff --git a/ui/src/pages/model-setup/wizard-view.ts b/ui/src/pages/model-setup/wizard-view.ts index 9dce97e308b5..86479c183458 100644 --- a/ui/src/pages/model-setup/wizard-view.ts +++ b/ui/src/pages/model-setup/wizard-view.ts @@ -69,6 +69,10 @@ export function renderModelSetupWizard(props: WizardViewProps): TemplateResult | value: props.value, busy: props.state.busy, inputId: WIZARD_TEXT_INPUT_ID, + answerLabel: + props.mode === "prepare" && props.state.step.type === "confirm" + ? t("modelSetup.wizard.continue") + : undefined, onValueChange: props.onValueChange, onAnswer: props.onAnswer, })} @@ -77,11 +81,21 @@ export function renderModelSetupWizard(props: WizardViewProps): TemplateResult | : nothing} `} - + ${props.mode === "prepare" && + props.state.phase === "step" && + props.state.step.type === "confirm" + ? nothing + : html` + + `} `;