diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index 2730717..0a07a8a 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal } from "@/shared/components"; +import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; @@ -14,6 +14,7 @@ export default function CombosPage() { const [showCreateModal, setShowCreateModal] = useState(false); const [editingCombo, setEditingCombo] = useState(null); const [activeProviders, setActiveProviders] = useState([]); + const [comboStrategies, setComboStrategies] = useState({}); const { copied, copy } = useCopyToClipboard(); useEffect(() => { @@ -22,17 +23,20 @@ export default function CombosPage() { const fetchData = async () => { try { - const [combosRes, providersRes] = await Promise.all([ + const [combosRes, providersRes, settingsRes] = await Promise.all([ fetch("/api/combos"), fetch("/api/providers"), + fetch("/api/settings"), ]); const combosData = await combosRes.json(); const providersData = await providersRes.json(); + const settingsData = settingsRes.ok ? await settingsRes.json() : {}; if (combosRes.ok) setCombos(combosData.combos || []); if (providersRes.ok) { setActiveProviders(providersData.connections || []); } + setComboStrategies(settingsData.comboStrategies || {}); } catch (error) { console.log("Error fetching data:", error); } finally { @@ -90,6 +94,27 @@ export default function CombosPage() { } }; + const handleToggleRoundRobin = async (comboName, enabled) => { + try { + const updated = { ...comboStrategies }; + if (enabled) { + updated[comboName] = { fallbackStrategy: "round-robin" }; + } else { + delete updated[comboName]; + } + + await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ comboStrategies: updated }), + }); + + setComboStrategies(updated); + } catch (error) { + console.log("Error updating combo strategy:", error); + } + }; + if (loading) { return (
@@ -138,6 +163,8 @@ export default function CombosPage() { onCopy={copy} onEdit={() => setEditingCombo(combo)} onDelete={() => handleDelete(combo.id)} + roundRobinEnabled={comboStrategies[combo.name]?.fallbackStrategy === "round-robin"} + onToggleRoundRobin={(enabled) => handleToggleRoundRobin(combo.name, enabled)} /> ))}
@@ -165,7 +192,7 @@ export default function CombosPage() { ); } -function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) { +function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled, onToggleRoundRobin }) { return (
@@ -204,21 +231,33 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) {
{/* Actions */} -
- - +
+ {/* Round Robin Toggle */} +
+ Round Robin + +
+ +
+ + +
diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 053b291..5d634be 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -55,6 +55,7 @@ const defaultData = { stickyRoundRobinLimit: 3, providerStrategies: {}, comboStrategy: "fallback", + comboStrategies: {}, requireLogin: true, observabilityEnabled: true, observabilityMaxRecords: 1000, @@ -83,6 +84,8 @@ function cloneDefaultData() { tunnelUrl: "", stickyRoundRobinLimit: 3, providerStrategies: {}, + comboStrategy: "fallback", + comboStrategies: {}, requireLogin: true, observabilityEnabled: true, observabilityMaxRecords: 1000, diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js index 245365b..1ab2e91 100644 --- a/src/sse/handlers/chat.js +++ b/src/sse/handlers/chat.js @@ -84,7 +84,11 @@ export async function handleChat(request, clientRawRequest = null) { // Check if model is a combo (has multiple models with fallback) const comboModels = await getComboModels(modelStr); if (comboModels) { - const comboStrategy = settings.comboStrategy || "fallback"; + // Check for combo-specific strategy first, fallback to global + const comboStrategies = settings.comboStrategies || {}; + const comboSpecificStrategy = comboStrategies[modelStr]?.fallbackStrategy; + const comboStrategy = comboSpecificStrategy || settings.comboStrategy || "fallback"; + log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: ${comboStrategy})`); return handleComboChat({ body, @@ -111,7 +115,11 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re const comboModels = await getComboModels(modelStr); if (comboModels) { const chatSettings = await getSettings(); - const comboStrategy = chatSettings.comboStrategy || "fallback"; + // Check for combo-specific strategy first, fallback to global + const comboStrategies = chatSettings.comboStrategies || {}; + const comboSpecificStrategy = comboStrategies[modelStr]?.fallbackStrategy; + const comboStrategy = comboSpecificStrategy || chatSettings.comboStrategy || "fallback"; + log.info("CHAT", `Combo "${modelStr}" with ${comboModels.length} models (strategy: ${comboStrategy})`); return handleComboChat({ body,