Skip to content
Closed
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
26 changes: 13 additions & 13 deletions src/lib/local-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,25 @@ 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(getLocalProviderHealthEndpoint("ollama-local")).toBe(
"http://localhost:11434/api/tags",
"http://127.0.0.1:11434/api/tags",
);
expect(getLocalProviderLabel("ollama-local")).toBe("Local Ollama");
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(getLocalProviderHealthEndpoint("vllm-local")).toBe("http://localhost:8000/v1/models");
expect(getLocalProviderValidationBaseUrl("ollama-local")).toBe("http://127.0.0.1:11434/v1");
expect(getLocalProviderHealthEndpoint("vllm-local")).toBe("http://127.0.0.1:8000/v1/models");
expect(getLocalProviderLabel("vllm-local")).toBe("Local vLLM");
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 @@ -91,7 +91,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 @@ -108,7 +108,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("probes local provider health successfully", () => {
Expand All @@ -126,8 +126,8 @@ describe("local inference helpers", () => {
expect(result).toEqual({
ok: true,
providerLabel: "Local Ollama",
endpoint: "http://localhost:11434/api/tags",
detail: "Local Ollama is reachable on http://localhost:11434/api/tags.",
endpoint: "http://127.0.0.1:11434/api/tags",
detail: "Local Ollama is reachable on http://127.0.0.1:11434/api/tags.",
});
});

Expand All @@ -146,7 +146,7 @@ describe("local inference helpers", () => {
expect(result?.ok).toBe(false);
expect(result?.detail).toContain("Local Ollama is selected for inference");
expect(result?.detail).toContain("Start Ollama and retry");
expect(result?.detail).toContain("http://localhost:11434/api/tags");
expect(result?.detail).toContain("http://127.0.0.1:11434/api/tags");
});

it("returns null when provider health probing is not supported", () => {
Expand Down Expand Up @@ -263,7 +263,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 @@ -276,7 +276,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 @@ -54,9 +54,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 @@ -65,9 +65,9 @@ export function getLocalProviderValidationBaseUrl(provider: string): string | nu
export function getLocalProviderHealthEndpoint(provider: string): string | null {
switch (provider) {
case "vllm-local":
return "http://localhost:8000/v1/models";
return "http://127.0.0.1:8000/v1/models";
case "ollama-local":
return "http://localhost:11434/api/tags";
return "http://127.0.0.1:11434/api/tags";
default:
return null;
}
Expand Down Expand Up @@ -170,13 +170,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 @@ -198,13 +198,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 @@ -236,7 +236,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 @@ -275,7 +275,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 @@ -289,7 +289,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 @@ -2823,10 +2824,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 @@ -3352,7 +3353,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
40 changes: 20 additions & 20 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 @@ -677,9 +677,9 @@ credentials.ensureApiKey = async () => { process.env.NVIDIA_API_KEY = "nvapi-goo
runner.run = () => ({ status: 0 });
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 @@ -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