Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,21 @@ 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 when the installed binary is stale, because the version the installer calls latest is below the minimum on some hosts.
If the installed binary is already at or above the minimum and only the daemon is stale, the wizard restarts the daemon without running the installer or replacing the newer binary.
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.
Both versions must be readable and at or above `0.32.9` before onboarding accepts the upgrade.
If either version is below the minimum or cannot be read, interactive onboarding returns to provider selection, and non-interactive onboarding exits.
The failure identifies each stale or unreadable version.
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 only the binary cannot be read, the failure asks you to verify the installed Ollama binary before you retry.
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
2 changes: 1 addition & 1 deletion src/lib/onboard/__test-helpers__/setup-nim-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function makeHostState(
vllmProfile: null,
hasVllmImage: false,
vllmEntries: [],
ollamaInstallMenu: { entry: null, hasUpgradableOllama: false },
ollamaInstallMenu: { entry: null, hasUpgradableOllama: false, binaryNeedsUpgrade: false },
gpuNimCapable: false,
...overrides,
};
Expand Down
63 changes: 63 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,68 @@ 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("restarts the daemon for an already-current binary without running the pinned installer", () => {
const runShellImpl = vi
.fn()
.mockReturnValue({ status: 0, stdout: "", stderr: "", error: null });
const ensureOverride = vi.fn().mockReturnValue("ready");
const log = vi.fn();
const opts = makeOpts({
modeOverride: "system",
runShellImpl,
ensureManagedOllamaLoopbackSystemdOverrideImpl: ensureOverride,
isUpgrade: true,
restartOnly: true,
log,
});
expect(installOllamaOnLinux(opts).ok).toBe(true);
expect(findRunShellCall(runShellImpl, "ollama.com/install.sh")).toBeUndefined();
expect(ensureOverride).toHaveBeenCalledWith(expect.objectContaining({ isUpgrade: true }));
expect(log).toHaveBeenCalledWith(expect.stringContaining("without replacing the binary"));
});

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
24 changes: 20 additions & 4 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 @@ -55,6 +56,8 @@ export type InstallOllamaLinuxResult = {
};

export type InstallOllamaLinuxOptions = InstallOllamaLinuxModeOptions & {
/** Restart the daemon for an already-current binary without running the pinned installer. */
restartOnly?: boolean;
/** Test seam: override `os.homedir()`. */
homedir?: () => string;
/** Test seam: override `process.arch`. */
Expand Down Expand Up @@ -115,7 +118,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 that replaces a stale
* binary 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 +130,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 @@ -316,12 +325,19 @@ function installOllamaSystem(opts: InstallOllamaLinuxOptions): InstallOllamaLinu
opts.ensureManagedOllamaLoopbackSystemdOverrideImpl ??
ensureManagedOllamaLoopbackSystemdOverride;

runOfficialInstallScript(opts);
sleepSecondsImpl(2);
if (opts.restartOnly) {
log(
` Installed Ollama already meets ${MIN_OLLAMA_VERSION}; restarting its daemon without replacing the binary.`,
);
} else {
runOfficialInstallScript(opts);
sleepSecondsImpl(2);
}

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
86 changes: 73 additions & 13 deletions src/lib/onboard/ollama-install-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ import {

const LINUX_NON_WSL = { platform: "linux" as const, isWsl: false };

function captureOllamaVersions(
daemonVersion: string | null,
binaryVersion: string | null,
): (command: readonly string[]) => string {
const responses = new Map<string, string>([
[
JSON.stringify([
"curl",
"-sf",
"--connect-timeout",
"2",
"--max-time",
"5",
"http://127.0.0.1:11434/api/version",
]),
daemonVersion ? JSON.stringify({ version: daemonVersion }) : "",
],
[
JSON.stringify(["ollama", "--version"]),
binaryVersion ? `ollama version is ${binaryVersion}` : "",
],
]);
return (command) => responses.get(JSON.stringify(command)) ?? "";
}

describe("resolveRunningOllamaMenuEntry", () => {
it("labels unsupported Windows-host Ollama without suggesting it", () => {
const entry = resolveRunningOllamaMenuEntry({
Expand Down Expand Up @@ -80,6 +105,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
...LINUX_NON_WSL,
});
expect(result.hasUpgradableOllama).toBe(true);
expect(result.binaryNeedsUpgrade).toBe(true);
expect(result.entry?.key).toBe("install-ollama");
expect(result.entry?.label).toBe(
`Upgrade Ollama (Linux) — upgrade installed binary 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
Expand Down Expand Up @@ -112,12 +138,31 @@ describe("resolveOllamaInstallMenuEntry", () => {
...LINUX_NON_WSL,
});
expect(result.hasUpgradableOllama).toBe(true);
expect(result.binaryNeedsUpgrade).toBe(false);
// Stale source is the daemon; suffix names "running daemon" with that version.
expect(result.entry?.label).toBe(
`Upgrade Ollama (Linux) — upgrade running daemon 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
);
});

it("requires a binary install when a stale daemon is reachable without a local binary", () => {
const result = resolveOllamaInstallMenuEntry({
hasOllama: false,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
ollamaHost: "127.0.0.1",
installedOllamaVersion: null,
runningOllamaVersion: "0.6.2",
...LINUX_NON_WSL,
});
expect(result.hasUpgradableOllama).toBe(true);
expect(result.binaryNeedsUpgrade).toBe(true);
expect(result.entry?.label).toBe(
`Upgrade Ollama (Linux) — upgrade running daemon 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
);
});

it("offers an upgrade entry when the binary is stale even though the daemon is fresh", () => {
const result = resolveOllamaInstallMenuEntry({
hasOllama: true,
Expand All @@ -130,6 +175,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
...LINUX_NON_WSL,
});
expect(result.hasUpgradableOllama).toBe(true);
expect(result.binaryNeedsUpgrade).toBe(true);
// Stale source is the binary; suffix names "installed binary" with that version.
expect(result.entry?.label).toBe(
`Upgrade Ollama (Linux) — upgrade installed binary 0.6.2 to ≥ ${MIN_OLLAMA_VERSION}`,
Expand Down Expand Up @@ -243,32 +289,45 @@ describe("resolveOllamaInstallMenuEntry", () => {
expect(result.ok).toBe(true);
});

it("accepts the upgrade when the running daemon reports a fresh version", () => {
const capture = (cmd: readonly string[]) => {
const joined = cmd.join(" ");
if (joined.includes("/api/version")) return '{"version":"0.32.9"}';
if (joined.includes("ollama --version")) return "ollama version is 0.32.9";
return "";
};
it("accepts the upgrade when the daemon and installed binary meet the minimum", () => {
const capture = captureOllamaVersions("0.32.9", "0.32.9");
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
expect(result.ok).toBe(true);
expect(result.detectedDaemonVersion).toBe("0.32.9");
expect(result.detectedBinaryVersion).toBe("0.32.9");
});

it("rejects the upgrade when the daemon still serves the stale version even though the binary is fresh", () => {
const capture = (cmd: readonly string[]) => {
const joined = cmd.join(" ");
if (joined.includes("/api/version")) return '{"version":"0.6.2"}';
if (joined.includes("ollama --version")) return "ollama version is 0.32.9";
return "";
};
const capture = captureOllamaVersions("0.6.2", "0.32.9");
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
expect(result.ok).toBe(false);
expect(result.detectedDaemonVersion).toBe("0.6.2");
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("rejects a stale binary when the running daemon meets the minimum (#9276)", () => {
const capture = captureOllamaVersions("0.32.9", "0.23.4");
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
expect(result.ok).toBe(false);
expect(result.detectedDaemonVersion).toBe("0.32.9");
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("reports a known daemon when the installed binary version is unavailable (#9276)", () => {
const capture = captureOllamaVersions("0.23.4", null);
const result = assertOllamaUpgradeApplied({ hasUpgradableOllama: true }, capture);
expect(result.ok).toBe(false);
expect(result.detectedDaemonVersion).toBe("0.23.4");
expect(result.detectedBinaryVersion).toBeNull();
expect(result.message).toContain("running daemon reports 0.23.4 (binary: unknown)");
expect(result.message).toContain("installed binary version could not be read");
expect(result.message).not.toContain("Neither the daemon nor the binary could be read");
});

it("rejects the upgrade when the daemon is unreachable", () => {
Expand All @@ -277,6 +336,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
expect(result.ok).toBe(false);
expect(result.detectedDaemonVersion).toBeNull();
expect(result.message).toContain("unreachable");
expect(result.message).toContain("Neither the daemon nor the binary could be read");
});

it("does not return an entry on unsupported platforms", () => {
Expand Down
Loading
Loading