Skip to content
113 changes: 41 additions & 72 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ const {
getSelectionDrift,
}: typeof import("./onboard/selection-drift") = require("./onboard/selection-drift");
const {
resolveProviderKeyFallback,
}: typeof import("./onboard/provider-key-fallback") = require("./onboard/provider-key-fallback");
resolveRequestedProviderSelection,
}: typeof import("./onboard/provider-selection") = require("./onboard/provider-selection");
const {
isLinuxDockerDriverGatewayEnabled,
}: typeof import("./onboard/docker-driver-platform") = require("./onboard/docker-driver-platform");
Expand Down Expand Up @@ -3801,93 +3801,62 @@ async function setupNim(
hermesAuthMethod = null;

if (isNonInteractive() || requestedProvider) {
let providerKey = requestedProvider;
if (!providerKey) {
const recordedProvider = readRecordedProvider(sandboxName);
const hasNimContainer = !!readRecordedNimContainer(sandboxName);
const recoveredKey = providerRecovery.providerNameToOptionKey(
REMOTE_PROVIDER_CONFIG,
recordedProvider,
{ hasNimContainer },
);
if (recoveredKey) {
// Refuse to silently switch providers behind the user's back; if
// the previously-recorded one is gone, surface the recorded value
// so the user can fix the dependency or override via env var.
// Special case: on WSL, recorded ollama-local was WSL Ollama at
// record time. If the only reachable Ollama is now Windows-host
// (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 (isWslHost && recordedProvider === "ollama-local" && isWindowsHostOllama) {
const providerSelection = resolveRequestedProviderSelection({
options,
requestedProvider,
sandboxName,
remoteProviderConfig: REMOTE_PROVIDER_CONFIG,
isWsl: isWslHost,
isWindowsHostOllama,
windowsHostOllamaSupported: windowsHostOllamaDockerRequirement.supported,
hermesProviderAvailable,
readRecordedProvider,
readRecordedNimContainer,
readRecordedModel,
});
if (providerSelection.kind === "failure") {
switch (providerSelection.reason.kind) {
case "wsl-recorded-ollama-windows-host":
console.error(
` Recorded provider '${recordedProvider}' (WSL Ollama) is not available in this environment.`,
` Recorded provider '${providerSelection.reason.recordedProvider}' (WSL Ollama) is not available in this environment.`,
);
console.error(
" Hint: Windows-host Ollama is reachable here; re-run with NEMOCLAW_PROVIDER=ollama to use it explicitly.",
);
process.exit(1);
}
if (!options.some((o) => o.key === recoveredKey)) {
break;
case "recorded-provider-unavailable":
console.error(
` Recorded provider '${recordedProvider}' is not available in this environment.`,
` Recorded provider '${providerSelection.reason.recordedProvider}' is not available in this environment.`,
);
console.error(
" Set NEMOCLAW_PROVIDER explicitly, or restore the missing local-inference dependency.",
);
if (recoveredKey === "ollama") {
const winHostKey = options.find(
(o) => o.key === "start-windows-ollama" || o.key === "install-windows-ollama",
)?.key;
if (winHostKey) {
console.error(
` Hint: Windows-host Ollama is available here — re-run with NEMOCLAW_PROVIDER=${winHostKey} to use it.`,
);
}
if (providerSelection.reason.windowsHostKey) {
console.error(
` Hint: Windows-host Ollama is available here — re-run with NEMOCLAW_PROVIDER=${providerSelection.reason.windowsHostKey} to use it.`,
);
}
process.exit(1);
}
providerKey = recoveredKey;
recoveredFromSandbox = true;
recoveredModel = readRecordedModel(sandboxName);
} else if (recordedProvider === "vllm-local") {
// vllm-local without a nimContainer marker is ambiguous — could be
// standalone vLLM or Local NIM. Don't guess; require an override.
console.error(
" Recorded provider 'vllm-local' is ambiguous (could be standalone vLLM or Local NIM).",
);
console.error(" Set NEMOCLAW_PROVIDER explicitly (vllm or nim-local) and re-run.");
process.exit(1);
} else {
providerKey = "build";
}
}
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, {
canUseWindowsHostOllama:
isWindowsHostOllama && windowsHostOllamaDockerRequirement.supported,
});
if (!selected) {
if (providerKey === "hermesProvider" && !hermesProviderAvailable) {
break;
case "unsupported-windows-host-ollama":
rejectWindowsHostOllama(providerSelection.reason.providerKey, isWindowsHostOllama);
break;
case "hermes-provider-unavailable":
console.error(" Hermes Provider is only available when onboarding Hermes Agent.");
console.error(
" Re-run with `nemohermes onboard` or `nemoclaw onboard --agent hermes`.",
);
process.exit(1);
}
console.error(
` Requested provider '${providerKey}' is not available in this environment.`,
);
process.exit(1);
break;
case "requested-provider-unavailable":
console.error(
` Requested provider '${providerSelection.reason.providerKey}' is not available in this environment.`,
);
break;
}
process.exit(1);
}
selected = providerSelection.selected;
recoveredFromSandbox = providerSelection.recoveredFromSandbox;
recoveredModel = providerSelection.recoveredModel;
note(
recoveredFromSandbox
? ` [non-interactive] Provider: ${selected.key} (recovered from sandbox '${sandboxName}')`
Expand Down
131 changes: 131 additions & 0 deletions src/lib/onboard/provider-selection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import assert from "node:assert/strict";
import { describe, it } from "vitest";

import { resolveRequestedProviderSelection } from "../../../dist/lib/onboard/provider-selection";

const option = (key: string) => ({ key, label: key });

const remoteProviderConfig = {
build: { providerName: "nvidia-prod" },
openai: { providerName: "openai-api" },
hermesProvider: { providerName: "hermes-provider" },
};

function resolve(overrides: Partial<Parameters<typeof resolveRequestedProviderSelection>[0]> = {}) {
return resolveRequestedProviderSelection({
options: [option("build")],
requestedProvider: null,
sandboxName: "sandbox",
remoteProviderConfig,
isWsl: false,
isWindowsHostOllama: false,
windowsHostOllamaSupported: false,
hermesProviderAvailable: false,
readRecordedProvider: () => null,
readRecordedNimContainer: () => null,
readRecordedModel: () => null,
...overrides,
});
}

describe("resolveRequestedProviderSelection", () => {
it("falls back install action keys to currently available providers", () => {
const result = resolve({
options: [option("build"), option("ollama")],
requestedProvider: "install-ollama",
});

assert.equal(result.kind, "selected");
if (result.kind === "selected") {
assert.equal(result.selected.key, "ollama");
assert.equal(result.recoveredFromSandbox, false);
assert.equal(result.recoveredModel, null);
}
});

it("recovers the recorded provider and model when no provider was requested", () => {
const result = resolve({
options: [option("build"), option("openai")],
readRecordedProvider: () => "openai-api",
readRecordedModel: () => "gpt-example",
});

assert.equal(result.kind, "selected");
if (result.kind === "selected") {
assert.equal(result.selected.key, "openai");
assert.equal(result.recoveredFromSandbox, true);
assert.equal(result.recoveredModel, "gpt-example");
}
});

it("does not silently map a recorded WSL Ollama provider to Windows-host Ollama", () => {
const result = resolve({
options: [option("build"), option("ollama")],
isWsl: true,
isWindowsHostOllama: true,
windowsHostOllamaSupported: true,
readRecordedProvider: () => "ollama-local",
});

assert.equal(result.kind, "failure");
if (result.kind === "failure") {
assert.equal(result.reason.kind, "wsl-recorded-ollama-windows-host");
}
});

it("returns a Windows-host hint when recorded Ollama is unavailable but a host action exists", () => {
const result = resolve({
options: [option("build"), option("start-windows-ollama")],
readRecordedProvider: () => "ollama-local",
});

assert.equal(result.kind, "failure");
if (result.kind === "failure") {
assert.equal(result.reason.kind, "recorded-provider-unavailable");
if (result.reason.kind === "recorded-provider-unavailable") {
assert.equal(result.reason.recoveredKey, "ollama");
assert.equal(result.reason.windowsHostKey, "start-windows-ollama");
}
}
});

it("reports Hermes Provider as agent-gated when it is requested for another agent", () => {
const result = resolve({
requestedProvider: "hermesProvider",
hermesProviderAvailable: false,
});

assert.equal(result.kind, "failure");
if (result.kind === "failure") {
assert.equal(result.reason.kind, "hermes-provider-unavailable");
}
});

it("reports unsupported Windows-host Ollama before applying compatible fallbacks", () => {
const result = resolve({
requestedProvider: "start-windows-ollama",
isWindowsHostOllama: true,
windowsHostOllamaSupported: false,
});

assert.equal(result.kind, "failure");
if (result.kind === "failure") {
assert.equal(result.reason.kind, "unsupported-windows-host-ollama");
}
});

it("defaults to NVIDIA Endpoints when no requested or recorded provider is available", () => {
const result = resolve({
options: [option("build"), option("openai")],
});

assert.equal(result.kind, "selected");
if (result.kind === "selected") {
assert.equal(result.selected.key, "build");
assert.equal(result.recoveredFromSandbox, false);
}
});
});
Loading
Loading