From 65af4328fdaf8b7ba3d6624de29ba7332c0fff8a Mon Sep 17 00:00:00 2001 From: moophat Date: Mon, 16 Mar 2026 09:21:05 +0700 Subject: [PATCH] Add optional modelID input for custom API Key Providers testing (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../dashboard/providers/[id]/page.js | 17 +- .../(dashboard)/dashboard/providers/page.js | 141 ++++++--- src/app/api/provider-nodes/validate/route.js | 98 ++++++- tests/unit/provider-validation.test.js | 271 ++++++++++++++++++ 4 files changed, 470 insertions(+), 57 deletions(-) create mode 100644 tests/unit/provider-validation.test.js diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 11869b20..69d5ca0d 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -1987,6 +1987,7 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) }); const [saving, setSaving] = useState(false); const [checkKey, setCheckKey] = useState(""); + const [checkModelId, setCheckModelId] = useState(""); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); @@ -2030,10 +2031,11 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) const res = await fetch("/api/provider-nodes/validate", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: formData.baseUrl, - apiKey: checkKey, - type: isAnthropic ? "anthropic-compatible" : "openai-compatible" + body: JSON.stringify({ + baseUrl: formData.baseUrl, + apiKey: checkKey, + type: isAnthropic ? "anthropic-compatible" : "openai-compatible", + modelId: checkModelId.trim() || undefined }), }); const data = await res.json(); @@ -2093,6 +2095,13 @@ function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) + setCheckModelId(e.target.value)} + placeholder="e.g. my-model-id" + hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." + /> {validationResult && ( {validationResult === "success" ? "Valid" : "Invalid"} diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index de6efa3c..8b0bb12e 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -657,6 +657,7 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { }); const [submitting, setSubmitting] = useState(false); const [checkKey, setCheckKey] = useState(""); + const [checkModelId, setCheckModelId] = useState(""); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); @@ -705,17 +706,43 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { const res = await fetch("/api/provider-nodes/validate", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ baseUrl: formData.baseUrl, apiKey: checkKey, type: "openai-compatible" }), + body: JSON.stringify({ + baseUrl: formData.baseUrl, + apiKey: checkKey, + type: "openai-compatible", + modelId: checkModelId.trim() || undefined + }), }); const data = await res.json(); - setValidationResult(data.valid ? "success" : "failed"); + setValidationResult(data); } catch { - setValidationResult("failed"); + setValidationResult({ valid: false, error: "Network error" }); } finally { setValidating(false); } }; + // Helper to render validation result + const renderValidationResult = () => { + if (!validationResult) return null; + const { valid, error, method } = validationResult; + + if (valid) { + return ( + <> + Valid + {method === "chat" && (via inference test)} + + ); + } + return ( +
+ Invalid + {error && {error}} +
+ ); + }; + return (
@@ -746,25 +773,25 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) { placeholder="https://api.openai.com/v1" hint="Use the base URL (ending in /v1) for your OpenAI-compatible API." /> -
- setCheckKey(e.target.value)} - className="flex-1" - /> -
- -
+ setCheckKey(e.target.value)} + /> + setCheckModelId(e.target.value)} + placeholder="e.g. gpt-4, claude-3-opus" + hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." + /> +
+ + {renderValidationResult()}
- {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )}
-
+ setCheckKey(e.target.value)} + /> + setCheckModelId(e.target.value)} + placeholder="e.g. claude-3-opus" + hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead." + /> +
+ + {renderValidationResult()}
- {validationResult && ( - - {validationResult === "success" ? "Valid" : "Invalid"} - - )}