From 1d872ce2548d29198173433e4418ea1b0999ce5e Mon Sep 17 00:00:00 2001 From: Anurag Saxena Date: Fri, 17 Apr 2026 01:04:46 -0400 Subject: [PATCH] fix: add multi-model support for Factory Droid CLI tool (closes #521) (#618) --- .../cli-tools/components/DroidToolCard.js | 154 ++++++++++++------ src/app/api/cli-tools/droid-settings/route.js | 72 +++++--- 2 files changed, 154 insertions(+), 72 deletions(-) diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js index 6282ba2c..b8edebc5 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.js @@ -23,7 +23,8 @@ export default function DroidToolCard({ const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [selectedApiKey, setSelectedApiKey] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); + const [modelList, setModelList] = useState([]); + const [modelInput, setModelInput] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); @@ -32,7 +33,8 @@ export default function DroidToolCard({ const getConfigStatus = () => { if (!droidStatus?.installed) return null; - const currentConfig = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); + // Check for any 9Router model entry (support multi-model: custom:9Router-0, custom:9Router-1, ...) + const currentConfig = droidStatus.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router")); if (!currentConfig) return "not_configured"; const localMatch = currentConfig.baseUrl?.includes("localhost") || currentConfig.baseUrl?.includes("127.0.0.1"); const cloudMatch = cloudEnabled && CLOUD_URL && currentConfig.baseUrl?.startsWith(CLOUD_URL); @@ -71,18 +73,25 @@ export default function DroidToolCard({ } }; + // Pre-fill model list from existing config (supports multi-model) useEffect(() => { if (droidStatus?.installed && !hasInitializedModel.current) { hasInitializedModel.current = true; - const customModel = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); - if (customModel) { - if (customModel.model) setSelectedModel(customModel.model); - if (customModel.apiKey && apiKeys?.some(k => k.key === customModel.apiKey)) { - setSelectedApiKey(customModel.apiKey); + const existingModels = (droidStatus.settings?.customModels || []) + .filter(m => m.id?.startsWith("custom:9Router")) + .sort((a, b) => (a.index || 0) - (b.index || 0)) + .map(m => m.model); + if (existingModels.length > 0) { + setModelList(existingModels); + } else { + // Legacy: single model stored as custom:9Router-0 + const legacy = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); + if (legacy?.model) { + setModelList([legacy.model]); } } } - }, [droidStatus, apiKeys]); + }, [droidStatus]); const checkDroidStatus = async () => { setCheckingDroid(true); @@ -107,21 +116,37 @@ export default function DroidToolCard({ return url.endsWith("/v1") ? url : `${url}/v1`; }; + const addModel = () => { + const val = modelInput.trim(); + if (!val || modelList.includes(val)) return; + setModelList((prev) => [...prev, val]); + setModelInput(""); + }; + + const removeModel = (id) => setModelList((prev) => prev.filter((m) => m !== id)); + + const handleModelSelect = (model) => { + if (!model.value || modelList.includes(model.value)) return; + setModelList((prev) => [...prev, model.value]); + setModalOpen(false); + }; + const handleApplySettings = async () => { setApplying(true); setMessage(null); try { - const keyToUse = selectedApiKey?.trim() + const keyToUse = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].key : null) || (!cloudEnabled ? "sk_9router" : null); const res = await fetch("/api/cli-tools/droid-settings", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - baseUrl: getEffectiveBaseUrl(), + body: JSON.stringify({ + baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, - model: selectedModel + models: modelList, + activeModel: modelList[0] || "", }), }); const data = await res.json(); @@ -146,8 +171,7 @@ export default function DroidToolCard({ const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings reset successfully!" }); - setSelectedModel(""); - setSelectedApiKey(""); + setModelList([]); checkDroidStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to reset settings" }); @@ -159,35 +183,28 @@ export default function DroidToolCard({ } }; - const handleModelSelect = (model) => { - setSelectedModel(model.value); - setModalOpen(false); - }; - const getManualConfigs = () => { - const keyToUse = (selectedApiKey && selectedApiKey.trim()) - ? selectedApiKey + const keyToUse = (selectedApiKey && selectedApiKey.trim()) + ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); const settingsContent = { - customModels: [ - { - model: selectedModel || "provider/model-id", - id: "custom:9Router-0", - index: 0, - baseUrl: getEffectiveBaseUrl(), - apiKey: keyToUse, - displayName: selectedModel || "provider/model-id", - maxOutputTokens: 131072, - noImageSupport: false, - provider: "openai", - }, - ], + customModels: modelList.map((m, i) => ({ + model: m, + id: `custom:9Router-${i}`, + index: i, + baseUrl: getEffectiveBaseUrl(), + apiKey: keyToUse, + displayName: m, + maxOutputTokens: 131072, + noImageSupport: false, + provider: "openai", + })), }; const platform = typeof navigator !== "undefined" && navigator.platform; const isWindows = platform?.toLowerCase().includes("win"); - const settingsPath = isWindows + const settingsPath = isWindows ? "%USERPROFILE%\\.factory\\settings.json" : "~/.factory/settings.json"; @@ -242,12 +259,12 @@ export default function DroidToolCard({ <>
{/* Current Base URL */} - {droidStatus?.settings?.customModels?.find(m => m.id === "custom:9Router-0")?.baseUrl && ( + {droidStatus?.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router"))?.baseUrl && (
Current arrow_forward - {droidStatus.settings.customModels.find(m => m.id === "custom:9Router-0").baseUrl} + {droidStatus.settings.customModels.find(m => m.id?.startsWith("custom:9Router")).baseUrl}
)} @@ -256,12 +273,12 @@ export default function DroidToolCard({
Base URL arrow_forward - setCustomBaseUrl(e.target.value)} - placeholder="https://.../v1" - className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + setCustomBaseUrl(e.target.value)} + placeholder="https://.../v1" + className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {customBaseUrl && customBaseUrl !== baseUrl && (
- {/* Model */} + {/* Models */}
- Model + + Models {modelList.length > 0 && ({modelList.length})} + arrow_forward - setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> - - {selectedModel && } +
+ {/* Model list */} + {modelList.length > 0 && ( +
+ {modelList.map((id) => ( +
+ {id} + +
+ ))} +
+ )} + {/* Model input row */} +
+ setModelInput(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }} + placeholder="provider/model-id" + className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + + +
+
@@ -303,7 +355,7 @@ export default function DroidToolCard({ )}
-