diff --git a/docs/inference/set-up-ollama.mdx b/docs/inference/set-up-ollama.mdx index b35e0630d48..c1438cd1ffb 100644 --- a/docs/inference/set-up-ollama.mdx +++ b/docs/inference/set-up-ollama.mdx @@ -193,6 +193,15 @@ If the tag is absent from the rendered installed or starter list, the normal mem If a known bootstrap model does not fit, NemoClaw warns and falls back to the largest known model that does fit. Unknown or custom tags pass through to the Ollama runner for validation. +OpenShell currently uses one inference route per gateway. +If another sandbox on the same gateway already uses an Ollama model, onboarding names that required model instead of silently presenting it as a new choice. +Use a separate gateway on an unused port to select a different model for the new agent. +The following command uses `8990` as an example; choose another unused port when necessary: + +```bash +NEMOCLAW_GATEWAY_PORT=8990 NEMOCLAW_MODEL=qwen3.5:9b $$nemoclaw onboard +``` + Interactive onboarding filters installed registry-known tags that do not fit current GPU memory. If no installed known tag fits, NemoClaw displays starter choices and warns when even the smallest tag might not fit. After a model fails validation, NemoClaw excludes it from the next installed-model menu. diff --git a/src/lib/onboard/setup-nim-ollama.test.ts b/src/lib/onboard/setup-nim-ollama.test.ts index 68ae3578973..3cb7ab7d796 100644 --- a/src/lib/onboard/setup-nim-ollama.test.ts +++ b/src/lib/onboard/setup-nim-ollama.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { MIN_HERMES_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runtime-context"; import { createSetupNimOllamaHandlers } from "./setup-nim-ollama"; @@ -26,6 +26,10 @@ function makeState(): SetupNimSelectionState { type Deps = Parameters[0]; +afterEach(() => { + vi.restoreAllMocks(); +}); + function makeDeps(overrides: Partial = {}): Deps { return { OLLAMA_PORT: 11434, @@ -64,6 +68,7 @@ function makeDeps(overrides: Partial = {}): Deps { describe("createSetupNimOllamaHandlers", () => { it("guards the selected route before systemd recovery and model preparation (#6315)", async () => { const events: string[] = []; + const log = vi.spyOn(console, "log").mockImplementation(() => {}); const state = makeState(); state.assertRouteCompatible = () => { events.push(`guard:${String(state.model)}`); @@ -75,6 +80,7 @@ describe("createSetupNimOllamaHandlers", () => { }; const { handleRunningOllamaSelection } = createSetupNimOllamaHandlers( makeDeps({ + isNonInteractive: () => false, ensureOllamaLoopbackSystemdOverride: () => { events.push("systemd"); return "unchanged"; @@ -97,6 +103,42 @@ describe("createSetupNimOllamaHandlers", () => { "prepare-model", "guard:required/model", ]); + expect(log.mock.calls.map(([message]) => message)).toContain( + " Shared gateway route requires Ollama model 'required/model'.", + ); + expect(log.mock.calls.map(([message]) => message)).toContain( + " To use a different model for this agent, rerun with an unused NEMOCLAW_GATEWAY_PORT.", + ); + log.mockRestore(); + }); + + it("keeps shared-route guidance silent in non-interactive mode (#6758)", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + const selectModel = vi.fn(async () => ({ + outcome: "selected" as const, + model: "required/model", + allowToolsIncompatible: false, + })); + const state = makeState(); + state.assertRouteCompatible = () => ({ + requiredModel: "required/model", + requiredEndpointUrl: null, + requiredInferenceApi: null, + }); + const { handleRunningOllamaSelection } = createSetupNimOllamaHandlers( + makeDeps({ selectAndValidateOllamaModel: selectModel }), + ); + + await handleRunningOllamaSelection(null, "required/model", null, true, state); + + expect(selectModel.mock.calls[0]?.[2].lockedModel).toBe("required/model"); + expect(log).not.toHaveBeenCalledWith( + " Shared gateway route requires Ollama model 'required/model'.", + ); + expect(log).not.toHaveBeenCalledWith( + " To use a different model for this agent, rerun with an unused NEMOCLAW_GATEWAY_PORT.", + ); + log.mockRestore(); }); it("passes NEMOCLAW_MODEL as the interactive Ollama prompt default", async () => { diff --git a/src/lib/onboard/setup-nim-ollama.ts b/src/lib/onboard/setup-nim-ollama.ts index 466f1a26eb1..bea9b498b84 100644 --- a/src/lib/onboard/setup-nim-ollama.ts +++ b/src/lib/onboard/setup-nim-ollama.ts @@ -163,7 +163,14 @@ export function createSetupNimOllamaHandlers(deps: SetupNimOllamaDeps): { ): string | null { configureOllamaState(state); state.model = requestedModel || recoveredModel; - return state.assertRouteCompatible?.().requiredModel ?? null; + const requiredModel = state.assertRouteCompatible?.().requiredModel ?? null; + if (requiredModel && !deps.isNonInteractive()) { + console.log(` Shared gateway route requires Ollama model '${requiredModel}'.`); + console.log( + " To use a different model for this agent, rerun with an unused NEMOCLAW_GATEWAY_PORT.", + ); + } + return requiredModel; } function applyOllamaFallbackState(