diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 69f7ea86ad..46dc3953f3 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -310,7 +310,8 @@ export async function handleComboChat({ const parsed = parseModel(modelStr); const provider = parsed.provider || parsed.providerAlias || "unknown"; const profile = getProviderProfile(provider); - const breaker = getCircuitBreaker(`combo:${provider}`, { + const breakerKey = `combo:${modelStr}`; + const breaker = getCircuitBreaker(breakerKey, { failureThreshold: profile.circuitBreakerThreshold, resetTimeout: profile.circuitBreakerReset, }); @@ -440,8 +441,7 @@ export async function handleComboChat({ // Early exit: check if all models have breaker OPEN const allBreakersOpen = orderedModels.every((m) => { - const p = parseModel(m).provider || parseModel(m).providerAlias || "unknown"; - return !getCircuitBreaker(`combo:${p}`).canExecute(); + return !getCircuitBreaker(`combo:${m}`).canExecute(); }); // All models failed @@ -532,7 +532,8 @@ async function handleRoundRobinCombo({ const parsed = parseModel(modelStr); const provider = parsed.provider || parsed.providerAlias || "unknown"; const profile = getProviderProfile(provider); - const breaker = getCircuitBreaker(`combo:${provider}`, { + const breakerKey = `combo:${modelStr}`; + const breaker = getCircuitBreaker(breakerKey, { failureThreshold: profile.circuitBreakerThreshold, resetTimeout: profile.circuitBreakerReset, }); @@ -694,8 +695,7 @@ async function handleRoundRobinCombo({ // Early exit: check if all models have breaker OPEN const allBreakersOpen = orderedModels.every((m) => { - const p = parseModel(m).provider || parseModel(m).providerAlias || "unknown"; - return !getCircuitBreaker(`combo:${p}`).canExecute(); + return !getCircuitBreaker(`combo:${m}`).canExecute(); }); if (allBreakersOpen) { diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 1bebb37da0..a3c6d6ea1b 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -1338,6 +1338,8 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { const [customModels, setCustomModels] = useState([]); const [newModelId, setNewModelId] = useState(""); const [newModelName, setNewModelName] = useState(""); + const [newApiFormat, setNewApiFormat] = useState("chat-completions"); + const [newEndpoints, setNewEndpoints] = useState(["chat"]); const [adding, setAdding] = useState(false); const [loading, setLoading] = useState(true); @@ -1370,11 +1372,15 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { provider: providerId, modelId: newModelId.trim(), modelName: newModelName.trim() || undefined, + apiFormat: newApiFormat, + supportedEndpoints: newEndpoints, }), }); if (res.ok) { setNewModelId(""); setNewModelName(""); + setNewApiFormat("chat-completions"); + setNewEndpoints(["chat"]); await fetchCustomModels(); } } catch (e) { @@ -1407,38 +1413,89 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) {

{t("customModelsHint")}

{/* Add form */} -
-
- - setNewModelId(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder={t("customModelPlaceholder")} - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> +
+
+
+ + setNewModelId(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + placeholder={t("customModelPlaceholder")} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> +
+
+ + setNewModelName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + placeholder={t("optional")} + className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> +
+
-
- - setNewModelName(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleAdd()} - placeholder={t("optional")} - className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" - /> + + {/* API Format + Supported Endpoints */} +
+
+ + +
+
+ Supported Endpoints +
+ {["chat", "embeddings", "images", "audio"].map((ep) => ( + + ))} +
+
-
{/* List */} @@ -1457,7 +1514,7 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { tune

{model.name || model.id}

-
+
{fullModel} @@ -1470,6 +1527,26 @@ function CustomModelsSection({ providerId, providerAlias, copied, onCopy }) { {copied === copyKey ? "check" : "content_copy"} + {model.apiFormat === "responses" && ( + + Responses + + )} + {model.supportedEndpoints?.includes("embeddings") && ( + + 📐 Embed + + )} + {model.supportedEndpoints?.includes("images") && ( + + 🖼️ Images + + )} + {model.supportedEndpoints?.includes("audio") && ( + + 🔊 Audio + + )}