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:
2026-08-17 09:49:44 +07:00
parent 1256f29d92
commit b5c0f10610
3 changed files with 109 additions and 0 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -13,6 +13,8 @@ const DEFAULT_SETTINGS = {
tailscaleUrl: "",
stickyRoundRobinLimit: 3,
providerStrategies: {},
providerTimeouts: {},
defaultTimeoutMs: null,
quotaVisibility: {},
comboStrategy: "fallback",
comboStickyRoundRobinLimit: 1,