mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
This commit is contained in:
@@ -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" : "<API_KEY_FROM_DASHBOARD>");
|
||||
|
||||
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({
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* 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 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
{droidStatus.settings.customModels.find(m => m.id === "custom:9Router-0").baseUrl}
|
||||
{droidStatus.settings.customModels.find(m => m.id?.startsWith("custom:9Router")).baseUrl}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -256,12 +273,12 @@ export default function DroidToolCard({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => 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"
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => 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 && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -285,13 +302,48 @@ export default function DroidToolCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model */}
|
||||
{/* Models */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Model</span>
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Models {modelList.length > 0 && <span className="text-primary">({modelList.length})</span>}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => 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" />
|
||||
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
|
||||
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
{/* Model list */}
|
||||
{modelList.length > 0 && (
|
||||
<div className="flex flex-col gap-0.5 mb-1">
|
||||
{modelList.map((id) => (
|
||||
<div key={id} className="flex items-center gap-1.5 px-2 py-1 bg-bg-secondary rounded border border-border">
|
||||
<span className="flex-1 text-xs font-mono truncate">{id}</span>
|
||||
<button onClick={() => removeModel(id)} className="text-text-muted hover:text-red-500 transition-colors shrink-0" title="Remove">
|
||||
<span className="material-symbols-outlined text-[12px]">close</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Model input row */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={modelInput}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs shrink-0 ${hasActiveProviders ? "bg-surface border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<button onClick={addModel} disabled={!modelInput.trim()} className="px-2 py-1.5 rounded border bg-surface border-border hover:border-primary text-xs shrink-0 disabled:opacity-50" title="Add model">
|
||||
<span className="material-symbols-outlined text-[14px]">add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -303,7 +355,7 @@ export default function DroidToolCard({
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={!selectedModel} loading={applying}>
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={modelList.length === 0} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleResetSettings} disabled={!droidStatus?.has9Router} loading={restoring}>
|
||||
@@ -322,7 +374,7 @@ export default function DroidToolCard({
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSelect={handleModelSelect}
|
||||
selectedModel={selectedModel}
|
||||
selectedModel={null}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title="Select Model for Factory Droid"
|
||||
@@ -336,4 +388,4 @@ export default function DroidToolCard({
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ const readSettings = async () => {
|
||||
// Check if settings has 9Router customModels
|
||||
const has9RouterConfig = (settings) => {
|
||||
if (!settings || !settings.customModels) return false;
|
||||
return settings.customModels.some(m => m.id === "custom:9Router-0");
|
||||
return settings.customModels.some(m => m.id?.startsWith("custom:9Router"));
|
||||
};
|
||||
|
||||
// GET - Check droid CLI and read current settings
|
||||
@@ -78,12 +78,17 @@ export async function GET() {
|
||||
}
|
||||
|
||||
// POST - Update 9Router customModels (merge with existing settings)
|
||||
// Accepts either `model` (string, legacy single-model) or `models` (array of strings, multi-model)
|
||||
// Also accepts `activeModel` to set which model is active/primary
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { baseUrl, apiKey, model } = await request.json();
|
||||
const { baseUrl, apiKey, model, models, activeModel } = await request.json();
|
||||
|
||||
if (!baseUrl || !model) {
|
||||
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
|
||||
// Accept either `models` (array) or `model` (string, legacy)
|
||||
const modelsArray = Array.isArray(models) ? models.slice() : (typeof model === "string" ? [model] : []);
|
||||
|
||||
if (!baseUrl || modelsArray.length === 0) {
|
||||
return NextResponse.json({ error: "baseUrl and at least one model are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const droidDir = getDroidDir();
|
||||
@@ -104,26 +109,51 @@ export async function POST(request) {
|
||||
settings.customModels = [];
|
||||
}
|
||||
|
||||
// Remove existing 9Router config if any
|
||||
settings.customModels = settings.customModels.filter(m => m.id !== "custom:9Router-0");
|
||||
// Remove all existing 9Router configs
|
||||
settings.customModels = settings.customModels.filter(m => !m.id?.startsWith("custom:9Router"));
|
||||
|
||||
// Normalize baseUrl to ensure /v1 suffix
|
||||
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
||||
const keyToUse = apiKey || "your_api_key";
|
||||
|
||||
// Add new 9Router config
|
||||
const customModel = {
|
||||
model: model,
|
||||
id: "custom:9Router-0",
|
||||
index: 0,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
apiKey: apiKey || "your_api_key",
|
||||
displayName: model,
|
||||
maxOutputTokens: 131072,
|
||||
noImageSupport: false,
|
||||
provider: "openai",
|
||||
};
|
||||
// Determine active model: prefer explicit activeModel, else first of modelsArray
|
||||
// If activeModel is explicitly empty string, no model will be set as default
|
||||
let defaultIndex = 0;
|
||||
if (typeof activeModel === "string") {
|
||||
if (activeModel === "") {
|
||||
defaultIndex = -1; // signal: don't set a default
|
||||
} else {
|
||||
const idx = modelsArray.indexOf(activeModel);
|
||||
defaultIndex = idx >= 0 ? idx : 0;
|
||||
}
|
||||
}
|
||||
|
||||
settings.customModels.unshift(customModel);
|
||||
// Add entries for all requested models
|
||||
// The first one (index 0) will be the default if defaultIndex >= 0
|
||||
for (let i = 0; i < modelsArray.length; i++) {
|
||||
const m = modelsArray[i];
|
||||
if (!m || typeof m !== "string") continue;
|
||||
settings.customModels.push({
|
||||
model: m,
|
||||
id: `custom:9Router-${i}`,
|
||||
index: i,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
apiKey: keyToUse,
|
||||
displayName: m,
|
||||
maxOutputTokens: 131072,
|
||||
noImageSupport: false,
|
||||
provider: "openai",
|
||||
});
|
||||
}
|
||||
|
||||
// Set default model if applicable
|
||||
if (defaultIndex >= 0 && settings.customModels[defaultIndex]) {
|
||||
// Reorder so the default comes first
|
||||
const [defaultEntry] = settings.customModels.splice(defaultIndex, 1);
|
||||
settings.customModels.unshift({ ...defaultEntry, index: 0 });
|
||||
// Re-index the rest
|
||||
settings.customModels.forEach((m, i) => { m.index = i; });
|
||||
}
|
||||
|
||||
// Write settings
|
||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
||||
@@ -161,7 +191,7 @@ export async function DELETE() {
|
||||
|
||||
// Remove 9Router customModels
|
||||
if (settings.customModels) {
|
||||
settings.customModels = settings.customModels.filter(m => m.id !== "custom:9Router-0");
|
||||
settings.customModels = settings.customModels.filter(m => !m.id?.startsWith("custom:9Router"));
|
||||
|
||||
// Remove customModels array if empty
|
||||
if (settings.customModels.length === 0) {
|
||||
@@ -180,4 +210,4 @@ export async function DELETE() {
|
||||
console.log("Error resetting droid settings:", error);
|
||||
return NextResponse.json({ error: "Failed to reset droid settings" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user