Skip to content

Commit ec8bd67

Browse files
harjothkharaclaude
andcommitted
fix(status): probe inference.local route for cloud providers in status/doctor
`status` and `doctor` reported inference healthy for cloud/managed providers by probing only the upstream provider endpoint, never the `inference.local` route the agent actually uses. When `inference.local` was broken inside the sandbox they still reported healthy (exit 0), contradicting `connect`. Widen the inference.local gateway-chain probe (added for local providers in #3265) to run for every provider. In doctor this removes the local-only gate in collectInferenceSubprobes; in status it drops the provider clause on the snapshot probe. A broken route now surfaces as `[fail] Provider health (gateway)` and flips doctor to fail (exit 1). Refs #6192 Signed-off-by: harjoth <harjoth.khara@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3a767fb commit ec8bd67

3 files changed

Lines changed: 77 additions & 18 deletions

File tree

src/lib/actions/sandbox/doctor-flow.test.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type RunSandboxDoctor = typeof import("./doctor")["runSandboxDoctor"];
1111
const requireDist = createRequire(import.meta.url);
1212
const doctorModulePath = "./doctor.js";
1313

14-
function createDoctorHarness(): {
14+
function createDoctorHarness(overrides: { provider?: string; gatewayChainOk?: boolean } = {}): {
1515
buildToolScopeChecksSpy: MockInstance;
1616
captureOpenShellSpy: MockInstance;
1717
captureHostCommandSpy: MockInstance;
@@ -29,6 +29,8 @@ function createDoctorHarness(): {
2929
resolveOpenShellSpy: MockInstance;
3030
runSandboxDoctor: RunSandboxDoctor;
3131
} {
32+
const provider = overrides.provider ?? "ollama-local";
33+
const gatewayChainOk = overrides.gatewayChainOk ?? false;
3234
delete require.cache[requireDist.resolve(doctorModulePath)];
3335

3436
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
@@ -56,7 +58,7 @@ function createDoctorHarness(): {
5658
name: "alpha",
5759
agent: "openclaw",
5860
model: "registry-model",
59-
provider: "ollama-local",
61+
provider,
6062
openshellDriver: "docker",
6163
gatewayName: "nemoclaw-19080",
6264
gatewayPort: 19080,
@@ -95,7 +97,7 @@ function createDoctorHarness(): {
9597
return { status: 0, output: "alpha Ready" };
9698
}
9799
if (argv[0] === "inference" && argv[1] === "get") {
98-
return { status: 0, output: "Provider: ollama-local\nModel: live-model\n" };
100+
return { status: 0, output: `Provider: ${provider}\nModel: live-model\n` };
99101
}
100102
return { status: 0, output: "" };
101103
});
@@ -118,9 +120,12 @@ function createDoctorHarness(): {
118120
const probeSandboxInferenceGatewayHealthSpy = vi
119121
.spyOn(processRecovery, "probeSandboxInferenceGatewayHealth")
120122
.mockResolvedValue({
121-
ok: false,
122-
endpoint: "http://127.0.0.1:19000/v1/chat/completions",
123-
detail: "gateway refused connection",
123+
ok: gatewayChainOk,
124+
endpoint: "https://inference.local/v1/models",
125+
httpStatus: gatewayChainOk ? 200 : 0,
126+
detail: gatewayChainOk
127+
? "Inference gateway responded HTTP 200 on https://inference.local/v1/models (full chain reachable)."
128+
: "Inference gateway unreachable on https://inference.local/v1/models from inside the sandbox.",
124129
});
125130
const loadAgentSpy = vi.spyOn(agentDefs, "loadAgent").mockReturnValue({
126131
name: "openclaw",
@@ -251,6 +256,59 @@ describe("runSandboxDoctor flow", () => {
251256
},
252257
);
253258

259+
it(
260+
"probes the inference.local route for cloud providers, not just local ones (#6192)",
261+
testTimeoutOptions(30_000),
262+
async () => {
263+
// Regression: doctor appended the `inference.local` gateway-chain subprobe
264+
// only for ollama-local/vllm-local. A cloud sandbox whose upstream endpoint
265+
// was reachable but whose in-sandbox inference.local route was broken
266+
// reported "healthy" (exit 0), contradicting `connect`.
267+
const harness = createDoctorHarness({ provider: "nvidia-prod", gatewayChainOk: false });
268+
269+
const report = await harness.runSandboxDoctor("alpha", ["--json"], { quietJson: true });
270+
271+
// Upstream probe stays green (negative control — we did not break it)...
272+
expect(report?.checks).toEqual(
273+
expect.arrayContaining([
274+
expect.objectContaining({ group: "Inference", label: "Provider health", status: "ok" }),
275+
]),
276+
);
277+
// ...but the real inference.local route is now probed and reported broken,
278+
// flipping the overall verdict to fail.
279+
expect(report?.checks).toEqual(
280+
expect.arrayContaining([
281+
expect.objectContaining({
282+
group: "Inference",
283+
label: "Provider health (gateway)",
284+
status: "fail",
285+
}),
286+
]),
287+
);
288+
expect(report?.status).toBe("fail");
289+
},
290+
);
291+
292+
it(
293+
"keeps cloud-provider doctor green when inference.local is reachable (#6192)",
294+
testTimeoutOptions(30_000),
295+
async () => {
296+
const harness = createDoctorHarness({ provider: "nvidia-prod", gatewayChainOk: true });
297+
298+
const report = await harness.runSandboxDoctor("alpha", ["--json"], { quietJson: true });
299+
300+
expect(report?.checks).toEqual(
301+
expect.arrayContaining([
302+
expect.objectContaining({
303+
group: "Inference",
304+
label: "Provider health (gateway)",
305+
status: "ok",
306+
}),
307+
]),
308+
);
309+
},
310+
);
311+
254312
it("rejects mutating --fix when JSON output was requested", async () => {
255313
const harness = createDoctorHarness();
256314

src/lib/actions/sandbox/doctor.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,6 @@ function inferenceRouteCheck(sandboxName: string, route: InferenceRoute): Doctor
326326
};
327327
}
328328

329-
function isLocalInferenceProvider(provider: string): boolean {
330-
return provider === "ollama-local" || provider === "vllm-local";
331-
}
332-
333329
function skippedInferenceGatewayProbe(): ProviderHealthStatus {
334330
return {
335331
ok: false,
@@ -343,11 +339,15 @@ function skippedInferenceGatewayProbe(): ProviderHealthStatus {
343339

344340
async function collectInferenceSubprobes(
345341
sandboxName: string,
346-
provider: string,
347342
sandboxReachable: boolean,
348343
existing: ProviderHealthStatus[],
349344
): Promise<ProviderHealthStatus[]> {
350-
if (!isLocalInferenceProvider(provider)) return existing;
345+
// #6192: probe the `inference.local` gateway chain for every provider, not
346+
// just local ones. `inference.local` is the route the agent actually uses
347+
// (openclaw gateway -> auth proxy -> backend) regardless of whether the
348+
// backend is a local runtime or a cloud/managed endpoint. Gating this to
349+
// local providers let cloud sandboxes report "healthy" off the upstream
350+
// probe while the real in-sandbox route was broken, contradicting `connect`.
351351
if (!sandboxReachable) return [...existing, skippedInferenceGatewayProbe()];
352352
const gateway = await probeSandboxInferenceGatewayHealth(sandboxName);
353353
if (!gateway) return existing;
@@ -385,7 +385,6 @@ async function collectInferenceChecks(
385385

386386
const subprobes = await collectInferenceSubprobes(
387387
sandboxName,
388-
route.provider,
389388
sandboxReachable,
390389
health.subprobes ?? [],
391390
);

src/lib/actions/sandbox/status-snapshot.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,13 @@ export async function collectSandboxStatusSnapshot(
220220
currentModel,
221221
opts.deps?.probeProviderHealthImpl,
222222
);
223-
if (
224-
inferenceHealth &&
225-
lookup.state === "present" &&
226-
(currentProvider === "ollama-local" || currentProvider === "vllm-local")
227-
) {
223+
// #6192: probe the `inference.local` gateway chain for every provider, not
224+
// just local ones. `inference.local` is the route the agent actually uses
225+
// (openclaw gateway -> auth proxy -> backend) regardless of whether the
226+
// backend is a local runtime or a cloud/managed endpoint. Gating this to
227+
// local providers let cloud sandboxes report "healthy" off the upstream
228+
// probe while the real in-sandbox route was broken.
229+
if (inferenceHealth && lookup.state === "present") {
228230
const gatewayChain = await probeSandboxInferenceGatewayHealth(sandboxName);
229231
if (gatewayChain) {
230232
const gatewaySubprobe: ProviderHealthStatus = {

0 commit comments

Comments
 (0)