github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
7.29 kB
import { NextResponse } from "next/server";
// Fetch with timeout wrapper
const fetchWithTimeout = (url, options, timeout = 10000) => {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timeout")), timeout)
)
]);
};
// Validate URL format
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Parse error details for user-friendly messages
const getErrorMessage = (error) => {
if (error.cause?.code === "ECONNREFUSED") return "Connection refused - provider node offline or unreachable";
if (error.cause?.code === "ENOTFOUND") return "DNS lookup failed - invalid domain or network issue";
if (error.cause?.code === "ETIMEDOUT") return "Connection timeout - provider node too slow";
if (error.message.includes("timeout")) return "Request timeout (>10s) - provider node not responding";
if (error.cause?.code === "CERT_HAS_EXPIRED") return "SSL certificate expired";
if (error.cause?.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") return "SSL certificate verification failed";
if (error.cause?.code) return `Network error: ${error.cause.code}`;
return "Network connection failed - check URL and network connectivity";
};
// Get status-specific error message for /models endpoint
const getModelsErrorMessage = (status) => {
if (status === 401 || status === 403) return "API key unauthorized";
if (status === 404) return "/models endpoint not found - try chat validation with model ID";
if (status >= 500) return "Server error - try again later";
return `Unexpected response (${status})`;
};
// Get status-specific error message for /chat/completions endpoint
const getChatErrorMessage = (status) => {
if (status === 401 || status === 403) return "API key unauthorized";
if (status === 400) return "Invalid model or bad request";
if (status === 404) return "Chat endpoint not found";
if (status >= 500) return "Server error - try again later";
return `Chat request failed (${status})`;
};
// POST /api/provider-nodes/validate - Validate API key against base URL
export async function POST(request) {
try {
const body = await request.json();
const { baseUrl, apiKey, type, modelId } = body;
if (!baseUrl || !apiKey) {
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
}
// Validate URL format
if (!isValidUrl(baseUrl)) {
return NextResponse.json({ error: "Invalid URL format" }, { status: 400 });
}
// Custom Embedding Validation - test POST /embeddings directly
if (type === "custom-embedding") {
const normalizedBase = baseUrl.trim().replace(/\/$/, "");
if (!modelId?.trim()) {
return NextResponse.json({ valid: false, error: "Model ID required for embedding validation" });
}
const embedRes = await fetchWithTimeout(`${normalizedBase}/embeddings`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ model: modelId.trim(), input: "ping" })
});
if (embedRes.ok) {
const data = await embedRes.json().catch(() => null);
const dims = Array.isArray(data?.data?.[0]?.embedding) ? data.data[0].embedding.length : null;
return NextResponse.json({ valid: true, method: "embeddings", dimensions: dims });
}
if (embedRes.status === 401 || embedRes.status === 403) {
return NextResponse.json({ valid: false, error: "API key unauthorized" });
}
const errBody = await embedRes.text().catch(() => "");
return NextResponse.json({
valid: false,
error: `Embeddings request failed (${embedRes.status})${errBody ? `: ${errBody.slice(0, 200)}` : ""}`,
method: "embeddings"
});
}
// Anthropic Compatible Validation
if (type === "anthropic-compatible") {
let normalizedBase = baseUrl.trim().replace(/\/$/, "");
if (normalizedBase.endsWith("/messages")) {
normalizedBase = normalizedBase.slice(0, -9);
}
const modelsUrl = `${normalizedBase}/models`;
const res = await fetchWithTimeout(modelsUrl, {
method: "GET",
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"Authorization": `Bearer ${apiKey}`
}
});
if (res.ok) return NextResponse.json({ valid: true });
// Auth errors - no point trying chat fallback
if (res.status === 401 || res.status === 403) {
return NextResponse.json({ valid: false, error: "API key unauthorized" });
}
// Fallback: try chat/completions if modelId provided
if (modelId) {
const chatRes = await fetchWithTimeout(`${normalizedBase}/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: modelId,
messages: [{ role: "user", content: "ping" }],
max_tokens: 1
})
});
if (chatRes.ok) {
return NextResponse.json({ valid: true, method: "chat" });
}
return NextResponse.json({
valid: false,
error: getChatErrorMessage(chatRes.status),
method: "chat"
});
}
return NextResponse.json({ valid: false, error: getModelsErrorMessage(res.status) });
}
// OpenAI Compatible Validation (Default)
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
const res = await fetchWithTimeout(modelsUrl, {
headers: { "Authorization": `Bearer ${apiKey}` },
});
if (res.ok) return NextResponse.json({ valid: true });
// Auth errors - no point trying chat fallback
if (res.status === 401 || res.status === 403) {
return NextResponse.json({ valid: false, error: "API key unauthorized" });
}
// Fallback: try chat/completions if modelId provided
if (modelId) {
const chatRes = await fetchWithTimeout(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: modelId,
messages: [{ role: "user", content: "ping" }],
max_tokens: 1
})
});
if (chatRes.ok) {
return NextResponse.json({ valid: true, method: "chat" });
}
return NextResponse.json({
valid: false,
error: getChatErrorMessage(chatRes.status),
method: "chat"
});
}
return NextResponse.json({ valid: false, error: getModelsErrorMessage(res.status) });
} catch (error) {
const errorMessage = getErrorMessage(error);
console.error("Error validating provider node:", {
message: error.message,
cause: error.cause,
code: error.cause?.code,
userMessage: errorMessage
});
return NextResponse.json({
valid: false,
error: errorMessage
}, { status: 500 });
}
}