Skip to content
Merged
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
4 changes: 2 additions & 2 deletions ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"src/lib/cli/branding.ts": 87,
"src/lib/cli/nemoclaw-oclif-command.ts": 106,
"src/lib/cli/terminal-style.ts": 43,
"src/lib/core/json-types.ts": 36,
"src/lib/core/json-types.ts": 37,
"src/lib/core/ports.ts": 89,
"src/lib/core/shell-quote.ts": 28,
"src/lib/core/url-utils.ts": 30,
Expand Down Expand Up @@ -46,7 +46,7 @@
"src/lib/actions/sandbox/rebuild-pipeline.ts": 29,
"src/lib/actions/sandbox/snapshot.ts": 40,
"src/lib/actions/uninstall/run-plan.ts": 25,
"src/lib/inference/local.ts": 21,
"src/lib/inference/local.ts": 22,
"src/lib/inference/onboard-probes.ts": 21,
"src/lib/inference/vllm.ts": 21,
"src/lib/onboard.ts": 203,
Expand Down
20 changes: 8 additions & 12 deletions src/lib/inference/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CONTAINER_REACHABILITY_IMAGE } from "../adapters/http/container-curl-pr
import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args";
import type { CurlProbeOptions, CurlProbeResult } from "../adapters/http/probe";
import { runCurlProbe } from "../adapters/http/probe";
import { isObjectRecord } from "../core/json-types";
import { OLLAMA_PORT, OLLAMA_PROXY_PORT, VLLM_PORT } from "../core/ports";

import { retryUntil } from "../core/retry";
Expand Down Expand Up @@ -357,32 +358,27 @@ export function probeVllmModels(
}
}

// A 200 response on `/api/tags` alone is not enough to call Ollama healthy —
// a captive HTTP_PROXY, a stale listener, or a stub on the loopback port can
// all answer with arbitrary 2xx bodies that look healthy at the curl-status
// level. The authoritative signal is the Ollama wire format itself:
// `{ "models": [...] }`. An empty array is fine — that just means no models
// pulled yet — but a body that doesn't parse as JSON-with-array-`models` did
// not come from Ollama and the probe should not call it healthy. (#4275)
// A successful `/api/tags` response proves Ollama health only when its body
// contains a `models` array of objects. An empty array is valid. (#4275)
export function isValidOllamaTagsResponseBody(body: string): boolean {
if (!body) return false;
try {
const parsed = JSON.parse(body);
return parsed !== null && typeof parsed === "object" && Array.isArray(parsed.models);
return isObjectRecord(parsed) && Array.isArray(parsed.models) && parsed.models.every(isObjectRecord);
} catch {
return false;
}
}

function modelInventory(provider: string, body: string): string[] | null {
try {
const parsed = JSON.parse(body) as Record<string, unknown>;
const parsed = JSON.parse(body);
if (!isObjectRecord(parsed)) return null;
const entries = provider === "ollama-local" ? parsed.models : parsed.data;
if (!Array.isArray(entries)) return null;
return entries.flatMap((entry) => {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
const record = entry as Record<string, unknown>;
const values = provider === "ollama-local" ? [record.name, record.model] : [record.id];
if (!isObjectRecord(entry)) return [];
const values = provider === "ollama-local" ? [entry.name, entry.model] : [entry.id];
return values.filter((value): value is string => typeof value === "string" && value !== "");
});
} catch {
Expand Down
11 changes: 7 additions & 4 deletions src/lib/onboard/provider-host-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ describe("detectInferenceProviderHostState", () => {
expect(state.ollamaInstallMenu.entry?.label).toBe("Install Ollama (WSL Linux)");
});

it("does not treat a non-Ollama body from host.docker.internal as a live Windows daemon (#9348)", () => {
it.each([
["an HTML response", "<html>captive portal</html>"],
["a null model entry", '{"models":[null]}'],
["a primitive model entry", '{"models":[1]}'],
["a nested-array model entry", '{"models":[[]]}'],
])("does not treat %s as a live Windows daemon (#9348)", (_label, body) => {
const deps = buildDeps({
isWsl: vi.fn(() => true),
getContainerRuntime: vi.fn<DetectInferenceProviderHostStateDeps["getContainerRuntime"]>(
Expand All @@ -272,9 +277,7 @@ describe("detectInferenceProviderHostState", () => {
installedPath: "C:\\Users\\me\\AppData\\Local\\Programs\\Ollama\\ollama.exe",
loopbackOnly: false,
})),
dockerCapture: vi.fn<DetectInferenceProviderHostStateDeps["dockerCapture"]>(() =>
"<html>captive portal</html>",
),
dockerCapture: vi.fn<DetectInferenceProviderHostStateDeps["dockerCapture"]>(() => body),
});

const state = detectWithDeps(deps);
Expand Down
Loading