Skip to content

Commit be5c298

Browse files
committed
add provider credential validation to model router and fix icon alignment
1 parent 275eaff commit be5c298

File tree

2 files changed

+162
-21
lines changed

2 files changed

+162
-21
lines changed

frontend/src/pages/GeneralSettings/ModelRouters/LLMProviderModelPicker/index.jsx

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { useEffect, useState } from "react";
22
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
33
import System from "@/models/system";
4+
import ModalWrapper from "@/components/ModalWrapper";
5+
import { useModal } from "@/hooks/useModal";
6+
import { X } from "@phosphor-icons/react";
7+
import showToast from "@/utils/toast";
8+
9+
// Providers that can't be routing targets
10+
const EXCLUDED_PROVIDERS = ["anythingllm-router"];
411

512
export default function LLMProviderModelPicker({
613
providerFieldName = "fallback_provider",
@@ -14,12 +21,34 @@ export default function LLMProviderModelPicker({
1421
const [selectedModel, setSelectedModel] = useState(defaultModel);
1522
const [models, setModels] = useState([]);
1623
const [loadingModels, setLoadingModels] = useState(false);
24+
const [settings, setSettings] = useState(null);
25+
const { isOpen, openModal, closeModal } = useModal();
26+
27+
const availableProviders = AVAILABLE_LLM_PROVIDERS.filter(
28+
(llm) => !EXCLUDED_PROVIDERS.includes(llm.value)
29+
);
30+
31+
useEffect(() => {
32+
async function fetchSettings() {
33+
const _settings = await System.keys();
34+
setSettings(_settings ?? {});
35+
}
36+
fetchSettings();
37+
}, []);
38+
39+
function isConfigured(providerValue) {
40+
if (!settings) return true;
41+
const llm = availableProviders.find((l) => l.value === providerValue);
42+
if (!llm?.requiredConfig?.length) return true;
43+
return llm.requiredConfig.every((key) => !!settings[key]);
44+
}
1745

1846
useEffect(() => {
19-
if (!selectedProvider) {
47+
if (!selectedProvider || !settings) {
2048
setModels([]);
2149
return;
2250
}
51+
if (!isConfigured(selectedProvider)) return;
2352

2453
async function fetchModels() {
2554
setLoadingModels(true);
@@ -29,7 +58,46 @@ export default function LLMProviderModelPicker({
2958
setLoadingModels(false);
3059
}
3160
fetchModels();
32-
}, [selectedProvider]);
61+
}, [selectedProvider, settings]);
62+
63+
function handleProviderChange(e) {
64+
const value = e.target.value;
65+
setSelectedProvider(value);
66+
setSelectedModel("");
67+
setModels([]);
68+
if (value && !isConfigured(value)) openModal();
69+
}
70+
71+
function handleSetupCancel() {
72+
closeModal();
73+
if (!isConfigured(selectedProvider)) {
74+
setSelectedProvider(defaultProvider || "");
75+
setSelectedModel(defaultModel || "");
76+
}
77+
}
78+
79+
async function handleSetupSave(e) {
80+
e.preventDefault();
81+
e.stopPropagation();
82+
const data = {};
83+
const form = new FormData(e.target);
84+
for (const [key, value] of form.entries()) data[key] = value;
85+
const { error } = await System.updateSystem(data);
86+
if (error) {
87+
showToast(`Failed to save settings: ${error}`, "error");
88+
return;
89+
}
90+
const _settings = await System.keys();
91+
setSettings(_settings ?? {});
92+
closeModal();
93+
showToast("Provider configured successfully", "success", { clear: true });
94+
}
95+
96+
const selectedLlm = availableProviders.find(
97+
(l) => l.value === selectedProvider
98+
);
99+
const needsSetup =
100+
selectedProvider && selectedLlm && !isConfigured(selectedProvider);
33101

34102
return (
35103
<div className="flex flex-col gap-y-1.5">
@@ -46,23 +114,29 @@ export default function LLMProviderModelPicker({
46114
<select
47115
name={providerFieldName}
48116
value={selectedProvider}
49-
onChange={(e) => {
50-
setSelectedProvider(e.target.value);
51-
setSelectedModel("");
52-
}}
117+
onChange={handleProviderChange}
53118
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-white light:text-slate-900 text-sm rounded-lg outline-none block w-full p-2.5"
54119
required
55120
>
56121
<option value="">Select provider</option>
57-
{AVAILABLE_LLM_PROVIDERS.map((llm) => (
122+
{availableProviders.map((llm) => (
58123
<option key={llm.value} value={llm.value}>
59124
{llm.name}
125+
{!isConfigured(llm.value) ? " (setup required)" : ""}
60126
</option>
61127
))}
62128
</select>
63129
</div>
64130
<div className="flex-1">
65-
{loadingModels ? (
131+
{needsSetup ? (
132+
<button
133+
type="button"
134+
onClick={openModal}
135+
className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-blue-400 light:text-blue-500 text-sm rounded-lg block w-full p-2.5 text-left hover:text-blue-300 light:hover:text-blue-600 transition-colors"
136+
>
137+
Configure {selectedLlm.name} to continue
138+
</button>
139+
) : loadingModels ? (
66140
<div className="bg-zinc-800 light:bg-white light:border light:border-slate-300 text-zinc-400 light:text-slate-500 text-sm rounded-lg p-2.5">
67141
Loading models...
68142
</div>
@@ -99,6 +173,71 @@ export default function LLMProviderModelPicker({
99173
)}
100174
</div>
101175
</div>
176+
177+
<ProviderSetupModal
178+
isOpen={isOpen}
179+
provider={selectedLlm}
180+
settings={settings}
181+
onSave={handleSetupSave}
182+
onClose={handleSetupCancel}
183+
/>
102184
</div>
103185
);
104186
}
187+
188+
function ProviderSetupModal({ isOpen, provider, settings, onSave, onClose }) {
189+
if (!isOpen || !provider) return null;
190+
191+
return (
192+
<ModalWrapper isOpen={isOpen}>
193+
<div className="w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow-lg border border-zinc-700 light:border-slate-300">
194+
<div className="flex items-center justify-between p-6 border-b border-zinc-700 light:border-slate-300">
195+
<div className="flex items-center gap-x-3">
196+
{provider.logo && (
197+
<img
198+
src={provider.logo}
199+
alt={`${provider.name} logo`}
200+
className="w-8 h-8 rounded-md"
201+
/>
202+
)}
203+
<h3 className="text-lg font-semibold text-white light:text-slate-900">
204+
Configure {provider.name}
205+
</h3>
206+
</div>
207+
<button
208+
onClick={onClose}
209+
type="button"
210+
className="p-1 rounded-lg text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 hover:bg-zinc-800 light:hover:bg-slate-100 transition-colors"
211+
>
212+
<X size={20} weight="bold" />
213+
</button>
214+
</div>
215+
<form id="provider-setup-form" onSubmit={onSave}>
216+
<div className="px-6 py-5">
217+
<p className="text-sm text-zinc-400 light:text-slate-600 mb-4">
218+
Enter the required credentials to use {provider.name} as a routing
219+
target.
220+
</p>
221+
<div className="space-y-4">{provider.options(settings ?? {})}</div>
222+
</div>
223+
<div className="flex justify-end gap-x-3 px-6 py-4 border-t border-zinc-700 light:border-slate-300">
224+
<button
225+
type="button"
226+
onClick={onClose}
227+
className="text-sm font-medium text-zinc-400 light:text-slate-600 hover:text-white light:hover:text-slate-900 px-4 py-2 rounded-lg transition-colors"
228+
>
229+
Cancel
230+
</button>
231+
<button
232+
type="submit"
233+
form="provider-setup-form"
234+
className="text-sm font-medium bg-zinc-50 light:bg-slate-900 text-zinc-900 light:text-white rounded-lg px-5 py-2 hover:opacity-90 transition-opacity duration-200"
235+
>
236+
Save settings
237+
</button>
238+
</div>
239+
</form>
240+
</div>
241+
</ModalWrapper>
242+
);
243+
}

frontend/src/pages/GeneralSettings/ModelRouters/index.jsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,21 @@ function RouterRow({ router, removeRouter }) {
170170
{router.workspaceCount || 0}
171171
</span>
172172
</td>
173-
<td className="px-6 py-3 flex items-center gap-x-4">
174-
<button
175-
onClick={() => navigate(paths.settings.modelRouterEdit(router.id))}
176-
className="text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 transition-colors"
177-
>
178-
<PencilSimple className="h-5 w-5" />
179-
</button>
180-
<button
181-
onClick={handleDelete}
182-
className="text-zinc-400 light:text-slate-500 hover:text-red-400 light:hover:text-red-500 transition-colors"
183-
>
184-
<Trash className="h-5 w-5" />
185-
</button>
173+
<td className="px-6 py-3">
174+
<div className="flex items-center gap-x-4">
175+
<button
176+
onClick={() => navigate(paths.settings.modelRouterEdit(router.id))}
177+
className="text-zinc-400 light:text-slate-500 hover:text-white light:hover:text-slate-900 transition-colors"
178+
>
179+
<PencilSimple className="h-5 w-5" />
180+
</button>
181+
<button
182+
onClick={handleDelete}
183+
className="text-zinc-400 light:text-slate-500 hover:text-red-400 light:hover:text-red-500 transition-colors"
184+
>
185+
<Trash className="h-5 w-5" />
186+
</button>
187+
</div>
186188
</td>
187189
</tr>
188190
);

0 commit comments

Comments
 (0)