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,