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
27 changes: 13 additions & 14 deletions src/lib/onboard/inference-selection-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,7 @@ describe("inference selection validation", () => {

it("routes an unreachable custom endpoint through transport recovery, not a silent loop (#6854)", async () => {
const probeOpenAiLikeEndpoint = vi.fn(() => ({ ok: true, api: "openai-completions" }));
let capturedRecovery: { kind?: string } | undefined;
const promptValidationRecovery = vi.fn(async (_label: string, recovery: { kind?: string }) => {
capturedRecovery = recovery;
return "retry" as const;
});
const promptValidationRecovery = vi.fn(async () => "retry" as const);
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const helpers = createInferenceSelectionValidationHelpers({
isNonInteractive: () => false,
Expand All @@ -258,18 +254,21 @@ describe("inference selection validation", () => {
});

try {
await helpers.validateCustomOpenAiLikeSelection(
await expect(
helpers.validateCustomOpenAiLikeSelection(
"Custom endpoint",
"https://example.invalid/v1",
"model-a",
"COMPATIBLE_API_KEY",
),
).resolves.toEqual({ ok: false, retry: "retry" });
expect(probeOpenAiLikeEndpoint).not.toHaveBeenCalled();
expect(promptValidationRecovery).toHaveBeenCalledWith(
"Custom endpoint",
"https://example.invalid/v1",
"model-a",
expect.objectContaining({ kind: "transport", retry: "retry" }),
"COMPATIBLE_API_KEY",
null,
);
// A DNS-unreachable endpoint is a transport failure: the recovery prompt
// receives a transport classification (DNS/VPN/URL hint + retry/back/exit),
// not the silent selection loop the private-IP path takes.
expect(promptValidationRecovery).toHaveBeenCalled();
expect(capturedRecovery?.kind).toBe("transport");
expect(probeOpenAiLikeEndpoint).not.toHaveBeenCalled();
} finally {
error.mockRestore();
}
Expand Down
19 changes: 19 additions & 0 deletions src/lib/validation-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ describe("validation-recovery helpers", () => {
expect(
getTransportRecoveryMessage({ curlStatus: 6, message: "Could not resolve host" }),
).toContain("could not resolve");
expect(
getTransportRecoveryMessage({
message: 'cannot resolve endpoint host "example.invalid": getaddrinfo ENOTFOUND',
}),
).toContain("Check DNS, VPN, or the endpoint URL");
expect(
getTransportRecoveryMessage({ curlStatus: 60, message: "SSL certificate problem" }),
).toContain("TLS/certificate");
Expand All @@ -44,6 +49,20 @@ describe("validation-recovery helpers", () => {
});
});

it("routes DNS-backed preflight resolution failures through transport recovery (#6854)", () => {
const failure = {
name: "SSRF preflight",
httpStatus: 0,
curlStatus: 0,
message: 'cannot resolve endpoint host "example.invalid": getaddrinfo ENOTFOUND',
};
expect(getProbeRecovery({ failures: [failure] })).toEqual({
kind: "transport",
retry: "retry",
failure,
});
});

it("only allows model-specific retry when explicitly enabled", () => {
const probe = { failures: [{ httpStatus: 400, message: "unknown model" }] };
expect(getProbeRecovery(probe)).toEqual({ kind: "unknown", retry: "selection" });
Expand Down
7 changes: 6 additions & 1 deletion src/lib/validation-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export function getTransportRecoveryMessage(failure: ValidationFailureLike = {})
if (failure.httpStatus && failure.httpStatus >= 500 && failure.httpStatus < 600) {
return " The provider endpoint is reachable but currently failing upstream.";
}
if (failure.curlStatus === 6 || /could not resolve host|name or service not known/.test(text)) {
if (
failure.curlStatus === 6 ||
/cannot resolve endpoint host|did not resolve to any address|could not resolve host|name or service not known|enotfound|eai_again/.test(
text,
)
) {
return " Validation could not resolve the provider hostname. Check DNS, VPN, or the endpoint URL.";
}
if (failure.curlStatus === 7 || /connection refused|failed to connect/.test(text)) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export function classifyValidationFailure({
if (/unauthorized|forbidden|invalid api key|invalid_auth|permission/i.test(normalized)) {
return { kind: "credential", retry: "credential" };
}
if (
/cannot resolve endpoint host|did not resolve to any address|could not resolve host|name or service not known|enotfound|eai_again|no http response/i.test(
normalized,
)
) {
return { kind: "transport", retry: "retry" };
}
if (/ssl|tls|certificate|handshake/i.test(normalized)) {
return { kind: "transport", retry: "retry" };
}
Expand Down
Loading