diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 2259dd22bf..699099bd15 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -123,12 +123,6 @@ const { }: typeof import("./onboard/model-router-process") = require("./onboard/model-router-process"); const bedrockRuntimeOnboard: typeof import("./onboard/bedrock-runtime") = require("./onboard/bedrock-runtime"); -const { - buildVllmMenuEntries, -}: typeof import("./onboard/vllm-menu") = require("./onboard/vllm-menu"); -const { - detectWindowsHostOllama, -}: typeof import("./onboard/windows-host-ollama") = require("./onboard/windows-host-ollama"); const { installOllamaOnLinux, }: typeof import("./onboard/install-ollama-linux") = require("./onboard/install-ollama-linux"); @@ -204,24 +198,24 @@ const { } = require("./core/ports"); const localInference: typeof import("./inference/local") = require("./inference/local"); const { - findReachableOllamaHost, resetOllamaHostCache, getLocalProviderBaseUrl, getLocalProviderHealthCheck, getLocalProviderValidationBaseUrl, getOllamaModelOptions, getOllamaWarmupCommand, - OLLAMA_HOST_DOCKER_INTERNAL, validateLocalProvider, } = localInference; const { checkOllamaPortsOrWarn, - resolveOllamaInstallMenuEntry, assertOllamaUpgradeApplied, } = require("./onboard/ollama-install-menu"); const { buildInferenceProviderMenu, }: typeof import("./onboard/provider-menu") = require("./onboard/provider-menu"); +const { + detectInferenceProviderHostState, +}: typeof import("./onboard/provider-host-state") = require("./onboard/provider-host-state"); const { ensureOllamaAuthProxy, getOllamaProxyToken, @@ -236,7 +230,7 @@ const { switchToWindowsOllamaHost, printWindowsOllamaTimeoutDiagnostics, } = require("./inference/ollama/windows"); -const { detectVllmProfile, installVllm } = require("./inference/vllm"); +const { installVllm } = require("./inference/vllm"); const inferenceConfig: typeof import("./inference/config") = require("./inference/config"); const { DEFAULT_CLOUD_MODEL, getProviderSelectionConfig, parseGatewayInference } = inferenceConfig; @@ -310,7 +304,6 @@ const platformUtils: typeof import("./platform") = require("./platform"); const { isWsl, shouldPatchCoredns } = platformUtils; const { getContainerRuntime, - getWindowsHostOllamaDockerRequirement, repairLocalInferenceSystemdOverrideOrExit, rejectUnsupportedWindowsHostOllama, shouldFrontOllamaWithProxy, @@ -1396,12 +1389,6 @@ function buildGatewayClusterExecArgv(script: string): string[] { return dockerExecArgv(getGatewayClusterContainerName(), ["sh", "-lc", script]); } -function hostCommandExists(commandName: string): boolean { - return !!runCapture(["sh", "-c", 'command -v "$1"', "--", commandName], { - ignoreError: true, - }); -} - function captureProcessArgs(pid: number): string { return runCapture(["ps", "-p", String(pid), "-o", "args="], { ignoreError: true, @@ -3781,72 +3768,33 @@ async function setupNim( let preferredInferenceApi: string | null = null; let allowToolsIncompatible = false; - const localProbeCurlArgs = ["--connect-timeout", "2", "--max-time", "5"] as const; - 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 }, - ); - // Pick a vLLM install recipe for this host. Profiles live in inference/vllm.ts; - // null means "no supported platform" (vLLM stays behind EXPERIMENTAL). - const vllmProfile = detectVllmProfile(gpu); - // If the profile's image is already cached, the install path is really a - // "start" — docker pull is a no-op and the container can come up in seconds. - const hasVllmImage = !!( - 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. - const winOllamaState = detectWindowsHostOllama(); - const hasWindowsOllama = winOllamaState.installed; - const winOllamaInstalledPath = winOllamaState.installedPath; - const winOllamaLoopbackOnly = winOllamaState.loopbackOnly; - - // Independent of findReachableOllamaHost: when WSL Ollama wins the cache - // 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() && !isWindowsHostOllama) { - windowsOllamaReachable = !!runCapture( - ["curl", "-sf", ...localProbeCurlArgs, `http://host.docker.internal:${OLLAMA_PORT}/api/tags`], - { ignoreError: true }, - ); - } - - // Mirrored mode shares loopback so both probes hit the same instance; - // only NAT mode actually has two separate daemons to warn about. - if (isWsl() && ollamaHost === "127.0.0.1" && windowsOllamaReachable) { - const networkingMode = runCapture(["wslinfo", "--networking-mode"], { - ignoreError: true, - }).trim(); - if (networkingMode !== "mirrored") { - console.log(""); - console.log(" ⚠ Ollama is running on both WSL and the Windows host."); - console.log(" Stop one to avoid duplicated GPU memory and model caches."); - console.log(""); - } - } + const providerHostState = detectInferenceProviderHostState({ + gpu, + experimental: EXPERIMENTAL, + }); + const { + hasOllama, + ollamaHost, + ollamaRunning, + isWindowsHostOllama, + isWsl: isWslHost, + hasWindowsOllama, + winOllamaInstalledPath, + winOllamaLoopbackOnly, + windowsOllamaReachable, + windowsHostOllamaDockerRequirement, + vllmRunning, + vllmProfile, + hasVllmImage, + vllmEntries, + ollamaInstallMenu, + gpuNimCapable, + } = providerHostState; const requestedProvider = getNonInteractiveProvider(); const requestedModel = isNonInteractive() ? getNonInteractiveModel(requestedProvider || "build") : null; const agentProviderOptions = getAgentInferenceProviderOptions(agent); - const ollamaInstallMenu = resolveOllamaInstallMenuEntry({ - hasOllama, - ollamaRunning, - hasWindowsOllama, - ollamaHost, - platform: process.platform, - isWsl: isWsl(), - }); // Model Router: complexity-based routing via blueprint config. const blueprintRouterCfg = loadBlueprintProfile("routed"); @@ -3854,12 +3802,12 @@ async function setupNim( remoteProviderConfig: REMOTE_PROVIDER_CONFIG, agentProviderOptions, experimental: EXPERIMENTAL, - gpuNimCapable: Boolean(gpu && gpu.nimCapable), + gpuNimCapable, hasOllama, ollamaRunning, ollamaHost, ollamaPort: OLLAMA_PORT, - isWsl: isWsl(), + isWsl: isWslHost, hasWindowsOllama, isWindowsHostOllama, windowsHostLabelSuffix: windowsHostOllamaDockerRequirement.supported @@ -3870,13 +3818,7 @@ async function setupNim( windowsOllamaReachable, winOllamaLoopbackOnly, ollamaInstallEntry: ollamaInstallMenu.entry, - vllmEntries: buildVllmMenuEntries({ - vllmRunning, - vllmProfile, - experimental: EXPERIMENTAL, - platform: gpu?.platform, - hasVllmImage, - }), + vllmEntries, routedEnabled: blueprintRouterCfg?.router?.enabled === true, }); @@ -3918,7 +3860,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" && isWindowsHostOllama) { + if (isWslHost && recordedProvider === "ollama-local" && isWindowsHostOllama) { console.error( ` Recorded provider '${recordedProvider}' (WSL Ollama) is not available in this environment.`, ); diff --git a/src/lib/onboard/provider-host-state.test.ts b/src/lib/onboard/provider-host-state.test.ts new file mode 100644 index 0000000000..65ac33a702 --- /dev/null +++ b/src/lib/onboard/provider-host-state.test.ts @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +import { + detectInferenceProviderHostState, + type DetectInferenceProviderHostStateDeps, + type InferenceProviderHostGpu, +} from "../../../dist/lib/onboard/provider-host-state"; + +const SUPPORTED_WINDOWS_OLLAMA = { + supported: true, + detectedRuntime: "Docker Desktop", + installLabel: "Install Ollama on Windows host (recommended)", + startLabel: ({ reachable }: { reachable: boolean; loopbackOnly: boolean }) => + reachable ? "Use Ollama on Windows host - running (suggested)" : "Start Ollama on Windows host", +} as const; + +function buildDeps( + overrides: Partial = {}, +): DetectInferenceProviderHostStateDeps { + return { + runCapture: vi.fn(() => ""), + dockerCapture: vi.fn(() => ""), + hostCommandExists: vi.fn(() => false), + findReachableOllamaHost: vi.fn(() => null), + isWsl: vi.fn(() => false), + getContainerRuntime: vi.fn( + () => "docker-desktop", + ), + detectWindowsHostOllama: vi.fn(() => ({ + installed: false, + installedPath: "", + loopbackOnly: false, + })), + getWindowsHostOllamaDockerRequirement: vi.fn(() => SUPPORTED_WINDOWS_OLLAMA), + detectVllmProfile: vi.fn(() => null), + ...overrides, + }; +} + +function detectWithDeps( + deps: DetectInferenceProviderHostStateDeps, + gpu: InferenceProviderHostGpu | null = null, +) { + return detectInferenceProviderHostState({ + gpu, + experimental: true, + platform: "linux", + env: {}, + log: () => {}, + installedOllamaVersion: "0.24.0", + runningOllamaVersion: "0.24.0", + deps, + }); +} + +describe("detectInferenceProviderHostState", () => { + it("collects local Ollama and vLLM state into one provider host snapshot", () => { + const deps = buildDeps({ + hostCommandExists: vi.fn((command) => command === "ollama"), + findReachableOllamaHost: vi.fn(() => "127.0.0.1"), + runCapture: vi.fn((command) => + command.join(" ").includes(`http://127.0.0.1:8000/v1/models`) ? "{}" : "", + ), + dockerCapture: vi.fn(() => "sha256:cached-image\n"), + detectVllmProfile: vi.fn(() => ({ + name: "Linux + NVIDIA GPU", + platform: "linux" as const, + image: "nvcr.io/nvidia/vllm:test", + defaultModel: {} as never, + containerName: "nemoclaw-vllm", + dockerRunFlags: [], + pullTimeoutSec: 1, + loadTimeoutSec: 1, + })), + }); + + const state = detectWithDeps(deps, { nimCapable: true, type: "nvidia", platform: "linux" }); + + expect(state.hasOllama).toBe(true); + expect(state.ollamaRunning).toBe(true); + expect(state.ollamaHost).toBe("127.0.0.1"); + expect(state.isWindowsHostOllama).toBe(false); + expect(state.vllmRunning).toBe(true); + expect(state.hasVllmImage).toBe(true); + expect(state.vllmEntries.map((entry) => entry.key)).toEqual(["vllm"]); + expect(state.gpuNimCapable).toBe(true); + expect(state.ollamaInstallMenu.entry).toBeNull(); + expect(deps.getWindowsHostOllamaDockerRequirement).toHaveBeenCalledWith(null); + }); + + it("detects a reachable Windows-host Ollama beside WSL-local Ollama and warns outside mirrored networking", () => { + const logs: string[] = []; + const deps = buildDeps({ + isWsl: vi.fn(() => true), + findReachableOllamaHost: vi.fn(() => "127.0.0.1"), + detectWindowsHostOllama: vi.fn(() => ({ + installed: true, + installedPath: "C:\\Users\\me\\AppData\\Local\\Programs\\Ollama\\ollama.exe", + loopbackOnly: false, + })), + runCapture: vi.fn((command) => { + const joined = command.join(" "); + if (joined.includes("host.docker.internal:11434/api/tags")) return "{}"; + if (joined.includes("wslinfo --networking-mode")) return "nat\n"; + return ""; + }), + }); + + const state = detectInferenceProviderHostState({ + gpu: null, + experimental: false, + platform: "linux", + env: {}, + log: (message = "") => logs.push(message), + installedOllamaVersion: "0.24.0", + runningOllamaVersion: "0.24.0", + deps, + }); + + expect(state.isWsl).toBe(true); + expect(state.hasWindowsOllama).toBe(true); + expect(state.windowsOllamaReachable).toBe(true); + expect(state.winOllamaInstalledPath).toMatch(/ollama\.exe$/); + expect(logs.join("\n")).toContain("Ollama is running on both WSL and the Windows host"); + expect(deps.getWindowsHostOllamaDockerRequirement).toHaveBeenCalledWith("docker-desktop"); + }); + + it("passes injected platform and env through WSL detection", () => { + const env = { WSL_DISTRO_NAME: "Ubuntu" } as NodeJS.ProcessEnv; + const isWsl = vi.fn(() => true); + const deps = buildDeps({ isWsl }); + + const state = detectInferenceProviderHostState({ + gpu: null, + experimental: false, + platform: "linux", + env, + log: () => {}, + installedOllamaVersion: "0.24.0", + runningOllamaVersion: "0.24.0", + deps, + }); + + expect(state.isWsl).toBe(true); + expect(isWsl).toHaveBeenCalledWith({ platform: "linux", env }); + }); + + it("suppresses the duplicate-daemon warning when WSL mirrored networking makes the probes equivalent", () => { + const logs: string[] = []; + const deps = buildDeps({ + isWsl: vi.fn(() => true), + findReachableOllamaHost: vi.fn(() => "127.0.0.1"), + detectWindowsHostOllama: vi.fn(() => ({ + installed: true, + installedPath: "C:\\Ollama\\ollama.exe", + loopbackOnly: false, + })), + runCapture: vi.fn((command) => { + const joined = command.join(" "); + if (joined.includes("host.docker.internal:11434/api/tags")) return "{}"; + if (joined.includes("wslinfo --networking-mode")) return "mirrored\n"; + return ""; + }), + }); + + const state = detectInferenceProviderHostState({ + gpu: null, + experimental: false, + platform: "linux", + env: {}, + log: (message = "") => logs.push(message), + installedOllamaVersion: "0.24.0", + runningOllamaVersion: "0.24.0", + deps, + }); + + expect(state.windowsOllamaReachable).toBe(true); + expect(logs).toEqual([]); + }); + + it("does not probe the Windows-host switch path when running Ollama already resolves to the Windows host", () => { + const runCapture = vi.fn(() => ""); + const deps = buildDeps({ + isWsl: vi.fn(() => true), + findReachableOllamaHost: vi.fn(() => "host.docker.internal"), + detectWindowsHostOllama: vi.fn(() => ({ + installed: true, + installedPath: "C:\\Ollama\\ollama.exe", + loopbackOnly: true, + })), + runCapture, + }); + + const state = detectWithDeps(deps); + + expect(state.isWindowsHostOllama).toBe(true); + expect(state.windowsOllamaReachable).toBe(false); + expect( + runCapture.mock.calls.some(([command]) => + command.join(" ").includes("host.docker.internal:11434/api/tags"), + ), + ).toBe(false); + }); +}); diff --git a/src/lib/onboard/provider-host-state.ts b/src/lib/onboard/provider-host-state.ts new file mode 100644 index 0000000000..8b50e5c998 --- /dev/null +++ b/src/lib/onboard/provider-host-state.ts @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { dockerCapture as defaultDockerCapture } from "../adapters/docker"; +import { OLLAMA_PORT, VLLM_PORT } from "../core/ports"; +import { findReachableOllamaHost, OLLAMA_HOST_DOCKER_INTERNAL } from "../inference/local"; +import type { NvidiaPlatform } from "../inference/nim"; +import { detectVllmProfile, type VllmProfile } from "../inference/vllm"; +import { + type ContainerRuntime, + isWsl as defaultIsWsl, + type WslDetectionOptions, +} from "../platform"; +import { runCapture as defaultRunCapture } from "../runner"; +import { + getContainerRuntime as defaultGetContainerRuntime, + getWindowsHostOllamaDockerRequirement, + type WindowsHostOllamaDockerRequirement, +} from "./local-inference-topology"; +import { resolveOllamaInstallMenuEntry, type OllamaInstallMenuResult } from "./ollama-install-menu"; +import { buildVllmMenuEntries, type VllmMenuEntry } from "./vllm-menu"; +import { detectWindowsHostOllama, type WindowsHostOllamaState } from "./windows-host-ollama"; + +type RunCapture = (args: string[], options?: { ignoreError?: boolean }) => string; +type DockerCapture = (args: string[], options?: { ignoreError?: boolean }) => string; + +export interface InferenceProviderHostGpu { + nimCapable?: boolean; + spark?: boolean; + type?: string; + platform?: NvidiaPlatform; +} + +export interface InferenceProviderHostState { + hasOllama: boolean; + ollamaHost: string | null; + ollamaRunning: boolean; + isWindowsHostOllama: boolean; + isWsl: boolean; + hasWindowsOllama: boolean; + winOllamaInstalledPath: string; + winOllamaLoopbackOnly: boolean; + windowsOllamaReachable: boolean; + windowsHostOllamaDockerRequirement: WindowsHostOllamaDockerRequirement; + vllmRunning: boolean; + vllmProfile: VllmProfile | null; + hasVllmImage: boolean; + vllmEntries: VllmMenuEntry[]; + ollamaInstallMenu: OllamaInstallMenuResult; + gpuNimCapable: boolean; +} + +export interface DetectInferenceProviderHostStateInput { + gpu: InferenceProviderHostGpu | null | undefined; + experimental: boolean; + platform?: NodeJS.Platform; + env?: NodeJS.ProcessEnv; + log?: (message?: string) => void; + installedOllamaVersion?: string | null; + runningOllamaVersion?: string | null; + deps?: Partial; +} + +export interface DetectInferenceProviderHostStateDeps { + runCapture: RunCapture; + dockerCapture: DockerCapture; + hostCommandExists: (commandName: string) => boolean; + findReachableOllamaHost: () => string | null; + isWsl: (opts?: WslDetectionOptions) => boolean; + getContainerRuntime: () => ContainerRuntime; + detectWindowsHostOllama: () => WindowsHostOllamaState; + getWindowsHostOllamaDockerRequirement: ( + runtime: ContainerRuntime | null, + ) => WindowsHostOllamaDockerRequirement; + detectVllmProfile: (gpu: InferenceProviderHostGpu | null | undefined) => VllmProfile | null; +} + +const LOCAL_PROVIDER_PROBE_CURL_ARGS = ["--connect-timeout", "2", "--max-time", "5"] as const; + +function hostCommandExists(commandName: string, runCapture: RunCapture): boolean { + return !!runCapture(["sh", "-c", 'command -v "$1"', "--", commandName], { + ignoreError: true, + }); +} + +function buildDeps( + overrides: Partial = {}, +): DetectInferenceProviderHostStateDeps { + const runCapture = overrides.runCapture ?? defaultRunCapture; + return { + runCapture, + dockerCapture: overrides.dockerCapture ?? defaultDockerCapture, + hostCommandExists: + overrides.hostCommandExists ?? ((command) => hostCommandExists(command, runCapture)), + findReachableOllamaHost: overrides.findReachableOllamaHost ?? findReachableOllamaHost, + isWsl: overrides.isWsl ?? defaultIsWsl, + getContainerRuntime: overrides.getContainerRuntime ?? defaultGetContainerRuntime, + detectWindowsHostOllama: overrides.detectWindowsHostOllama ?? detectWindowsHostOllama, + getWindowsHostOllamaDockerRequirement: + overrides.getWindowsHostOllamaDockerRequirement ?? getWindowsHostOllamaDockerRequirement, + detectVllmProfile: + overrides.detectVllmProfile ?? + ((gpu) => detectVllmProfile(gpu as Parameters[0])), + }; +} + +function probeVllmRunning(runCapture: RunCapture): boolean { + return !!runCapture( + ["curl", "-sf", ...LOCAL_PROVIDER_PROBE_CURL_ARGS, `http://127.0.0.1:${VLLM_PORT}/v1/models`], + { ignoreError: true }, + ); +} + +function probeWindowsOllamaReachable(input: { + isWsl: boolean; + isWindowsHostOllama: boolean; + runCapture: RunCapture; +}): boolean { + if (!input.isWsl || input.isWindowsHostOllama) return false; + return !!input.runCapture( + [ + "curl", + "-sf", + ...LOCAL_PROVIDER_PROBE_CURL_ARGS, + `http://host.docker.internal:${OLLAMA_PORT}/api/tags`, + ], + { ignoreError: true }, + ); +} + +function maybeWarnAboutDuplicateOllamaDaemons(input: { + isWsl: boolean; + ollamaHost: string | null; + windowsOllamaReachable: boolean; + runCapture: RunCapture; + log: (message?: string) => void; +}): void { + if (!input.isWsl || input.ollamaHost !== "127.0.0.1" || !input.windowsOllamaReachable) return; + const networkingMode = input + .runCapture(["wslinfo", "--networking-mode"], { + ignoreError: true, + }) + .trim(); + if (networkingMode === "mirrored") return; + input.log(""); + input.log(" ⚠ Ollama is running on both WSL and the Windows host."); + input.log(" Stop one to avoid duplicated GPU memory and model caches."); + input.log(""); +} + +export function detectInferenceProviderHostState( + input: DetectInferenceProviderHostStateInput, +): InferenceProviderHostState { + const deps = buildDeps(input.deps); + const log = input.log ?? console.log; + const platform = input.platform ?? process.platform; + const isWsl = deps.isWsl({ platform, env: input.env }); + const hasOllama = deps.hostCommandExists("ollama"); + const ollamaHost = deps.findReachableOllamaHost(); + const ollamaRunning = ollamaHost !== null; + const isWindowsHostOllama = ollamaHost === OLLAMA_HOST_DOCKER_INTERNAL; + const vllmRunning = probeVllmRunning(deps.runCapture); + const vllmProfile = deps.detectVllmProfile(input.gpu); + const hasVllmImage = !!( + vllmProfile && + deps.dockerCapture(["images", "-q", vllmProfile.image], { ignoreError: true }).trim() + ); + const windowsHostOllamaDockerRequirement = deps.getWindowsHostOllamaDockerRequirement( + isWsl ? deps.getContainerRuntime() : null, + ); + const winOllamaState = deps.detectWindowsHostOllama(); + const hasWindowsOllama = winOllamaState.installed; + const windowsOllamaReachable = probeWindowsOllamaReachable({ + isWsl, + isWindowsHostOllama, + runCapture: deps.runCapture, + }); + + maybeWarnAboutDuplicateOllamaDaemons({ + isWsl, + ollamaHost, + windowsOllamaReachable, + runCapture: deps.runCapture, + log, + }); + + const ollamaInstallMenu = resolveOllamaInstallMenuEntry({ + hasOllama, + ollamaRunning, + hasWindowsOllama, + ollamaHost, + platform, + isWsl, + installedOllamaVersion: input.installedOllamaVersion, + runningOllamaVersion: input.runningOllamaVersion, + }); + + return { + hasOllama, + ollamaHost, + ollamaRunning, + isWindowsHostOllama, + isWsl, + hasWindowsOllama, + winOllamaInstalledPath: winOllamaState.installedPath, + winOllamaLoopbackOnly: winOllamaState.loopbackOnly, + windowsOllamaReachable, + windowsHostOllamaDockerRequirement, + vllmRunning, + vllmProfile, + hasVllmImage, + vllmEntries: buildVllmMenuEntries({ + vllmRunning, + vllmProfile, + experimental: input.experimental, + platform: input.gpu?.platform, + hasVllmImage, + env: input.env, + log: (message) => log(message), + }), + ollamaInstallMenu, + gpuNimCapable: Boolean(input.gpu?.nimCapable), + }; +}