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
122 changes: 61 additions & 61 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,15 @@ const {
getOllamaModelOptions,
getOllamaWarmupCommand,
OLLAMA_HOST_DOCKER_INTERNAL,
validateOllamaPortConfiguration,
validateOllamaModel,
validateLocalProvider,
} = localInference;
const { resolveOllamaInstallMenuEntry, assertOllamaUpgradeApplied } = require(
"./onboard/ollama-install-menu",
);
const {
checkOllamaPortsOrWarn,
resolveOllamaInstallMenuEntry,
resolveRunningOllamaMenuEntry,
assertOllamaUpgradeApplied,
} = require("./onboard/ollama-install-menu");
const {
ensureOllamaAuthProxy,
getOllamaProxyToken,
Expand Down Expand Up @@ -288,7 +290,9 @@ const platformUtils: typeof import("./platform") = require("./platform");
const { isWsl, shouldPatchCoredns } = platformUtils;
const {
getContainerRuntime,
getWindowsHostOllamaDockerRequirement,
repairLocalInferenceSystemdOverrideOrExit,
rejectUnsupportedWindowsHostOllama,
shouldFrontOllamaWithProxy,
}: typeof import("./onboard/local-inference-topology") = require("./onboard/local-inference-topology");
const { resolveOpenshell } = require("./adapters/openshell/resolve");
Expand Down Expand Up @@ -3994,6 +3998,7 @@ async function setupNim(
const hasOllama = hostCommandExists("ollama");
const ollamaHost = findReachableOllamaHost();
const ollamaRunning = ollamaHost !== null;
const isWindowsHostOllama = ollamaHost === OLLAMA_HOST_DOCKER_INTERNAL;
const vllmRunning = !!runCapture(
["curl", "-sf", ...localProbeCurlArgs, `http://127.0.0.1:${VLLM_PORT}/v1/models`],
{ ignoreError: true },
Expand All @@ -4007,6 +4012,9 @@ async function setupNim(
vllmProfile &&
docker.dockerCapture(["images", "-q", vllmProfile.image], { ignoreError: true }).trim()
);
const windowsHostOllamaDockerRequirement = getWindowsHostOllamaDockerRequirement(
isWsl() ? getContainerRuntime() : null,
);
// Probed even when WSL has its own Ollama: users may prefer the Windows
// instance for GPU access and a unified model cache. See
// src/lib/onboard/windows-host-ollama.ts for process/path fallback details.
Expand All @@ -4019,7 +4027,7 @@ async function setupNim(
// on 127.0.0.1, Windows-host may also be running on 0.0.0.0 and we want
// to offer a "switch" without restarting anything.
let windowsOllamaReachable = false;
if (isWsl() && ollamaHost !== "host.docker.internal") {
if (isWsl() && !isWindowsHostOllama) {
windowsOllamaReachable = !!runCapture(
["curl", "-sf", ...localProbeCurlArgs, `http://host.docker.internal:${OLLAMA_PORT}/api/tags`],
{ ignoreError: true },
Expand Down Expand Up @@ -4052,26 +4060,17 @@ async function setupNim(
options.push({ key: "anthropic", label: "Anthropic" });
options.push({ key: "anthropicCompatible", label: "Other Anthropic-compatible endpoint" });
options.push({ key: "gemini", label: "Google Gemini" });
if (hasOllama || ollamaRunning) {
let hostDisplay: string;
if (ollamaHost === "host.docker.internal") {
hostDisplay = `Windows host:${OLLAMA_PORT}`;
} else if (isWsl()) {
hostDisplay = `WSL:${OLLAMA_PORT}`;
} else {
hostDisplay = `localhost:${OLLAMA_PORT}`;
}
// On WSL the Windows-host entry (Use/Restart/Start, or Install) always
// carries the suggestion instead, since Windows-host is preferred.
const wslOllamaSuggested =
ollamaRunning && (ollamaHost === "host.docker.internal" || !isWsl());
options.push({
key: "ollama",
label:
`Local Ollama (${hostDisplay})${ollamaRunning ? " — running" : ""}` +
(wslOllamaSuggested ? " (suggested)" : ""),
});
}
const runningOllamaMenu = resolveRunningOllamaMenuEntry({
hasOllama,
ollamaRunning,
ollamaHost,
isWsl: isWsl(),
ollamaPort: OLLAMA_PORT,
windowsHostLabelSuffix: windowsHostOllamaDockerRequirement.supported
? ""
: windowsHostOllamaDockerRequirement.labelSuffix,
});
if (runningOllamaMenu) options.push(runningOllamaMenu);
if (EXPERIMENTAL && gpu && gpu.nimCapable) {
options.push({ key: "nim-local", label: "Local NVIDIA NIM [experimental]" });
}
Expand All @@ -4086,24 +4085,22 @@ async function setupNim(
);
// Skipped when Windows-host already won the cache: the running entry
// above already covers that case.
if (hasWindowsOllama && ollamaHost !== "host.docker.internal") {
let windowsOllamaLabel: string;
if (windowsOllamaReachable) {
windowsOllamaLabel = "Use Ollama on Windows host - running (suggested)";
} else if (winOllamaLoopbackOnly) {
windowsOllamaLabel = "Restart Ollama on Windows host with 0.0.0.0 binding (suggested)";
} else {
windowsOllamaLabel = "Start Ollama on Windows host (suggested)";
}
options.push({ key: "start-windows-ollama", label: windowsOllamaLabel });
if (hasWindowsOllama && !isWindowsHostOllama) {
options.push({
key: "start-windows-ollama",
label: windowsHostOllamaDockerRequirement.startLabel({
reachable: windowsOllamaReachable,
loopbackOnly: winOllamaLoopbackOnly,
}),
});
}
// On WSL, always offer to install Ollama on the Windows host when not
// already installed, regardless of WSL Ollama state — users may prefer the
// Windows-host instance (GPU access) even with WSL Ollama running.
if (isWsl() && !hasWindowsOllama) {
options.push({
key: "install-windows-ollama",
label: "Install Ollama on Windows host (recommended)",
label: windowsHostOllamaDockerRequirement.installLabel,
});
}
const ollamaInstallMenu = resolveOllamaInstallMenuEntry({
Expand All @@ -4127,18 +4124,14 @@ async function setupNim(
options.push({ key: providerKey, label: remoteConfig.label });
}

function checkOllamaPortsOrWarn(): boolean {
const portValidation = validateOllamaPortConfiguration();
if (!portValidation.ok) {
console.error(` ${portValidation.message}`);
if (isNonInteractive()) {
process.exit(1);
}
console.log(" Choose a different local inference provider or fix the port settings.");
console.log("");
return false;
}
return true;
function rejectWindowsHostOllama(providerKey: string, windowsHostSelected: boolean): boolean {
return rejectUnsupportedWindowsHostOllama(
windowsHostOllamaDockerRequirement,
providerKey,
windowsHostSelected,
isNonInteractive,
abortNonInteractive,
);
}

if (options.length > 1) {
Expand All @@ -4165,11 +4158,7 @@ async function setupNim(
// (so the menu's "ollama" key points there), the availability
// check below would pass and silently swap the daemon. Detect
// and fail-loud with a hint.
if (
isWsl() &&
recordedProvider === "ollama-local" &&
ollamaHost === OLLAMA_HOST_DOCKER_INTERNAL
) {
if (isWsl() && recordedProvider === "ollama-local" && isWindowsHostOllama) {
console.error(
` Recorded provider '${recordedProvider}' (WSL Ollama) is not available in this environment.`,
);
Expand Down Expand Up @@ -4215,8 +4204,16 @@ async function setupNim(
}
selected = options.find((o) => o.key === providerKey);
if (!selected) {
if (
(providerKey === "start-windows-ollama" ||
providerKey === "install-windows-ollama") &&
rejectWindowsHostOllama(providerKey, isWindowsHostOllama)
) {
process.exit(1);
}
selected = resolveProviderKeyFallback(options, providerKey, {
isWindowsHostOllama: ollamaHost === OLLAMA_HOST_DOCKER_INTERNAL,
canUseWindowsHostOllama:
isWindowsHostOllama && windowsHostOllamaDockerRequirement.supported,
});
if (!selected) {
if (providerKey === "hermesProvider" && !hermesProviderAvailable) {
Expand Down Expand Up @@ -4848,7 +4845,10 @@ async function setupNim(
}
break;
} else if (selected.key === "ollama") {
if (!checkOllamaPortsOrWarn()) continue selectionLoop;
if (rejectWindowsHostOllama(selected.key, isWindowsHostOllama)) {
continue selectionLoop;
}
if (!checkOllamaPortsOrWarn({ isNonInteractive })) continue selectionLoop;
let ollamaReady = ollamaRunning;
const overrideState = ensureOllamaLoopbackSystemdOverride({ isNonInteractive });
if (overrideState === "ready") {
Expand Down Expand Up @@ -4900,11 +4900,11 @@ async function setupNim(
preferredInferenceApi = "openai-completions";
}
break;
} else if (
selected.key === "start-windows-ollama" ||
selected.key === "install-windows-ollama"
) {
if (!checkOllamaPortsOrWarn()) continue selectionLoop;
} else if (["start-windows-ollama", "install-windows-ollama"].includes(selected.key)) {
if (rejectWindowsHostOllama(selected.key, true)) {
continue selectionLoop;
}
if (!checkOllamaPortsOrWarn({ isNonInteractive })) continue selectionLoop;
const isInstall = selected.key === "install-windows-ollama";
const isSwitch = !isInstall && windowsOllamaReachable;
const isRestart = !isInstall && !isSwitch && winOllamaLoopbackOnly;
Expand Down Expand Up @@ -4987,7 +4987,7 @@ async function setupNim(
}
break;
} else if (selected.key === "install-ollama") {
if (!checkOllamaPortsOrWarn()) continue selectionLoop;
if (!checkOllamaPortsOrWarn({ isNonInteractive })) continue selectionLoop;
const isUpgrade = ollamaInstallMenu.hasUpgradableOllama;
const installResult = process.platform === "darwin"
? installOllamaOnMacOS({ isNonInteractive, isUpgrade })
Expand Down
124 changes: 124 additions & 0 deletions src/lib/onboard/local-inference-topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,130 @@ export function getContainerRuntime(): ContainerRuntime {
);
}

function describeContainerRuntime(runtime: ContainerRuntime): string {
switch (runtime) {
case "docker-desktop":
return "Docker Desktop";
case "docker":
return "native Docker Engine";
case "podman":
return "Podman";
case "colima":
return "Colima";
case "unknown":
return "unknown container runtime";
default:
const _exhaustive: never = runtime;
return _exhaustive;
}
}

type WindowsHostOllamaStartLabel = (opts: {
reachable: boolean;
loopbackOnly: boolean;
}) => string;

type WindowsHostOllamaReject = (
providerKey: string,
isNonInteractive: () => boolean,
abortNonInteractive: (reason: string, hint?: string) => never,
) => boolean;

export type WindowsHostOllamaDockerRequirement =
| {
supported: true;
detectedRuntime: string;
installLabel: string;
startLabel: WindowsHostOllamaStartLabel;
}
| {
supported: false;
detectedRuntime: string;
labelSuffix: string;
installLabel: string;
startLabel: WindowsHostOllamaStartLabel;
reason: string;
hint: string;
reject: WindowsHostOllamaReject;
};

export function getWindowsHostOllamaDockerRequirement(
runtime: ContainerRuntime | null,
): WindowsHostOllamaDockerRequirement {
if (runtime === null) {
return {
supported: true,
detectedRuntime: "not applicable",
installLabel: "Install Ollama on Windows host (recommended)",
startLabel(opts) {
if (opts.reachable) return "Use Ollama on Windows host - running (suggested)";
if (opts.loopbackOnly) return "Restart Ollama on Windows host with 0.0.0.0 binding (suggested)";
return "Start Ollama on Windows host (suggested)";
},
};
}
const detectedRuntime = describeContainerRuntime(runtime);
if (runtime === "docker-desktop") {
return {
supported: true,
detectedRuntime,
installLabel: "Install Ollama on Windows host (recommended)",
startLabel(opts) {
if (opts.reachable) return "Use Ollama on Windows host - running (suggested)";
if (opts.loopbackOnly) return "Restart Ollama on Windows host with 0.0.0.0 binding (suggested)";
return "Start Ollama on Windows host (suggested)";
},
};
}

const labelSuffix = " (requires Docker Desktop WSL integration)";
const reason =
`Windows-host Ollama requires Docker Desktop WSL integration; ` +
`detected ${detectedRuntime} in WSL.`;
const hint = "Choose WSL-local Ollama, or enable Docker Desktop WSL integration for this distro.";
return {
supported: false,
detectedRuntime,
labelSuffix,
installLabel: `Install Ollama on Windows host${labelSuffix}`,
startLabel(opts) {
if (opts.reachable) return `Use Ollama on Windows host - running${labelSuffix}`;
if (opts.loopbackOnly) {
return `Restart Ollama on Windows host with 0.0.0.0 binding${labelSuffix}`;
}
return `Start Ollama on Windows host${labelSuffix}`;
},
reason,
hint,
reject(providerKey, isNonInteractive, abortNonInteractive) {
if (isNonInteractive()) {
abortNonInteractive(
`${providerKey} requires Docker Desktop WSL integration; detected ${detectedRuntime} in WSL.`,
hint,
);
}
console.error(` ${reason}`);
console.error(
" Native Docker-in-WSL cannot reliably route sandbox containers to Windows-host Ollama.",
);
console.error(` ${hint}`);
console.log("");
return true;
},
};
}

export function rejectUnsupportedWindowsHostOllama(
requirement: WindowsHostOllamaDockerRequirement,
providerKey: string,
isWindowsHostOllama: boolean,
isNonInteractive: () => boolean,
abortNonInteractive: (reason: string, hint?: string) => never,
): boolean {
if (!isWindowsHostOllama || requirement.supported) return false;
return requirement.reject(providerKey, isNonInteractive, abortNonInteractive);
}

// True when the sandbox container needs the local Ollama auth proxy in front
// of raw Ollama. False only under Docker Desktop on WSL, where the docker-
// desktop VM publishes the host's 127.0.0.1 back into containers through
Expand Down
20 changes: 20 additions & 0 deletions src/lib/onboard/ollama-install-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,31 @@ import { describe, expect, it } from "vitest";
import {
assertOllamaUpgradeApplied,
resolveOllamaInstallMenuEntry,
resolveRunningOllamaMenuEntry,
} from "../../../dist/lib/onboard/ollama-install-menu";
import { MIN_OLLAMA_VERSION } from "../../../dist/lib/inference/ollama-version";

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

describe("resolveRunningOllamaMenuEntry", () => {
it("labels unsupported Windows-host Ollama without suggesting it", () => {
const entry = resolveRunningOllamaMenuEntry({
hasOllama: false,
ollamaRunning: true,
ollamaHost: "host.docker.internal",
isWsl: true,
ollamaPort: 11434,
windowsHostLabelSuffix: " (requires Docker Desktop WSL integration)",
});

expect(entry).toEqual({
key: "ollama",
label:
"Local Ollama (Windows host:11434) — running (requires Docker Desktop WSL integration)",
});
});
});

describe("resolveOllamaInstallMenuEntry", () => {
it("offers a fresh install when no Ollama is present", () => {
const result = resolveOllamaInstallMenuEntry({
Expand Down
Loading
Loading