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 updateStickyLimit = async (limit) => {
|
||||||
const numLimit = parseInt(limit);
|
const numLimit = parseInt(limit);
|
||||||
if (isNaN(numLimit) || numLimit < 1) return;
|
if (isNaN(numLimit) || numLimit < 1) return;
|
||||||
@@ -1520,6 +1539,43 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</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 */}
|
{/* Network */}
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export default function ProviderDetailPage() {
|
|||||||
const [providerStrategy, setProviderStrategy] = useState(null);
|
const [providerStrategy, setProviderStrategy] = useState(null);
|
||||||
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
||||||
const [providerNoAuthEnabled, setProviderNoAuthEnabled] = useState(true);
|
const [providerNoAuthEnabled, setProviderNoAuthEnabled] = useState(true);
|
||||||
|
const [providerTimeout, setProviderTimeout] = useState("");
|
||||||
const [thinkingMode, setThinkingMode] = useState("auto");
|
const [thinkingMode, setThinkingMode] = useState("auto");
|
||||||
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
|
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
|
||||||
const [suggestedModels, setSuggestedModels] = useState([]);
|
const [suggestedModels, setSuggestedModels] = useState([]);
|
||||||
@@ -323,6 +324,9 @@ export default function ProviderDetailPage() {
|
|||||||
setProviderStrategy(override.fallbackStrategy || null);
|
setProviderStrategy(override.fallbackStrategy || null);
|
||||||
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
|
setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1");
|
||||||
setProviderNoAuthEnabled(override.enabled !== false);
|
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
|
// Load per-provider thinking config
|
||||||
const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {};
|
const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {};
|
||||||
setThinkingMode(thinkingCfg.mode || "auto");
|
setThinkingMode(thinkingCfg.mode || "auto");
|
||||||
@@ -459,6 +463,38 @@ export default function ProviderDetailPage() {
|
|||||||
saveThinkingConfig(mode);
|
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 saveAutoPing = async (next) => {
|
||||||
const autoPingSettingsKey = AUTO_PING_SETTINGS_KEYS[providerId];
|
const autoPingSettingsKey = AUTO_PING_SETTINGS_KEYS[providerId];
|
||||||
if (!autoPingSettingsKey) return;
|
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 */}
|
{/* Round Robin toggle */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
tailscaleUrl: "",
|
tailscaleUrl: "",
|
||||||
stickyRoundRobinLimit: 3,
|
stickyRoundRobinLimit: 3,
|
||||||
providerStrategies: {},
|
providerStrategies: {},
|
||||||
|
providerTimeouts: {},
|
||||||
|
defaultTimeoutMs: null,
|
||||||
quotaVisibility: {},
|
quotaVisibility: {},
|
||||||
comboStrategy: "fallback",
|
comboStrategy: "fallback",
|
||||||
comboStickyRoundRobinLimit: 1,
|
comboStickyRoundRobinLimit: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user