feat(settings): restore per-provider connect timeout overrides
Re-apply the settings/UI layer of the per-provider timeout feature that
was dropped during the origin/master merge (core providerTimeout.js +
executor wiring survived; the settings keys and dashboard UI did not):
- settingsRepo: providerTimeouts:{} + defaultTimeoutMs:null defaults
- Profile page: "Default Connect Timeout" card (global fallback, ms)
- Provider detail page: per-provider "Connect Timeout" input, saved to
providerTimeouts[providerId].timeoutMs, applied via
resolveProviderTimeoutMs() priority: per-provider > global > registry > env
This commit is contained in:
@@ -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() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Default Timeout — global default for all providers */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">timer</span>
|
||||
</div>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Default Connect Timeout</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">All Providers</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Timeout for upstream connect (applies globally unless overridden per provider). Set to 0 or leave empty for system default (60s).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="60000"
|
||||
value={settings.defaultTimeoutMs != null ? String(settings.defaultTimeoutMs) : ""}
|
||||
onChange={handleGlobalTimeoutChange}
|
||||
disabled={loading}
|
||||
className="w-20 text-center"
|
||||
/>
|
||||
<span className="text-xs text-text-muted shrink-0">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted italic">
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Network */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
|
||||
@@ -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 */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-text-muted font-medium">Connect Timeout</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={providerTimeout}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-xs text-text-muted">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Round Robin toggle */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||
|
||||
@@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = {
|
||||
tailscaleUrl: "",
|
||||
stickyRoundRobinLimit: 3,
|
||||
providerStrategies: {},
|
||||
providerTimeouts: {},
|
||||
defaultTimeoutMs: null,
|
||||
quotaVisibility: {},
|
||||
comboStrategy: "fallback",
|
||||
comboStickyRoundRobinLimit: 1,
|
||||
|
||||
Reference in New Issue
Block a user