Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
16 changes: 8 additions & 8 deletions src/lib/local-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ describe("local inference helpers", () => {
});

it("returns the expected validation URL for vllm-local", () => {
expect(getLocalProviderValidationBaseUrl("vllm-local")).toBe("http://localhost:8000/v1");
expect(getLocalProviderValidationBaseUrl("vllm-local")).toBe("http://127.0.0.1:8000/v1");
});

it("returns the expected health check command for ollama-local", () => {
expect(getLocalProviderHealthCheck("ollama-local")).toBe(
"curl -sf http://localhost:11434/api/tags 2>/dev/null",
"curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null",
);
});

it("returns the expected validation and health check commands for vllm-local", () => {
expect(getLocalProviderValidationBaseUrl("ollama-local")).toBe("http://localhost:11434/v1");
expect(getLocalProviderValidationBaseUrl("ollama-local")).toBe("http://127.0.0.1:11434/v1");
expect(getLocalProviderHealthCheck("vllm-local")).toBe(
"curl -sf http://localhost:8000/v1/models 2>/dev/null",
"curl -sf http://127.0.0.1:8000/v1/models 2>/dev/null",
);
expect(getLocalProviderContainerReachabilityCheck("vllm-local")).toBe(
`docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:8000/v1/models 2>/dev/null`,
Expand All @@ -80,7 +80,7 @@ describe("local inference helpers", () => {
it("returns a clear error when ollama-local is unavailable", () => {
const result = validateLocalProvider("ollama-local", () => "");
expect(result.ok).toBe(false);
expect(result.message).toMatch(/http:\/\/localhost:11434/);
expect(result.message).toMatch(/http:\/\/127.0.0.1:11434/);
});

it("returns a clear error when ollama-local is not reachable from containers", () => {
Expand All @@ -97,7 +97,7 @@ describe("local inference helpers", () => {
it("returns a clear error when vllm-local is unavailable", () => {
const result = validateLocalProvider("vllm-local", () => "");
expect(result.ok).toBe(false);
expect(result.message).toMatch(/http:\/\/localhost:8000/);
expect(result.message).toMatch(/http:\/\/127.0.0.1:8000/);
});

it("returns a clear error when vllm-local is not reachable from containers", () => {
Expand Down Expand Up @@ -210,7 +210,7 @@ describe("local inference helpers", () => {

it("builds a background warmup command for ollama models", () => {
const command = getOllamaWarmupCommand("nemotron-3-nano:30b");
expect(command).toMatch(/^nohup curl -s http:\/\/localhost:11434\/api\/generate /);
expect(command).toMatch(/^nohup curl -s http:\/\/127.0.0.1:11434\/api\/generate /);
expect(command).toMatch(/"model":"nemotron-3-nano:30b"/);
expect(command).toMatch(/"keep_alive":"15m"/);
});
Expand All @@ -223,7 +223,7 @@ describe("local inference helpers", () => {

it("builds a foreground probe command for ollama models", () => {
const command = getOllamaProbeCommand("nemotron-3-nano:30b");
expect(command).toMatch(/^curl -sS --max-time 120 http:\/\/localhost:11434\/api\/generate /);
expect(command).toMatch(/^curl -sS --max-time 120 http:\/\/127.0.0.1:11434\/api\/generate /);
expect(command).toMatch(/"model":"nemotron-3-nano:30b"/);
});

Expand Down
22 changes: 11 additions & 11 deletions src/lib/local-inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export function getLocalProviderBaseUrl(provider: string): string | null {
export function getLocalProviderValidationBaseUrl(provider: string): string | null {
switch (provider) {
case "vllm-local":
return "http://localhost:8000/v1";
return "http://127.0.0.1:8000/v1";
case "ollama-local":
return "http://localhost:11434/v1";
return "http://127.0.0.1:11434/v1";
default:
return null;
}
Expand All @@ -51,9 +51,9 @@ export function getLocalProviderValidationBaseUrl(provider: string): string | nu
export function getLocalProviderHealthCheck(provider: string): string | null {
switch (provider) {
case "vllm-local":
return "curl -sf http://localhost:8000/v1/models 2>/dev/null";
return "curl -sf http://127.0.0.1:8000/v1/models 2>/dev/null";
case "ollama-local":
return "curl -sf http://localhost:11434/api/tags 2>/dev/null";
return "curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null";
default:
return null;
}
Expand Down Expand Up @@ -85,13 +85,13 @@ export function validateLocalProvider(
case "vllm-local":
return {
ok: false,
message: "Local vLLM was selected, but nothing is responding on http://localhost:8000.",
message: "Local vLLM was selected, but nothing is responding on http://127.0.0.1:8000.",
};
case "ollama-local":
return {
ok: false,
message:
"Local Ollama was selected, but nothing is responding on http://localhost:11434.",
"Local Ollama was selected, but nothing is responding on http://127.0.0.1:11434.",
};
default:
return { ok: false, message: "The selected local inference provider is unavailable." };
Expand All @@ -113,13 +113,13 @@ export function validateLocalProvider(
return {
ok: false,
message:
"Local vLLM is responding on localhost, but containers cannot reach http://host.openshell.internal:8000. Ensure the server is reachable from containers, not only from the host shell.",
"Local vLLM is responding on 127.0.0.1, but containers cannot reach http://host.openshell.internal:8000. Ensure the server is reachable from containers, not only from the host shell.",
};
case "ollama-local":
return {
ok: false,
message:
"Local Ollama is responding on localhost, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.",
"Local Ollama is responding on 127.0.0.1, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.",
};
default:
return {
Expand Down Expand Up @@ -151,7 +151,7 @@ export function parseOllamaTags(output: unknown): string[] {
}

export function getOllamaModelOptions(runCapture: RunCaptureFn): string[] {
const tagsOutput = runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", {
const tagsOutput = runCapture("curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null", {
ignoreError: true,
});
const tagsParsed = parseOllamaTags(tagsOutput);
Expand Down Expand Up @@ -190,7 +190,7 @@ export function getOllamaWarmupCommand(model: string, keepAlive = "15m"): string
stream: false,
keep_alive: keepAlive,
});
return `nohup curl -s http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`;
return `nohup curl -s http://127.0.0.1:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`;
}

export function getOllamaProbeCommand(
Expand All @@ -204,7 +204,7 @@ export function getOllamaProbeCommand(
stream: false,
keep_alive: keepAlive,
});
return `curl -sS --max-time ${timeoutSeconds} http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} 2>/dev/null`;
return `curl -sS --max-time ${timeoutSeconds} http://127.0.0.1:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} 2>/dev/null`;
}

export function validateOllamaModel(
Expand Down
7 changes: 4 additions & 3 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
getDefaultOllamaModel,
getBootstrapOllamaModelOptions,
getLocalProviderBaseUrl,
getLocalProviderHealthCheck,
getLocalProviderValidationBaseUrl,
getOllamaModelOptions,
getOllamaWarmupCommand,
Expand Down Expand Up @@ -2640,10 +2641,10 @@ async function setupNim(gpu) {

// Detect local inference options
const hasOllama = !!runCapture("command -v ollama", { ignoreError: true });
const ollamaRunning = !!runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", {
const ollamaRunning = !!runCapture(getLocalProviderHealthCheck("ollama-local"), {
ignoreError: true,
});
const vllmRunning = !!runCapture("curl -sf http://localhost:8000/v1/models 2>/dev/null", {
const vllmRunning = !!runCapture(getLocalProviderHealthCheck("vllm-local"), {
ignoreError: true,
});
const requestedProvider = isNonInteractive() ? getNonInteractiveProvider() : null;
Expand Down Expand Up @@ -3169,7 +3170,7 @@ async function setupNim(gpu) {
credentialEnv = "OPENAI_API_KEY";
endpointUrl = getLocalProviderBaseUrl(provider);
// Query vLLM for the actual model ID
const vllmModelsRaw = runCapture("curl -sf http://localhost:8000/v1/models 2>/dev/null", {
const vllmModelsRaw = runCapture(getLocalProviderHealthCheck("vllm-local"), {
ignoreError: true,
});
try {
Expand Down
18 changes: 18 additions & 0 deletions src/lib/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import {
planHostRemediation,
} from "../../dist/lib/preflight";

async function canBindLoopback(): Promise<boolean> {
const net = require("node:net");
const srv = net.createServer();

return await new Promise((resolve) => {
srv.once("error", () => resolve(false));
srv.listen(0, "127.0.0.1", () => {
srv.close(() => resolve(true));
});
});
}

describe("checkPortAvailable", () => {
it("falls through to the probe when lsof output is empty", async () => {
let probedPort: number | null = null;
Expand Down Expand Up @@ -135,6 +147,9 @@ describe("probePortAvailability", () => {
});

it("detects EADDRINUSE on an occupied port (real net probe)", async () => {
if (!(await canBindLoopback())) {
return;
}
// Start a server on a random port, then probe it
const net = require("node:net");
const srv = net.createServer();
Expand Down Expand Up @@ -171,6 +186,9 @@ describe("checkPortAvailable — real probe fallback", () => {
});

it("detects a real occupied port", async () => {
if (!(await canBindLoopback())) {
return;
}
const net = require("node:net");
const srv = net.createServer();
await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve));
Expand Down
36 changes: 18 additions & 18 deletions test/onboard-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ credentials.prompt = async (message) => {
credentials.ensureApiKey = async () => {};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "/usr/bin/ollama";
if (command.includes("localhost:11434/api/tags")) return JSON.stringify({ models: [{ name: "nemotron-3-nano:30b" }] });
if (command.includes("127.0.0.1:11434/api/tags")) return JSON.stringify({ models: [{ name: "nemotron-3-nano:30b" }] });
if (command.includes("ollama list")) return "nemotron-3-nano:30b abc 24 GB now\\nqwen3:32b def 20 GB now";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
return "";
};
registry.updateSandbox = (_name, update) => updates.push(update);
Expand Down Expand Up @@ -301,8 +301,8 @@ credentials.prompt = async (message) => {
credentials.ensureApiKey = async () => { process.env.NVIDIA_API_KEY = "nvapi-test"; };
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "";
if (command.includes("localhost:11434/api/tags")) return "";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:11434/api/tags")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
return "";
};

Expand Down Expand Up @@ -394,8 +394,8 @@ credentials.prompt = async (message) => {
credentials.ensureApiKey = async () => { process.env.NVIDIA_API_KEY = "nvapi-test"; };
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "";
if (command.includes("localhost:11434/api/tags")) return "";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:11434/api/tags")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
return "";
};

Expand Down Expand Up @@ -536,7 +536,7 @@ const { setupNim } = require(${onboardPath});
assert.ok(payload.lines.some((line) => line.includes("Chat Completions API available")));
});

it("warms and validates Ollama via localhost before moving on", () => {
it("warms and validates Ollama via 127.0.0.1 before moving on", () => {
const repoRoot = path.join(import.meta.dirname, "..");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-ollama-validation-"));
const fakeBin = path.join(tmpDir, "bin");
Expand Down Expand Up @@ -583,9 +583,9 @@ runner.run = (command, opts = {}) => {
};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "/usr/bin/ollama";
if (command.includes("localhost:11434/api/tags")) return JSON.stringify({ models: [{ name: "nemotron-3-nano:30b" }] });
if (command.includes("127.0.0.1:11434/api/tags")) return JSON.stringify({ models: [{ name: "nemotron-3-nano:30b" }] });
if (command.includes("ollama list")) return "nemotron-3-nano:30b abc 24 GB now";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
if (command.includes("api/generate")) return '{"response":"hello"}';
return "";
};
Expand Down Expand Up @@ -630,7 +630,7 @@ const { setupNim } = require(${onboardPath});
payload.lines.some((line) => line.includes("Loading Ollama model: nemotron-3-nano:30b")),
);
assert.ok(
payload.commands.some((command) => command.includes("http://localhost:11434/api/generate")),
payload.commands.some((command) => command.includes("http://127.0.0.1:11434/api/generate")),
);
});

Expand Down Expand Up @@ -777,9 +777,9 @@ credentials.prompt = async (message) => {
};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "/usr/bin/ollama";
if (command.includes("localhost:11434/api/tags")) return JSON.stringify({ models: [] });
if (command.includes("127.0.0.1:11434/api/tags")) return JSON.stringify({ models: [] });
if (command.includes("ollama list")) return "";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
if (command.includes("api/generate")) return '{"response":"hello"}';
return "";
};
Expand Down Expand Up @@ -884,9 +884,9 @@ credentials.prompt = async (message) => {
};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "/usr/bin/ollama";
if (command.includes("localhost:11434/api/tags")) return JSON.stringify({ models: [] });
if (command.includes("127.0.0.1:11434/api/tags")) return JSON.stringify({ models: [] });
if (command.includes("ollama list")) return "";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
if (command.includes("api/generate")) return '{"response":"hello"}';
return "";
};
Expand Down Expand Up @@ -2671,8 +2671,8 @@ credentials.prompt = async (message) => {
credentials.ensureApiKey = async () => {};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "";
if (command.includes("localhost:11434")) return "";
if (command.includes("localhost:8000/v1/models")) return JSON.stringify({ data: [{ id: "meta-llama/Llama-3.3-70B-Instruct" }] });
if (command.includes("127.0.0.1:11434")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return JSON.stringify({ data: [{ id: "meta-llama/Llama-3.3-70B-Instruct" }] });
return "";
};

Expand Down Expand Up @@ -2780,8 +2780,8 @@ credentials.prompt = async (message) => {
credentials.ensureApiKey = async () => {};
runner.runCapture = (command) => {
if (command.includes("command -v ollama")) return "";
if (command.includes("localhost:11434")) return "";
if (command.includes("localhost:8000/v1/models")) return "";
if (command.includes("127.0.0.1:11434")) return "";
if (command.includes("127.0.0.1:8000/v1/models")) return "";
return "";
};

Expand Down
Loading