Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,15 @@ It uses a 3-second connection timeout, a 5-second total timeout, and an 8-token
If the request reaches the time limit, NemoClaw reports the provider as `not probed` and leaves model health unverified instead of reporting it as unhealthy.
These checks are diagnostic only and do not override the authoritative `inference.local` result or determine the command exit status.

The `Inference (upstream)` check authenticates with the host credential that NemoClaw resolves for the provider, such as `NVIDIA_INFERENCE_API_KEY`.
The gateway stores the provider credential that the sandbox route uses.
The CLI cannot read the stored value back, so the two credentials can hold different secrets.
When the `inference.local` route has already served the inference request, an `unauthorized` result on `Inference (upstream)` describes the host credential.
NemoClaw then reports that check as `not probed` and names both credential sources.
An `Inference (upstream)` check that fails for another reason, such as `unreachable`, still reports its own state.
Local backend and auth proxy checks, such as `Inference (auth proxy)`, always report their own state and their own repair step.
`$$nemoclaw <name> doctor` sends no inference request, so it always reports the `Inference (upstream)` state that it measured.

Local providers add host-side backend diagnostics.
For Local Ollama, the command can also print an `Inference (auth proxy)` diagnostic when a proxy token is available.
Use these diagnostics to identify a failing auxiliary hop after checking the main `Inference` line.
Expand Down
34 changes: 32 additions & 2 deletions src/lib/actions/sandbox/inference-route-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,43 @@ export async function probeSandboxInferenceGatewayHealth(
};
}

/**
* The upstream probe authenticates with the host credential this command
* resolves. The gateway stores the provider credential the sandbox route uses
* and does not return its value, so the two can hold different secrets. Once
* the route has served an inference request, a provider rejection of the host
* credential reports nothing about the sandbox route. Local backend and auth
* proxy hops carry their own probeLabel and keep their own remediation.
*/
function unattributedUpstreamProbe(probe: ProviderHealthStatus): ProviderHealthStatus {
const { failureLabel: _failureLabel, ...rest } = probe;
return {
...rest,
ok: true,
probed: false,
detail:
`${probe.providerLabel} rejected the host credential this command resolved. The sandbox ` +
"route served an inference request with the provider credential stored in the gateway, so " +
"NemoClaw does not attribute this result to the sandbox route.",
Comment thread
apurvvkumaria marked this conversation as resolved.
};
}

function providerHealthDiagnostics(
providerHealth: ProviderHealthStatus | null,
routeServedRequest: boolean,
): ProviderHealthStatus[] {
if (!providerHealth) return [];
const { subprobes = [], ...primary } = providerHealth;
const labeledPrimary = primary.probeLabel ? primary : { ...primary, probeLabel: "upstream" };
return [labeledPrimary, ...subprobes];
return [labeledPrimary, ...subprobes].map((probe) =>
routeServedRequest &&
probe.probeLabel === "upstream" &&
probe.probed &&
!probe.ok &&
probe.failureLabel === "unauthorized"
? unattributedUpstreamProbe(probe)
: probe,
);
}

function classifyInferenceInvocationFailureLabel(
Expand Down Expand Up @@ -176,7 +206,7 @@ export function buildSandboxInferenceRouteHealth(
invocation: SandboxInferenceInvocationResult | null,
): ProviderHealthStatus {
const endpoint = gateway?.endpoint ?? "https://inference.local/v1/models";
const diagnostics = providerHealthDiagnostics(providerHealth);
const diagnostics = providerHealthDiagnostics(providerHealth, Boolean(invocation?.ok));
let routeHealth: ProviderHealthStatus;
if (gateway?.ok && invocation) {
routeHealth = buildInvokedRouteHealth(gateway, endpoint, invocation);
Expand Down
135 changes: 133 additions & 2 deletions src/lib/actions/sandbox/status-snapshot-inference-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ describe("collectSandboxStatusSnapshot inference route health", () => {
expect(snapshot.inferenceHealth?.failureLabel).toBeUndefined();
});

it("keeps an upstream subprobe failure out of the served-route verdict (#6846)", async () => {
it("keeps an unreachable upstream subprobe failure out of the served-route verdict (#6846)", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: true,
endpoint: "https://inference.local/v1/models",
Expand All @@ -385,7 +385,7 @@ describe("collectSandboxStatusSnapshot inference route health", () => {
providerLabel: "NVIDIA Endpoints",
endpoint: "https://integrate.api.nvidia.com/v1/chat/completions",
detail: "model invocation probe failed",
failureLabel: "unauthorized",
failureLabel: "unreachable",
};

const snapshot = await collectSandboxStatusSnapshot(
Expand Down Expand Up @@ -430,6 +430,137 @@ describe("collectSandboxStatusSnapshot inference route health", () => {
);
});

it("reports the upstream check as not probed after the provider rejected the host credential and the route served a request (#9595)", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: true,
endpoint: "https://inference.local/v1/models",
httpStatus: 200,
detail:
"Inference gateway responded HTTP 200 on https://inference.local/v1/models (full chain reachable).",
};
const providerHealth: ProviderHealthStatus = {
ok: false,
probed: true,
providerLabel: "NVIDIA Endpoints",
endpoint: "https://integrate.api.nvidia.com/v1/chat/completions",
detail: "model invocation probe rejected the credential",
failureLabel: "unauthorized",
};

const snapshot = await collectSandboxStatusSnapshot(
"alpha",
snapshotDeps(gateway, providerHealth),
);

expect(snapshot.inferenceHealth).toMatchObject({ ok: true });
const upstream = snapshot.inferenceHealth?.subprobes?.find(
(subprobe) => subprobe.probeLabel === "upstream",
);
expect(upstream).toMatchObject({ ok: true, probed: false });
expect(upstream?.failureLabel).toBeUndefined();
expect(upstream?.detail).toContain("rejected the host credential");
expect(upstream?.detail).toContain("provider credential stored in the gateway");
expect(upstream?.detail).toContain("does not attribute this result to the sandbox route");
});

it("keeps an unreachable upstream check after the route served a request (#9595)", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: true,
endpoint: "https://inference.local/v1/models",
httpStatus: 200,
detail:
"Inference gateway responded HTTP 200 on https://inference.local/v1/models (full chain reachable).",
};
const providerHealth: ProviderHealthStatus = {
ok: false,
probed: true,
providerLabel: "NVIDIA Endpoints",
endpoint: "https://integrate.api.nvidia.com/v1/chat/completions",
detail: "model invocation probe could not reach the endpoint",
failureLabel: "unreachable",
};

const snapshot = await collectSandboxStatusSnapshot(
"alpha",
snapshotDeps(gateway, providerHealth),
);

expect(snapshot.inferenceHealth?.subprobes).toContainEqual({
...providerHealth,
probeLabel: "upstream",
});
});

it("keeps an unauthorized auth proxy subprobe after the route served a request (#9595)", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: true,
endpoint: "https://inference.local/v1/models",
httpStatus: 200,
detail:
"Inference gateway responded HTTP 200 on https://inference.local/v1/models (full chain reachable).",
};
const authProxy: ProviderHealthStatus = {
ok: false,
probed: true,
providerLabel: "Local Ollama",
probeLabel: "auth proxy",
endpoint: "http://127.0.0.1:11435/api/tags",
detail:
"Ollama auth proxy returned 401 — the persisted token is no longer accepted. " +
"Re-run `nemoclaw onboard` (Ollama path) to rotate the proxy token.",
failureLabel: "unauthorized",
};
const providerHealth: ProviderHealthStatus = {
ok: true,
probed: true,
providerLabel: "Local Ollama",
probeLabel: "ollama backend",
endpoint: "http://127.0.0.1:11434/api/tags",
detail: "Local Ollama is reachable.",
subprobes: [authProxy],
};

const snapshot = await collectSandboxStatusSnapshot(
"alpha",
snapshotDeps(gateway, providerHealth),
);

expect(snapshot.inferenceHealth?.subprobes).toContainEqual(authProxy);
});

it("keeps an unauthorized upstream check when the route did not serve a request (#9595)", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: true,
endpoint: "https://inference.local/v1/models",
httpStatus: 401,
detail:
"Inference gateway responded HTTP 401 on https://inference.local/v1/models (full chain reachable).",
};
const providerHealth: ProviderHealthStatus = {
ok: false,
probed: true,
providerLabel: "NVIDIA Endpoints",
endpoint: "https://integrate.api.nvidia.com/v1/chat/completions",
detail: "model invocation probe rejected the credential",
failureLabel: "unauthorized",
};

const snapshot = await collectSandboxStatusSnapshot(
"alpha",
snapshotDeps(gateway, providerHealth, {
ok: false,
detail: "sandbox inference invocation probe returned HTTP 401",
httpStatus: 401,
}),
);

expect(snapshot.inferenceHealth).toMatchObject({ ok: false, failureLabel: "unauthorized" });
expect(snapshot.inferenceHealth?.subprobes).toContainEqual({
...providerHealth,
probeLabel: "upstream",
});
});

it("does not send an agent request when the route probe already failed", async () => {
const gateway: SandboxInferenceRouteHealth = {
ok: false,
Expand Down
14 changes: 14 additions & 0 deletions src/lib/inference/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,20 @@ describe("inference health", () => {
expect(result?.failureLabel).toBe("unauthorized");
});

it("names the host credential environment variable when the provider rejects the request (#9595)", () => {
const result = probeRemoteProviderHealth("nvidia-prod", {
model: "meta/llama-3.3-70b-instruct",
getCredentialImpl: () => "nvapi-stale",
runCurlProbeImpl: () => httpUnauthorized(),
});

expect(result?.failureLabel).toBe("unauthorized");
expect(result?.detail).toContain("rejected the");
expect(result?.detail).toContain("host credential in NVIDIA_INFERENCE_API_KEY");
expect(result?.detail).toContain("not the provider credential stored in the gateway");
expect(result?.detail).not.toContain("Check your network connection");
});

it("reports unhealthy on a non-auth HTTP failure", () => {
const result = probeRemoteProviderHealth("openai-api", {
model: "gpt-4o-mini",
Expand Down
8 changes: 8 additions & 0 deletions src/lib/inference/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ function buildInvocationProbeDetail(
`as proof that the model ran. (${result.message})`
);
}
if (classifyHealthProbeFailureLabel(result) === "unauthorized") {
return (
`${endpoint} rejected the ${route} request. ` +
`This probe authenticates with the host credential in ${credentialEnv}, not the provider ` +
`credential stored in the gateway. Check ${credentialEnv} where you run this command. ` +
`(${result.message})`
);
}
return (
`${route} at ${endpoint} did not succeed. ` +
`Check your network connection or ${credentialEnv}. (${result.message})`
Expand Down
Loading