diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index 2a70e19c..d499814b 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -286,6 +286,25 @@ export default function ProfilePage() { } }; + const handleGlobalTimeoutChange = async (e) => { + const raw = e.target.value.replace(/[^0-9]/g, ""); + const numTimeout = parseInt(raw, 10); + const patchValue = (raw !== "" && Number.isFinite(numTimeout) && numTimeout > 0) ? numTimeout : null; + + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ defaultTimeoutMs: patchValue }), + }); + if (res.ok) { + setSettings(prev => ({ ...prev, defaultTimeoutMs: patchValue })); + } + } catch (err) { + console.error("Failed to update default timeout:", err); + } + }; + const updateStickyLimit = async (limit) => { const numLimit = parseInt(limit); if (isNaN(numLimit) || numLimit < 1) return; @@ -1520,6 +1539,43 @@ export default function ProfilePage() { + {/* Default Timeout — global default for all providers */} + +
+
+ timer +
+

Default Connect Timeout

+
+
+
+
+

All Providers

+

+ Timeout for upstream connect (applies globally unless overridden per provider). Set to 0 or leave empty for system default (60s). +

+
+
+ + ms +
+
+

+ {settings.defaultTimeoutMs + ? `All providers will wait up to ${settings.defaultTimeoutMs}ms for a connection.` + : "Using system default (60s) — configure per-provider timeout on each provider's detail page for fine-grained control."} +

+
+
+ {/* Network */}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 764c59c2..494f055c 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -65,6 +65,7 @@ export default function ProviderDetailPage() { const [providerStrategy, setProviderStrategy] = useState(null); const [providerStickyLimit, setProviderStickyLimit] = useState(""); const [providerNoAuthEnabled, setProviderNoAuthEnabled] = useState(true); + const [providerTimeout, setProviderTimeout] = useState(""); const [thinkingMode, setThinkingMode] = useState("auto"); const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} }); const [suggestedModels, setSuggestedModels] = useState([]); @@ -323,6 +324,9 @@ export default function ProviderDetailPage() { setProviderStrategy(override.fallbackStrategy || null); setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1"); setProviderNoAuthEnabled(override.enabled !== false); + // Load per-provider connect timeout + const timeoutCfg = (settingsData.providerTimeouts || {})[providerId] || {}; + setProviderTimeout(timeoutCfg.timeoutMs != null ? String(timeoutCfg.timeoutMs) : ""); // Load per-provider thinking config const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {}; setThinkingMode(thinkingCfg.mode || "auto"); @@ -459,6 +463,38 @@ export default function ProviderDetailPage() { saveThinkingConfig(mode); }; + const saveProviderTimeout = async (ms) => { + try { + const settingsRes = await fetch("/api/settings", { cache: "no-store" }); + const settingsData = settingsRes.ok ? await settingsRes.json() : {}; + const current = settingsData.providerTimeouts || {}; + const updated = { ...current }; + if (!ms || ms === "") { + delete updated[providerId]; + } else { + const timeoutMs = parseInt(ms, 10); + if (Number.isFinite(timeoutMs) && timeoutMs > 0) { + updated[providerId] = { timeoutMs }; + } else { + delete updated[providerId]; + } + } + await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ providerTimeouts: updated }), + }); + } catch (error) { + console.log("Error saving provider timeout:", error); + } + }; + + const handleTimeoutChange = (value) => { + const cleaned = value.replace(/[^0-9]/g, ""); + setProviderTimeout(cleaned); + saveProviderTimeout(cleaned); + }; + const saveAutoPing = async (next) => { const autoPingSettingsKey = AUTO_PING_SETTINGS_KEYS[providerId]; if (!autoPingSettingsKey) return; @@ -1678,6 +1714,21 @@ export default function ProviderDetailPage() { )} )} + {/* Connect Timeout */} +
+ Connect Timeout +
+ handleTimeoutChange(e.target.value)} + placeholder="default" + className="w-20 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary" + /> + ms +
+
{/* Round Robin toggle */}
Round Robin diff --git a/src/lib/db/repos/settingsRepo.js b/src/lib/db/repos/settingsRepo.js index a317840b..433660c7 100644 --- a/src/lib/db/repos/settingsRepo.js +++ b/src/lib/db/repos/settingsRepo.js @@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = { tailscaleUrl: "", stickyRoundRobinLimit: 3, providerStrategies: {}, + providerTimeouts: {}, + defaultTimeoutMs: null, quotaVisibility: {}, comboStrategy: "fallback", comboStickyRoundRobinLimit: 1,