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
26 changes: 13 additions & 13 deletions src/lib/local-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,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")).toEqual(
["curl", "-sf", "http://localhost:11434/api/tags"],
["curl", "-sf", "http://127.0.0.1:11434/api/tags"],
);
});

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")).toEqual(
["curl", "-sf", "http://localhost:8000/v1/models"],
["curl", "-sf", "http://127.0.0.1:8000/v1/models"],
);
expect(getLocalProviderContainerReachabilityCheck("vllm-local")).toEqual([
"docker", "run", "--rm",
Expand Down Expand Up @@ -99,7 +99,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 @@ -117,7 +117,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 @@ -135,8 +135,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 @@ -155,7 +155,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 @@ -267,7 +267,7 @@ describe("local inference helpers", () => {
it("builds a background warmup command for ollama models", () => {
const command = getOllamaWarmupCommand("nemotron-3-nano:30b");
expect(command).toEqual(expect.arrayContaining(["bash", "-c"]));
expect(command[2]).toMatch(/^nohup curl -s http:\/\/localhost:11434\/api\/generate /);
expect(command[2]).toMatch(/^nohup curl -s http:\/\/127.0.0.1:11434\/api\/generate /);
expect(command[2]).toMatch(/"model":"nemotron-3-nano:30b"/);
expect(command[2]).toMatch(/"keep_alive":"15m"/);
});
Expand All @@ -288,7 +288,7 @@ describe("local inference helpers", () => {
expect(command).toContain("-sS");
expect(command).toContain("--max-time");
expect(command).toContain("120");
expect(command).toContain("http://localhost:11434/api/generate");
expect(command).toContain("http://127.0.0.1:11434/api/generate");
const payload = command[command.length - 1];
expect(payload).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 @@ -63,9 +63,9 @@ export function getLocalProviderBaseUrl(provider: string): string | null {
export function getLocalProviderValidationBaseUrl(provider: string): string | null {
switch (provider) {
case "vllm-local":
return `http://localhost:${VLLM_PORT}/v1`;
return `http://127.0.0.1:${VLLM_PORT}/v1`;
case "ollama-local":
return `http://localhost:${OLLAMA_PORT}/v1`;
return `http://127.0.0.1:${OLLAMA_PORT}/v1`;
default:
return null;
}
Expand All @@ -74,9 +74,9 @@ export function getLocalProviderValidationBaseUrl(provider: string): string | nu
export function getLocalProviderHealthEndpoint(provider: string): string | null {
switch (provider) {
case "vllm-local":
return `http://localhost:${VLLM_PORT}/v1/models`;
return `http://127.0.0.1:${VLLM_PORT}/v1/models`;
case "ollama-local":
return `http://localhost:${OLLAMA_PORT}/api/tags`;
return `http://127.0.0.1:${OLLAMA_PORT}/api/tags`;
default:
return null;
}
Expand Down Expand Up @@ -192,13 +192,13 @@ export function validateLocalProvider(
case "vllm-local":
return {
ok: false,
message: `Local vLLM was selected, but nothing is responding on http://localhost:${VLLM_PORT}.`,
message: `Local vLLM was selected, but nothing is responding on http://127.0.0.1:${VLLM_PORT}.`,
};
case "ollama-local":
return {
ok: false,
message:
`Local Ollama was selected, but nothing is responding on http://localhost:${OLLAMA_PORT}.`,
`Local Ollama was selected, but nothing is responding on http://127.0.0.1:${OLLAMA_PORT}.`,
};
default:
return { ok: false, message: "The selected local inference provider is unavailable." };
Expand All @@ -220,13 +220,13 @@ export function validateLocalProvider(
return {
ok: false,
message:
`Local vLLM is responding on localhost, but containers cannot reach http://host.openshell.internal:${VLLM_PORT}. 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:${VLLM_PORT}. 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 the auth proxy at http://host.openshell.internal:${OLLAMA_CONTAINER_PORT}. Ensure the Ollama auth proxy is running.`,
`Local Ollama is responding on 127.0.0.1, but containers cannot reach the auth proxy at http://host.openshell.internal:${OLLAMA_CONTAINER_PORT}. Ensure the Ollama auth proxy is running.`,
};
default:
return {
Expand Down Expand Up @@ -259,7 +259,7 @@ export function parseOllamaTags(output: unknown): string[] {

export function getOllamaModelOptions(runCaptureImpl?: RunCaptureFn): string[] {
const capture = runCaptureImpl ?? runCapture;
const tagsOutput = capture(["curl", "-sf", `http://localhost:${OLLAMA_PORT}/api/tags`], {
const tagsOutput = capture(["curl", "-sf", `http://127.0.0.1:${OLLAMA_PORT}/api/tags`], {
ignoreError: true,
});
const tagsParsed = parseOllamaTags(tagsOutput);
Expand Down Expand Up @@ -304,7 +304,7 @@ export function getOllamaWarmupCommand(model: string, keepAlive = "15m"): string
// names is not feasible. This is the one intentional bash -c exception.
return [
"bash", "-c",
`nohup curl -s http://localhost:${OLLAMA_PORT}/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`,
`nohup curl -s http://127.0.0.1:${OLLAMA_PORT}/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`,
];
}

Expand All @@ -322,7 +322,7 @@ export function getOllamaProbeCommand(
return [
"curl", "-sS",
"--max-time", String(timeoutSeconds),
`http://localhost:${OLLAMA_PORT}/api/generate`,
`http://127.0.0.1:${OLLAMA_PORT}/api/generate`,
"-H", "Content-Type: application/json",
"-d", payload,
];
Expand Down
8 changes: 4 additions & 4 deletions src/lib/nim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe("nim", () => {
const runCapture = vi.fn((cmd: string | string[]) => {
if (!Array.isArray(cmd)) throw new Error("expected argv array");
if (cmd[0] === "docker" && cmd.includes("inspect")) return "running";
if (cmd[0] === "curl" && hasArg(cmd, "http://localhost:9000/v1/models")) return '{"data":[]}';
if (cmd[0] === "curl" && hasArg(cmd, "http://127.0.0.1:9000/v1/models")) return '{"data":[]}';
return "";
});
const { nimModule, restore } = loadNimWithMockedRunner(runCapture);
Expand All @@ -202,7 +202,7 @@ describe("nim", () => {
state: "running",
});
expect(commands.some((c) => c[0] === "docker" && c.includes("port"))).toBe(false);
expect(commands.some((c) => c.includes("http://localhost:9000/v1/models"))).toBe(
expect(commands.some((c) => c.includes("http://127.0.0.1:9000/v1/models"))).toBe(
true,
);
} finally {
Expand All @@ -216,7 +216,7 @@ describe("nim", () => {
if (!Array.isArray(cmd)) throw new Error("expected argv array");
if (cmd[0] === "docker" && cmd.includes("inspect")) return "running";
if (cmd[0] === "docker" && cmd.includes("port")) return mapping;
if (cmd[0] === "curl" && hasArg(cmd, "http://localhost:9000/v1/models")) return '{"data":[]}';
if (cmd[0] === "curl" && hasArg(cmd, "http://127.0.0.1:9000/v1/models")) return '{"data":[]}';
return "";
});
const { nimModule, restore } = loadNimWithMockedRunner(runCapture);
Expand All @@ -238,7 +238,7 @@ describe("nim", () => {
if (!Array.isArray(cmd)) throw new Error("expected argv array");
if (cmd[0] === "docker" && cmd.includes("inspect")) return "running";
if (cmd[0] === "docker" && cmd.includes("port")) return "";
if (cmd[0] === "curl" && hasArg(cmd, "http://localhost:8000/v1/models")) return '{"data":[]}';
if (cmd[0] === "curl" && hasArg(cmd, "http://127.0.0.1:8000/v1/models")) return '{"data":[]}';
return "";
});
const { nimModule, restore } = loadNimWithMockedRunner(runCapture);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/nim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export function waitForNimHealth(port = VLLM_PORT, timeout = 300): boolean {

while ((Date.now() - start) / 1000 < timeout) {
try {
const result = runCapture(["curl", "-sf", `http://localhost:${hostPort}/v1/models`], {
const result = runCapture(["curl", "-sf", `http://127.0.0.1:${hostPort}/v1/models`], {
ignoreError: true,
});
if (result) {
Expand Down Expand Up @@ -272,7 +272,7 @@ export function nimStatusByName(name: string, port?: number): NimStatus {
resolvedHostPort = m ? Number(m[1]) : VLLM_PORT;
}
const health = runCapture(
["curl", "-sf", `http://localhost:${resolvedHostPort}/v1/models`],
["curl", "-sf", `http://127.0.0.1:${resolvedHostPort}/v1/models`],
{ ignoreError: true },
);
healthy = !!health;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3243,12 +3243,12 @@ async function setupNim(gpu) {
// Detect local inference options
const hasOllama = !!runCapture("command -v ollama", { ignoreError: true });
const ollamaRunning = !!runCapture(
`curl -sf http://localhost:${OLLAMA_PORT}/api/tags 2>/dev/null`,
`curl -sf http://127.0.0.1:${OLLAMA_PORT}/api/tags 2>/dev/null`,
{
ignoreError: true,
},
);
const vllmRunning = !!runCapture(`curl -sf http://localhost:${VLLM_PORT}/v1/models 2>/dev/null`, {
const vllmRunning = !!runCapture(`curl -sf http://127.0.0.1:${VLLM_PORT}/v1/models 2>/dev/null`, {
ignoreError: true,
});
const requestedProvider = isNonInteractive() ? getNonInteractiveProvider() : null;
Expand Down Expand Up @@ -3821,7 +3821,7 @@ async function setupNim(gpu) {
endpointUrl = getLocalProviderBaseUrl(provider);
// Query vLLM for the actual model ID
const vllmModelsRaw = runCapture(
`curl -sf http://localhost:${VLLM_PORT}/v1/models 2>/dev/null`,
`curl -sf http://127.0.0.1:${VLLM_PORT}/v1/models 2>/dev/null`,
{
ignoreError: true,
},
Expand Down
2 changes: 1 addition & 1 deletion test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1674,7 +1674,7 @@ describe("CLI dispatch", () => {
expect(r.out).toContain("Inference:");
expect(r.out).toContain("unreachable");
expect(r.out).toContain("Start Ollama and retry");
expect(r.out).toContain("http://localhost:11434/api/tags");
expect(r.out).toContain("http://127.0.0.1:11434/api/tags");
});

it("does not treat a different connected gateway as a healthy nemoclaw gateway", () => {
Expand Down
20 changes: 10 additions & 10 deletions test/e2e/test-gpu-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ if [ "${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}" != "1" ]; then
fi

# Verify port 11434 is free (onboard needs to start Ollama on 127.0.0.1:11434)
if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
info "WARNING: Something is already listening on port 11434."
info "Onboard may not be able to start Ollama."
info "On ephemeral runners this should not happen."
Expand Down Expand Up @@ -189,7 +189,7 @@ fi
# If the Ollama installer started a system service, stop it so onboard
# can start Ollama with OLLAMA_HOST=0.0.0.0:11434 (required for containers).
# This needs the ollama process to be owned by our user, or systemctl access.
if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
info "Ollama service is running — attempting to stop for clean onboard..."
# Try systemctl first (works if user has permissions)
systemctl --user stop ollama 2>/dev/null || true
Expand All @@ -198,7 +198,7 @@ if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
pkill -f "ollama serve" 2>/dev/null || true
sleep 2

if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
info "Could not stop existing Ollama — onboard will use it as-is"
else
pass "Existing Ollama stopped — port 11434 is free for onboard"
Expand Down Expand Up @@ -288,8 +288,8 @@ else
fi

# 4d: Ollama is running and reachable
if curl -sf http://localhost:11434/api/tags >/dev/null 2>&1; then
pass "Ollama running on localhost:11434 (started by onboard)"
if curl -sf http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
pass "Ollama running on 127.0.0.1:11434 (started by onboard)"
else
fail "Ollama not running — onboard should have started it"
fi
Expand Down Expand Up @@ -415,7 +415,7 @@ section "Phase 5: Local inference through sandbox"
CONFIGURED_MODEL="${NEMOCLAW_MODEL:-}"
if [ -n "$CONFIGURED_MODEL" ]; then
# Verify the expected model is actually available in Ollama
if curl -sf http://localhost:11434/api/tags 2>/dev/null \
if curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null \
| python3 -c "import json,sys; m=[x['name'] for x in json.load(sys.stdin).get('models',[])]; sys.exit(0 if '$CONFIGURED_MODEL' in m or any('$CONFIGURED_MODEL' in x for x in m) else 1)" 2>/dev/null; then
info "Using NEMOCLAW_MODEL: $CONFIGURED_MODEL (confirmed in Ollama)"
else
Expand All @@ -424,7 +424,7 @@ if [ -n "$CONFIGURED_MODEL" ]; then
fi
fi
if [ -z "$CONFIGURED_MODEL" ]; then
CONFIGURED_MODEL=$(curl -sf http://localhost:11434/api/tags 2>/dev/null \
CONFIGURED_MODEL=$(curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null \
| python3 -c "import json,sys; m=json.load(sys.stdin).get('models',[]); print(m[0]['name'] if m else '')" 2>/dev/null || echo "")
if [ -n "$CONFIGURED_MODEL" ]; then
info "Auto-detected Ollama model: $CONFIGURED_MODEL"
Expand All @@ -434,9 +434,9 @@ if [ -z "$CONFIGURED_MODEL" ]; then
fi

# 5a: Direct Ollama inference (host-side, OpenAI-compatible)
info "[LOCAL] Direct Ollama test → localhost:11434/v1/chat/completions..."
info "[LOCAL] Direct Ollama test → 127.0.0.1:11434/v1/chat/completions..."
direct_response=$(curl -s --max-time 120 \
-X POST http://localhost:11434/v1/chat/completions \
-X POST http://127.0.0.1:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$CONFIGURED_MODEL\",
Expand Down Expand Up @@ -550,7 +550,7 @@ echo " What this tested (real user flow):"
echo " - GPU detection (nvidia-smi)"
echo " - Ollama binary install"
echo " - install.sh --non-interactive with NEMOCLAW_PROVIDER=ollama"
echo " - Onboard: starts Ollama on localhost, starts auth proxy, pulls model, creates sandbox"
echo " - Onboard: starts Ollama on 127.0.0.1, starts auth proxy, pulls model, creates sandbox"
echo " - Auth proxy: token persistence, auth reject/accept, container reachability, recovery"
echo " - Local inference: direct + sandbox → gateway → auth proxy → Ollama on GPU"
echo " - Destroy + uninstall --delete-models"
Expand Down
Loading
Loading