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
15 changes: 10 additions & 5 deletions src/lib/onboard/inference-selection-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { probeAnthropicEndpoint, probeOpenAiLikeEndpoint } =

import { shouldForceCompletionsApi } from "../validation";
import { getProbeRecovery } from "../validation-recovery";
import { summarizeProbeForDisplay } from "./probe-diagnostics";

export type EndpointValidationResult =
| { ok: true; api: string | null; retry?: undefined }
Expand Down Expand Up @@ -80,8 +81,12 @@ export interface InferenceSelectionValidationHelpers {
export function createInferenceSelectionValidationHelpers(
deps: InferenceSelectionValidationDeps,
): InferenceSelectionValidationHelpers {
function printValidationFailure(label: string): void {
function printValidationFailure(
label: string,
probe?: { failures?: unknown[]; message?: unknown },
): void {
console.error(` ${label} endpoint validation failed.`);
if (probe) console.error(` Validation probe summary: ${summarizeProbeForDisplay(probe)}.`);
Comment thread
cv marked this conversation as resolved.
Dismissed
console.error(" Validation details were omitted to avoid exposing credentials.");
}

Expand All @@ -104,7 +109,7 @@ export function createInferenceSelectionValidationHelpers(
const apiKey = credentialEnv ? getCredential(credentialEnv) : "";
const probe = probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options);
if (!probe.ok) {
printValidationFailure(label);
printValidationFailure(label, probe);
if (deps.isNonInteractive()) {
process.exit(1);
}
Expand Down Expand Up @@ -139,7 +144,7 @@ export function createInferenceSelectionValidationHelpers(
const apiKey = getCredential(credentialEnv);
const probe = probeAnthropicEndpoint(endpointUrl, model, apiKey);
if (!probe.ok) {
printValidationFailure(label);
printValidationFailure(label, probe);
if (deps.isNonInteractive()) {
process.exit(1);
}
Expand Down Expand Up @@ -182,7 +187,7 @@ export function createInferenceSelectionValidationHelpers(
}
return { ok: true, api: probe.api ?? "openai-completions" };
}
printValidationFailure(label);
printValidationFailure(label, probe);
if (deps.isNonInteractive()) {
process.exit(1);
}
Expand Down Expand Up @@ -212,7 +217,7 @@ export function createInferenceSelectionValidationHelpers(
console.log(` ${probe.label} available — ${deps.agentProductName()} will use ${probe.api}.`);
return { ok: true, api: probe.api };
}
printValidationFailure(label);
printValidationFailure(label, probe);
if (deps.isNonInteractive()) {
process.exit(1);
}
Expand Down
53 changes: 53 additions & 0 deletions src/lib/onboard/probe-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { summarizeProbeForDisplay } from "./probe-diagnostics";

describe("summarizeProbeForDisplay", () => {
it("summarizes HTTP statuses without raw response bodies", () => {
const summary = summarizeProbeForDisplay({
message: "Chat Completions API: HTTP 429: raw provider body with secret-key",
failures: [
{
name: "Chat Completions API",
httpStatus: 429,
curlStatus: 0,
message: "HTTP 429: raw provider body with secret-key",
body: "raw provider body with secret-key",
},
],
});

expect(summary).toBe("Chat Completions API: HTTP 429");
expect(summary).not.toContain("secret-key");
expect(summary).not.toContain("raw provider body");
});

it("summarizes curl/timeout failures without raw stderr", () => {
const summary = summarizeProbeForDisplay({
message: "curl failed (exit 28): operation timed out with token secret-key",
failures: [
{
name: "Chat Completions API",
httpStatus: 0,
curlStatus: 28,
message: "curl failed (exit 28): operation timed out with token secret-key",
},
],
});

expect(summary).toBe("Chat Completions API: curl exit 28");
expect(summary).not.toContain("secret-key");
expect(summary).not.toContain("operation timed out with token");
});

it("falls back to coarse message classification", () => {
expect(summarizeProbeForDisplay({ message: "HTTP 404: not found for secret-key" })).toBe(
"HTTP 404",
);
expect(summarizeProbeForDisplay({ message: "request timed out with secret-key" })).toBe(
"timeout",
);
});
});
30 changes: 30 additions & 0 deletions src/lib/onboard/probe-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

function summarizeProbeFailureForDisplay(failure: Record<string, unknown>): string {
const name = typeof failure.name === "string" ? failure.name : "probe";
const httpStatus = typeof failure.httpStatus === "number" ? failure.httpStatus : 0;
const curlStatus = typeof failure.curlStatus === "number" ? failure.curlStatus : 0;
if (httpStatus > 0) return `${name}: HTTP ${httpStatus}`;
if (curlStatus !== 0) return `${name}: curl exit ${curlStatus}`;
return `${name}: no HTTP response`;
}

export function summarizeProbeForDisplay(probe: {
failures?: unknown[];
message?: unknown;
}): string {
const failures = Array.isArray(probe.failures)
? probe.failures.filter((failure): failure is Record<string, unknown> => {
return Boolean(failure) && typeof failure === "object";
})
: [];
if (failures.length > 0) return failures.map(summarizeProbeFailureForDisplay).join("; ");
const message = typeof probe.message === "string" ? probe.message : "no probe details available";
const httpMatch = message.match(/\bHTTP\s+(\d{3})\b/i);
if (httpMatch) return `HTTP ${httpMatch[1]}`;
const curlMatch = message.match(/curl failed \(exit (-?\d+)\)/i);
if (curlMatch) return `curl exit ${curlMatch[1]}`;
if (/timed? out|timeout/i.test(message)) return "timeout";
return "probe failed";
}