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
9 changes: 9 additions & 0 deletions docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 43 additions & 1 deletion src/lib/onboard/setup-nim-ollama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,6 +26,10 @@ function makeState(): SetupNimSelectionState {

type Deps = Parameters<typeof createSetupNimOllamaHandlers>[0];

afterEach(() => {
vi.restoreAllMocks();
});

function makeDeps(overrides: Partial<Deps> = {}): Deps {
return {
OLLAMA_PORT: 11434,
Expand Down Expand Up @@ -64,6 +68,7 @@ function makeDeps(overrides: Partial<Deps> = {}): 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)}`);
Expand All @@ -75,6 +80,7 @@ describe("createSetupNimOllamaHandlers", () => {
};
const { handleRunningOllamaSelection } = createSetupNimOllamaHandlers(
makeDeps({
isNonInteractive: () => false,
ensureOllamaLoopbackSystemdOverride: () => {
events.push("systemd");
return "unchanged";
Expand All @@ -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<Deps["selectAndValidateOllamaModel"]>(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 () => {
Expand Down
9 changes: 8 additions & 1 deletion src/lib/onboard/setup-nim-ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading