Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ The wizard checks `ollama --version` and `/api/version` on port `11434` independ
If NemoClaw detects an installed CLI or local running daemon but cannot read its version, onboarding uses the upgrade path instead of reusing it.

On macOS, the wizard uses `brew upgrade ollama` for the platform upgrade path.
On Linux, the wizard uses the official `https://ollama.com/install.sh` path.
On Linux, the wizard uses the official `https://ollama.com/install.sh` path and asks it for `0.32.9` by name, because the version the installer calls latest is below the minimum on some hosts.
Linux upgrades use the sudo-driven system path because a user-local fallback would leave an existing system daemon serving the stale binary.
If sudo is unavailable in a non-interactive run, rerun interactively or upgrade Ollama manually.

After an upgrade, NemoClaw probes the running daemon again.
If the version remains below the minimum or cannot be read, interactive onboarding returns to provider selection, and non-interactive onboarding exits.
An upgrade also needs sudo to restart the service onto the new binary, so it does not accept an already-loopback-only daemon as a reason to skip that step.
A fresh install takes the latest version.

After an upgrade, NemoClaw probes the running daemon and the installed binary again.
`ollama --version` reports the version of the daemon it can reach, so NemoClaw reads the binary's own version from the client-version line that the command prints when the two differ.
If the daemon remains below the minimum or cannot be read, interactive onboarding returns to provider selection, and non-interactive onboarding exits.
The failure names which side is stale.
A binary at or above the minimum means the service still serves the old one and needs a restart, while a binary below it means the installer did not deliver the required version on that host.
When neither side can be read, the failure asks you to check that Ollama is installed and running before you retry.
Fresh installs skip this second probe because the bundled installers provide a daemon at or above the minimum.

The version gate does not apply to Windows-host Ollama reached from Docker Desktop through `host.docker.internal`.
Expand Down
5 changes: 5 additions & 0 deletions src/lib/inference/ollama-version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ describe("Ollama version detection", () => {
expect(getInstalledOllamaVersion(capture)).toBe("0.6.2");
});

it("prefers the client version line over the daemon version the CLI reports (#9276)", () => {
const capture = () => "ollama version is 0.23.4\nWarning: client version is 0.32.9";
expect(getInstalledOllamaVersion(capture)).toBe("0.32.9");
});

it("returns null when ollama --version produces no output", () => {
const capture = () => "";
expect(getInstalledOllamaVersion(capture)).toBeNull();
Expand Down
4 changes: 4 additions & 0 deletions src/lib/inference/ollama-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ export type OllamaVersionRunCapture = (
*/
export const MIN_OLLAMA_VERSION = "0.32.9";

const OLLAMA_CLIENT_VERSION_LINE = /client version is\s+(\d+\.\d+\.\d+)/i;

export function getInstalledOllamaVersion(runCaptureImpl?: OllamaVersionRunCapture): string | null {
const capture = runCaptureImpl ?? runCapture;
const out = capture(["ollama", "--version"], { ignoreError: true });
if (!out) return null;
const clientVersion = out.match(OLLAMA_CLIENT_VERSION_LINE);
if (clientVersion) return clientVersion[1];
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
return match ? match[0] : null;
}
Expand Down
43 changes: 43 additions & 0 deletions src/lib/onboard/install-ollama-linux-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { MIN_OLLAMA_VERSION } from "../inference/ollama-version";
import {
decideInstallOllamaLinuxMode,
type InstallOllamaLinuxOptions,
Expand Down Expand Up @@ -213,6 +214,48 @@ describe("installOllamaOnLinux (upgrade recovery)", () => {
expect(sleepSecondsImpl).toHaveBeenCalled();
});

it("asks the official installer for the required version on an upgrade (#9276)", () => {
const runShellImpl = vi
.fn()
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
const opts = makeOpts({
modeOverride: "system",
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
runShellImpl,
isUpgrade: true,
});
expect(installOllamaOnLinux(opts).ok).toBe(true);
const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh");
expect(installer).toContain(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh`);
});

it("leaves a fresh install unpinned so it takes the latest Ollama", () => {
const runShellImpl = vi
.fn()
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
const opts = makeOpts({
modeOverride: "system",
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
runShellImpl,
});
expect(installOllamaOnLinux(opts).ok).toBe(true);
const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh");
expect(installer).toContain("| sh");
expect(installer).not.toContain("OLLAMA_VERSION=");
});

it("tells the systemd override that this run replaced the binary (#9276)", () => {
const ensureOverride = vi.fn().mockReturnValue("ready");
const opts = makeOpts({
modeOverride: "system",
runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"),
ensureManagedOllamaLoopbackSystemdOverrideImpl: ensureOverride,
isUpgrade: true,
});
expect(installOllamaOnLinux(opts).ok).toBe(true);
expect(ensureOverride).toHaveBeenCalledWith(expect.objectContaining({ isUpgrade: true }));
});

it("re-probes loopback fresh instead of trusting the cached findReachableOllamaHost result", () => {
const runShellImpl = vi
.fn()
Expand Down
12 changes: 10 additions & 2 deletions src/lib/onboard/install-ollama-linux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW,
resolveOllamaContextWindowFloor,
} from "../inference/ollama-runtime-context";
import { MIN_OLLAMA_VERSION } from "../inference/ollama-version";
import { cliName } from "./branding";
import {
OLLAMA_PORT,
Expand Down Expand Up @@ -115,7 +116,9 @@ function detectJetpackVariant(opts: InstallOllamaLinuxOptions): "jetpack5" | "je
/**
* Run the official `https://ollama.com/install.sh`. Sudo-bound. Configures
* the systemd `ollama.service`, creates the `ollama` system user, and
* installs CUDA drivers when applicable.
* installs CUDA drivers when applicable. An upgrade asks the installer for
* `MIN_OLLAMA_VERSION` by name; a fresh install has no floor to satisfy and
* takes latest.
*/
function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void {
const log = opts.log ?? ((m: string) => console.log(m));
Expand All @@ -125,7 +128,11 @@ function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void {
" The Ollama installer creates a system user, a systemd service, and writes to /usr/local. " +
"It uses sudo, may ask for your password, and can take a few minutes; installer output will stream below.",
);
runShellImpl("set -o pipefail; curl -fsSL https://ollama.com/install.sh | sh", {
const versionPin = opts.isUpgrade ? `OLLAMA_VERSION=${MIN_OLLAMA_VERSION} ` : "";
if (versionPin) {
log(` Requesting Ollama ${MIN_OLLAMA_VERSION} from the installer.`);
}
runShellImpl(`set -o pipefail; curl -fsSL https://ollama.com/install.sh | ${versionPin}sh`, {
stdio: "inherit",
});
}
Expand Down Expand Up @@ -322,6 +329,7 @@ function installOllamaSystem(opts: InstallOllamaLinuxOptions): InstallOllamaLinu
const overrideState: OllamaLoopbackSystemdOverrideState = ensureOverrideImpl({
isNonInteractive: opts.isNonInteractive,
contextWindowFloor: opts.contextWindowFloor,
isUpgrade: opts.isUpgrade,
});
if (overrideState === "failed") {
errorLog(" Ollama systemd restart did not recover after applying the loopback override.");
Expand Down
16 changes: 16 additions & 0 deletions src/lib/onboard/ollama-install-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,22 @@ describe("resolveOllamaInstallMenuEntry", () => {
expect(result.detectedBinaryVersion).toBe("0.32.9");
expect(result.message).toContain("0.6.2");
expect(result.message).toContain(MIN_OLLAMA_VERSION);
expect(result.message).toContain("systemctl restart ollama");
});

it("points at the installer, not a restart, when the binary is still below the floor (#9276)", () => {
const capture = (cmd: readonly string[]) => {
const joined = cmd.join(" ");
if (joined.includes("/api/version")) return '{"version":"0.23.4"}';
if (joined.includes("ollama --version")) return "ollama version is 0.23.4";
return "";
};
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
expect(result.ok).toBe(false);
expect(result.detectedBinaryVersion).toBe("0.23.4");
expect(result.message).toContain(`did not deliver ${MIN_OLLAMA_VERSION} on this host`);
expect(result.message).toContain(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh`);
expect(result.message).not.toContain("systemctl restart ollama");
});

it("rejects the upgrade when the daemon is unreachable", () => {
Expand Down
24 changes: 21 additions & 3 deletions src/lib/onboard/ollama-install-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,30 @@ export function assertOllamaUpgradeApplied(
}
const daemonLabel = detectedDaemonVersion ?? "unreachable";
const binaryLabel = detectedBinaryVersion ?? "unknown";
const state = `running daemon reports ${daemonLabel} (binary: ${binaryLabel}), need ≥ ${MIN_OLLAMA_VERSION}`;
return {
ok: false,
detectedDaemonVersion,
detectedBinaryVersion,
message:
`Ollama upgrade did not take effect — running daemon reports ${daemonLabel} (binary: ${binaryLabel}), need ≥ ${MIN_OLLAMA_VERSION}. ` +
"Restart the system daemon and rerun, or upgrade Ollama manually (https://ollama.com/download).",
message: `Ollama upgrade did not take effect — ${state}. ${resolveUpgradeRemedy(detectedBinaryVersion)}`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
};
}

function resolveUpgradeRemedy(detectedBinaryVersion: string | null): string {
if (isOllamaVersionAtLeast(detectedBinaryVersion, MIN_OLLAMA_VERSION)) {
return (
"The installed binary meets the minimum, so the service is still serving the old one. " +
"Restart it with 'sudo systemctl restart ollama' and rerun."
);
}
if (!detectedBinaryVersion) {
return (
"Neither the daemon nor the binary could be read. Check that Ollama is installed and running, " +
"then rerun, or install it manually (https://ollama.com/download)."
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
return (
`The installer did not deliver ${MIN_OLLAMA_VERSION} on this host. Install it directly with ` +
`'curl -fsSL https://ollama.com/install.sh | OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh' and rerun.`
);
}
30 changes: 30 additions & 0 deletions src/lib/onboard/ollama-systemd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,36 @@ describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", ()
}
});

it("refuses the loopback-only shortcut on an upgrade because the service must restart onto the new binary (#9276)", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const loopbackOnly = vi.fn(() => true);
try {
expect(() =>
ensureOllamaLoopbackSystemdOverride({
platformImpl: () => "linux",
hasOllamaSystemdUnitImpl: () => true,
isNonInteractive: () => true,
hasPasswordlessSudoImpl: () => false,
isOllamaLoopbackOnlyImpl: loopbackOnly,
isUpgrade: true,
}),
).toThrow("exit 1");
expect(loopbackOnly).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
expect.stringContaining("cannot be restarted onto the newly installed binary"),
);
expect(error).toHaveBeenCalledWith(
expect.stringContaining("keep serving the old version"),
);
} finally {
exit.mockRestore();
error.mockRestore();
}
});

it("fails before override commands when sudo is unavailable and loopback-only binding is unverified", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
Expand Down
17 changes: 17 additions & 0 deletions src/lib/onboard/ollama-systemd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type OllamaLoopbackSystemdOverrideOptions = {
enableService?: boolean;
/** Minimum daemon context length to preserve or apply in the systemd override. */
contextWindowFloor?: number;
/**
* Set when the caller has just replaced the Ollama binary. The service must
* then be restarted onto it, so an already-loopback-only listener is not
* evidence that this step can be skipped.
*/
isUpgrade?: boolean;
detectNvidiaPlatformImpl?: () => string;
hasOllamaCudaV13LibraryImpl?: () => boolean;
/**
Expand Down Expand Up @@ -205,6 +211,17 @@ export function ensureOllamaLoopbackSystemdOverride(
const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)());
const hasPasswordlessSudo = options.hasPasswordlessSudoImpl ?? defaultHasPasswordlessSudo;
if (shouldSkipOllamaLoopbackForMissingSudo(sudoPrefix, hasPasswordlessSudo)) {
if (options.isUpgrade) {
console.error(
" Passwordless sudo is not available, so the Ollama service cannot be restarted onto the " +
"newly installed binary.",
);
console.error(
` The running daemon would keep serving the old version. Set ${NON_INTERACTIVE_SUDO_MODE_ENV}=prompt ` +
"with a terminal, configure passwordless sudo, or run 'sudo systemctl restart ollama' and rerun onboarding.",
);
process.exit(1);
}
const loopbackOnly =
options.isOllamaLoopbackOnlyImpl?.() ?? isActiveOllamaListenerLoopbackOnly();
if (loopbackOnly) {
Expand Down
Loading
Loading