Add optional modelID input for custom API Key Providers testing (#315)

* feat: add modelId fallback for provider validation

- If /models endpoint unavailable, validate via /chat/completions
- Add optional Model ID input in EditCompatibleNodeModal
- Improves compatibility with providers lacking /models endpoint

* feat: improve provider validation with modelId fallback

- Add Model ID input for chat/completions fallback validation
- Reorder UI: API Key → Model ID → Check button + Badge
- Display detailed BE error messages in FE
- Add status-specific error handling (401/403/400/404/5xx)
- Add unit tests for error message helpers
- Add vitest devDependency
This commit is contained in:
moophat 2026-03-16 09:21:05 +07:00 committed by GitHub
parent 1dd5d60724
commit 65af4328fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 470 additions and 57 deletions

View file

@ -32,11 +32,28 @@ const getErrorMessage = (error) => {
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 } = body;
const { baseUrl, apiKey, type, modelId } = body;
if (!baseUrl || !apiKey) {
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
@ -49,25 +66,55 @@ export async function POST(request) {
// Anthropic Compatible Validation
if (type === "anthropic-compatible") {
// Robustly construct URL: remove trailing slash, and remove trailing /messages if user added it
let normalizedBase = baseUrl.trim().replace(/\/$/, "");
if (normalizedBase.endsWith("/messages")) {
normalizedBase = normalizedBase.slice(0, -9); // remove /messages
normalizedBase = normalizedBase.slice(0, -9);
}
// Use /models endpoint for validation as many compatible providers support it (like OpenAI)
const modelsUrl = `${normalizedBase}/models`;
const res = await fetchWithTimeout(modelsUrl, {
method: "GET",
headers: {
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"Authorization": `Bearer ${apiKey}` // Add Bearer token for hybrid proxies
"Authorization": `Bearer ${apiKey}`
}
});
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
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)
@ -76,7 +123,38 @@ export async function POST(request) {
headers: { "Authorization": `Bearer ${apiKey}` },
});
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key or unauthorized" });
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:", {