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
4 changed files with 470 additions and 57 deletions
@@ -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 })
</Button>
</div>
</div>
<Input
label="Model ID (optional)"
value={checkModelId}
onChange={(e) => 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 && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? "Valid" : "Invalid"}
+98 -43
View File
@@ -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 (
<>
<Badge variant="success">Valid</Badge>
{method === "chat" && <span className="text-sm text-text-muted">(via inference test)</span>}
</>
);
}
return (
<div className="flex flex-col gap-1">
<Badge variant="error">Invalid</Badge>
{error && <span className="text-sm text-red-500">{error}</span>}
</div>
);
};
return (
<Modal isOpen={isOpen} title="Add OpenAI Compatible" onClose={onClose}>
<div className="flex flex-col gap-4">
@@ -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."
/>
<div className="flex gap-2">
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
className="flex-1"
/>
<div className="pt-6">
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
</div>
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
/>
<Input
label="Model ID (optional)"
value={checkModelId}
onChange={(e) => 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."
/>
<div className="flex items-center gap-3">
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
{renderValidationResult()}
</div>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? "Valid" : "Invalid"}
</Badge>
)}
<div className="flex gap-2">
<Button onClick={handleSubmit} fullWidth disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || submitting}>
{submitting ? "Creating..." : "Create"}
@@ -790,13 +817,15 @@ function AddAnthropicCompatibleModal({ 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);
const [validationResult, setValidationResult] = useState(null); // { valid, error, method }
useEffect(() => {
if (isOpen) {
setValidationResult(null);
setCheckKey("");
setCheckModelId("");
}
}, [isOpen]);
@@ -834,17 +863,43 @@ function AddAnthropicCompatibleModal({ 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: "anthropic-compatible" }),
body: JSON.stringify({
baseUrl: formData.baseUrl,
apiKey: checkKey,
type: "anthropic-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 (
<>
<Badge variant="success">Valid</Badge>
{method === "chat" && <span className="text-sm text-text-muted">(via inference test)</span>}
</>
);
}
return (
<div className="flex flex-col gap-1">
<Badge variant="error">Invalid</Badge>
{error && <span className="text-sm text-red-500">{error}</span>}
</div>
);
};
return (
<Modal isOpen={isOpen} title="Add Anthropic Compatible" onClose={onClose}>
<div className="flex flex-col gap-4">
@@ -869,25 +924,25 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
placeholder="https://api.anthropic.com/v1"
hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages."
/>
<div className="flex gap-2">
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
className="flex-1"
/>
<div className="pt-6">
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
</div>
<Input
label="API Key (for Check)"
type="password"
value={checkKey}
onChange={(e) => setCheckKey(e.target.value)}
/>
<Input
label="Model ID (optional)"
value={checkModelId}
onChange={(e) => 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."
/>
<div className="flex items-center gap-3">
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
{validating ? "Checking..." : "Check"}
</Button>
{renderValidationResult()}
</div>
{validationResult && (
<Badge variant={validationResult === "success" ? "success" : "error"}>
{validationResult === "success" ? "Valid" : "Invalid"}
</Badge>
)}
<div className="flex gap-2">
<Button onClick={handleSubmit} fullWidth disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || submitting}>
{submitting ? "Creating..." : "Create"}