diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index b79e6c44a5e..4aabb2e6412 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -97,7 +97,8 @@ Use these details when your first-run path needs more control. curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- --yes-i-accept-third-party-software ``` - For a non-interactive first run, also set the provider, credential, and sandbox name. + For a non-interactive first run, set the sandbox name. + Set the provider and matching credential unless you want DGX Spark to select local vLLM automatically. ```bash curl -fsSL https://www.nvidia.com/nemoclaw.sh | \ @@ -113,6 +114,7 @@ Use these details when your first-run path needs more control. The example uses NVIDIA Endpoints. Set `NEMOCLAW_AGENT` to `hermes` or `langchain-deepagents-code` to install another agent. Set `NEMOCLAW_PROVIDER` and the matching credential variable for another provider, then use a sandbox name that does not depend on a previous onboarding session. + On DGX Spark, omit `NEMOCLAW_PROVIDER` only when you want the automatic local selection described in [Set Up vLLM](../inference/local-inference/set-up-vllm#run-non-interactive-onboarding). To select a specific NemoClaw release, replace `vX.Y.Z` with its versioned release tag. `NEMOCLAW_INSTALL_REF` is a higher-priority development override, so clear it when pinning a release tag. diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 143cde98022..1474c9157f2 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -211,6 +211,12 @@ For the current support status and direct GPU policy boundaries, see [Platform S ## Run Non-Interactive Onboarding +On DGX Spark, non-interactive onboarding can select local vLLM when `NEMOCLAW_PROVIDER` is unset and no provider is recorded for the sandbox. +It reuses a running local vLLM server first. +If no server is running, it selects the managed install or start entry. +If neither local entry is available, it selects NVIDIA Endpoints. +On DGX Station and other hosts, an unset provider keeps NVIDIA Endpoints as the automatic default. + Use an already-running server. ```bash diff --git a/src/lib/onboard/provider-selection.test.ts b/src/lib/onboard/provider-selection.test.ts index 13af98d275d..a7a1e4113f3 100644 --- a/src/lib/onboard/provider-selection.test.ts +++ b/src/lib/onboard/provider-selection.test.ts @@ -14,6 +14,10 @@ const remoteProviderConfig = { hermesProvider: { providerName: "hermes-provider" }, }; +// Ternary accessor (no `if`, per the changed-test-file conditionals guardrail). +const selectedKey = (result: ReturnType) => + result.kind === "selected" ? result.selected.key : null; + function resolve(overrides: Partial[0]> = {}) { return resolveRequestedProviderSelection({ options: [option("build")], @@ -128,4 +132,42 @@ describe("resolveRequestedProviderSelection", () => { assert.equal(result.recoveredFromSandbox, false); } }); + + it("auto-selects managed vLLM on a DGX managed-vLLM platform when no provider is given (#7293)", () => { + const result = resolve({ + options: [option("build"), option("install-vllm")], + preferManagedVllmDefault: true, + }); + + assert.equal(selectedKey(result), "install-vllm"); + }); + + it("auto-selects an already-running local vLLM on a managed-vLLM platform (#7293)", () => { + // When vLLM is already running, the menu exposes only `vllm` (not install-vllm). + const result = resolve({ + options: [option("build"), option("vllm")], + preferManagedVllmDefault: true, + }); + + assert.equal(selectedKey(result), "vllm"); + }); + + it("keeps the cloud default when the caller does not prefer managed vLLM (#7293)", () => { + // The menu can expose managed vLLM without changing the automatic selection. + const result = resolve({ + options: [option("build"), option("install-vllm")], + preferManagedVllmDefault: false, + }); + + assert.equal(selectedKey(result), "build"); + }); + + it("keeps the cloud default when no managed-vLLM entry is available (#7293)", () => { + const result = resolve({ + options: [option("build"), option("openai")], + preferManagedVllmDefault: true, + }); + + assert.equal(selectedKey(result), "build"); + }); }); diff --git a/src/lib/onboard/provider-selection.ts b/src/lib/onboard/provider-selection.ts index 03164ce359b..13f2addc833 100644 --- a/src/lib/onboard/provider-selection.ts +++ b/src/lib/onboard/provider-selection.ts @@ -59,12 +59,34 @@ export interface ResolveRequestedProviderSelectionInput(options: T[], key: string): T | undefined { return options.find((option) => option.key === key); } +/** + * On a managed-vLLM-default platform (#7293), pick the available local vLLM menu + * option: `vllm` when a server is already running (the menu exposes only that + * entry), otherwise the managed install `install-vllm`. Returns null when the + * preference is off or neither entry is present, so the caller falls back to + * cloud `build`. + */ +function resolveManagedVllmDefaultKey( + input: ResolveRequestedProviderSelectionInput, +): string | null { + if (!input.preferManagedVllmDefault) return null; + if (findOption(input.options, "vllm")) return "vllm"; + if (findOption(input.options, "install-vllm")) return "install-vllm"; + return null; +} + function findWindowsHostKey(options: ProviderOption[]): string | null { return ( options.find((option) => option.key === "start-windows-ollama")?.key || @@ -118,7 +140,9 @@ export function resolveRequestedProviderSelection( recoveredFromSandbox = true; recoveredModel = input.readRecordedModel(input.sandboxName); } else { - providerKey = "build"; + // Prefer managed local vLLM when the caller has approved that platform + // default; otherwise fall back to cloud NVIDIA Endpoints (#7293). + providerKey = resolveManagedVllmDefaultKey(input) ?? "build"; } } diff --git a/src/lib/onboard/setup-nim-flow.test.ts b/src/lib/onboard/setup-nim-flow.test.ts index e8151790ab1..83160750def 100644 --- a/src/lib/onboard/setup-nim-flow.test.ts +++ b/src/lib/onboard/setup-nim-flow.test.ts @@ -729,6 +729,136 @@ describe("createSetupNim", () => { }); }); + it("auto-selects managed vLLM on a DGX Spark non-interactive run with no requested provider (#7293)", async () => { + const profile = { name: "DGX Spark" } as VllmProfile; + const prompt = vi.fn(async () => unexpected("provider prompt")); + const detectInferenceProviderHostState = vi.fn(() => + makeHostState({ + vllmProfile: profile, + hasVllmImage: true, + vllmEntries: [{ key: "install-vllm", label: "Start vLLM (DGX Spark)" }], + }), + ); + const installVllm = vi.fn(async (_profile, options) => { + options.beforeInstall?.("vllm-model"); + return { ok: true }; + }); + const routeGuard = vi.fn(() => ({ + requiredModel: null, + requiredEndpointUrl: null, + requiredInferenceApi: null, + })); + const handleVllmSelection = vi.fn(async (state) => { + state.provider = "vllm"; + state.endpointUrl = "http://127.0.0.1:8000/v1"; + state.credentialEnv = null; + state.preferredInferenceApi = "openai-completions"; + return "selected"; + }); + const setupNim = createSetupNim( + makeDeps({ + isNonInteractive: () => true, + // No explicit provider: the DGX Spark platform default must still pick + // managed vLLM instead of falling back to the cloud `build` handler + // (handleRemoteProviderSelection stays `unexpected`, so a fallback throws). + getNonInteractiveProvider: () => null, + prompt, + detectInferenceProviderHostState, + installVllm, + handleVllmSelection, + }), + ); + + const sparkGpu = { platform: "spark" } as unknown as Parameters[0]; + const result = await setupNim(sparkGpu, null, null, true, null, "nemoclaw", routeGuard); + + expect(installVllm).toHaveBeenCalledWith( + profile, + expect.objectContaining({ hasImage: true, nonInteractive: true }), + ); + expect(handleVllmSelection).toHaveBeenCalledOnce(); + expect(prompt).not.toHaveBeenCalled(); + expect(result).toMatchObject({ provider: "vllm" }); + }); + + it("reuses an already-running local vLLM on a DGX Spark non-interactive run with no requested provider (#7293)", async () => { + const profile = { name: "DGX Spark" } as VllmProfile; + // vLLM already running → the menu exposes only `vllm`; the no-provider + // default must reuse it (handleVllmSelection) and never reinstall + // (installVllm stays `unexpected`) or fall back to the cloud handler. + const detectInferenceProviderHostState = vi.fn(() => + makeHostState({ + vllmRunning: true, + vllmProfile: profile, + hasVllmImage: true, + vllmEntries: [{ key: "vllm", label: "Local vLLM (localhost:8000) — running (suggested)" }], + }), + ); + const routeGuard = vi.fn(() => ({ + requiredModel: null, + requiredEndpointUrl: null, + requiredInferenceApi: null, + })); + const handleVllmSelection = vi.fn(async (state) => { + state.provider = "vllm"; + state.model = "vllm-model"; + state.endpointUrl = "http://127.0.0.1:8000/v1"; + state.credentialEnv = null; + state.preferredInferenceApi = "openai-completions"; + return "selected"; + }); + const setupNim = createSetupNim( + makeDeps({ + isNonInteractive: () => true, + getNonInteractiveProvider: () => null, + detectInferenceProviderHostState, + handleVllmSelection, + }), + ); + + const sparkGpu = { platform: "spark" } as unknown as Parameters[0]; + const result = await setupNim(sparkGpu, null, null, true, null, "nemoclaw", routeGuard); + + expect(handleVllmSelection).toHaveBeenCalledOnce(); + expect(handleVllmSelection).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ managedInstall: false }), + ); + expect(result).toMatchObject({ provider: "vllm" }); + }); + + it("does not extend the Spark automatic default to DGX Station (#7293)", async () => { + const handleRemoteProviderSelection = vi.fn( + async ({ selected }, state) => { + expect(selected.key).toBe("build"); + state.model = "nvidia/nemotron-3-ultra-550b-a55b"; + state.provider = "nvidia-prod"; + state.endpointUrl = "https://integrate.api.nvidia.com/v1"; + state.credentialEnv = "NVIDIA_INFERENCE_API_KEY"; + state.preferredInferenceApi = "openai-completions"; + return "selected"; + }, + ); + const setupNim = createSetupNim( + makeDeps({ + isNonInteractive: () => true, + getNonInteractiveProvider: () => null, + detectInferenceProviderHostState: () => + makeHostState({ + vllmProfile: { name: "DGX Station" } as VllmProfile, + vllmEntries: [{ key: "install-vllm", label: "Start vLLM (DGX Station)" }], + }), + handleRemoteProviderSelection, + }), + ); + + const stationGpu = { platform: "station" } as unknown as Parameters[0]; + const result = await setupNim(stationGpu); + + expect(handleRemoteProviderSelection).toHaveBeenCalledOnce(); + expect(result).toMatchObject({ provider: "nvidia-prod" }); + }); + it("threads the DGX Station express model through the standard managed-vLLM selection contract", async () => { const profile = { name: "DGX Station", platform: "station" } as VllmProfile; const servedModel = "nvidia/nemotron-3-ultra-550b-a55b"; diff --git a/src/lib/onboard/setup-nim-flow.ts b/src/lib/onboard/setup-nim-flow.ts index 9def65923a5..179d8249c68 100644 --- a/src/lib/onboard/setup-nim-flow.ts +++ b/src/lib/onboard/setup-nim-flow.ts @@ -403,6 +403,7 @@ export function createSetupNim( isWindowsHostOllama, windowsHostOllamaSupported: windowsHostOllamaDockerRequirement.supported, hermesProviderAvailable, + preferManagedVllmDefault: gpu?.platform === "spark", ...recordedProviderReaders, }); if (providerSelection.kind === "failure") { diff --git a/src/lib/onboard/vllm-menu.test.ts b/src/lib/onboard/vllm-menu.test.ts index 3564eccce5a..68ed495b0e4 100644 --- a/src/lib/onboard/vllm-menu.test.ts +++ b/src/lib/onboard/vllm-menu.test.ts @@ -5,7 +5,7 @@ import assert from "node:assert/strict"; import { describe, it } from "vitest"; -import { buildVllmMenuEntries } from "./vllm-menu"; +import { buildVllmMenuEntries, isManagedVllmDefaultPlatform } from "./vllm-menu"; describe("buildVllmMenuEntries", () => { it("returns no entries when nothing is running, no profile, and no opt-in", () => { @@ -182,3 +182,17 @@ describe("buildVllmMenuEntries", () => { assert.deepEqual(logs, []); }); }); + +describe("isManagedVllmDefaultPlatform (#7293)", () => { + it("is true for the DGX managed-vLLM default platforms", () => { + assert.equal(isManagedVllmDefaultPlatform("spark"), true); + assert.equal(isManagedVllmDefaultPlatform("station"), true); + }); + + it("is false for other or missing platforms", () => { + assert.equal(isManagedVllmDefaultPlatform("linux"), false); + assert.equal(isManagedVllmDefaultPlatform("jetson"), false); + assert.equal(isManagedVllmDefaultPlatform(null), false); + assert.equal(isManagedVllmDefaultPlatform(undefined), false); + }); +}); diff --git a/src/lib/onboard/vllm-menu.ts b/src/lib/onboard/vllm-menu.ts index 58709a87bee..91e8e89032a 100644 --- a/src/lib/onboard/vllm-menu.ts +++ b/src/lib/onboard/vllm-menu.ts @@ -32,6 +32,11 @@ interface VllmProfileShape { const MANAGED_VLLM_DEFAULT_PLATFORMS = new Set(["spark", "station"]); +/** DGX platforms where the provider menu exposes managed vLLM without `experimental`. */ +export function isManagedVllmDefaultPlatform(platform: NvidiaPlatform | null | undefined): boolean { + return platform != null && MANAGED_VLLM_DEFAULT_PLATFORMS.has(platform); +} + export interface VllmMenuEntry { key: "vllm" | "install-vllm"; label: string; @@ -62,8 +67,7 @@ export function buildVllmMenuEntries(opts: BuildVllmMenuOptions): VllmMenuEntry[ ` Note: NEMOCLAW_PROVIDER=install-vllm requested, but vLLM is already running on localhost:${VLLM_PORT} — selecting the running instance.`, ); } - const experimentalLabel = - opts.platform && MANAGED_VLLM_DEFAULT_PLATFORMS.has(opts.platform) ? "" : " [experimental]"; + const experimentalLabel = isManagedVllmDefaultPlatform(opts.platform) ? "" : " [experimental]"; return [ { key: "vllm", @@ -73,8 +77,7 @@ export function buildVllmMenuEntries(opts: BuildVllmMenuOptions): VllmMenuEntry[ } if ( userChoseManagedVllm || - (opts.vllmProfile && - (opts.experimental || (opts.platform && MANAGED_VLLM_DEFAULT_PLATFORMS.has(opts.platform)))) + (opts.vllmProfile && (opts.experimental || isManagedVllmDefaultPlatform(opts.platform))) ) { const verb = opts.hasVllmImage ? "Start" : "Install"; const profileLabel = opts.vllmProfile?.name ?? "no profile detected";