feat: Claude auto-ping to warm 5h window after reset
Auto-sends a minimal request right after each Claude OAuth connection's 5h quota window resets, so a fresh window starts immediately without waiting. Per-connection toggle on providers and quota dashboards. - claudeAutoPing scheduler (server-side, 60s tick) hooked into initializeApp - per-connection enable map in settings.claudeAutoPing.connections - toggle + tooltip in ConnectionRow and ProviderLimits (Claude OAuth only) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,10 +3,10 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { getStatusVariant as getConnectionStatusVariant } from "@/shared/utils/connectionStatus";
|
||||
import PropTypes from "prop-types";
|
||||
import { Badge, Toggle } from "@/shared/components";
|
||||
import { Badge, Toggle, Tooltip } from "@/shared/components";
|
||||
import CooldownTimer from "./CooldownTimer";
|
||||
|
||||
export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete, oneByOneStatus = null }) {
|
||||
export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete, oneByOneStatus = null, autoPing = null }) {
|
||||
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
|
||||
const [updatingProxy, setUpdatingProxy] = useState(false);
|
||||
const proxyDropdownRef = useRef(null);
|
||||
@@ -235,6 +235,17 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{autoPing && (
|
||||
<Tooltip text="When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away.">
|
||||
<button
|
||||
onClick={() => autoPing.onToggle(!autoPing.on)}
|
||||
className={`flex w-full flex-col items-center rounded px-2 py-1 transition-colors hover:bg-black/5 dark:hover:bg-white/5 ${autoPing.on ? "text-primary" : "text-text-muted hover:text-primary"}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">bolt</span>
|
||||
<span className="text-[10px] leading-tight">Auto-ping</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<button onClick={onEdit} className="flex flex-col items-center rounded px-2 py-1 text-text-muted hover:bg-black/5 hover:text-primary dark:hover:bg-white/5">
|
||||
<span className="material-symbols-outlined text-[18px]">edit</span>
|
||||
<span className="text-[10px] leading-tight">Edit</span>
|
||||
@@ -288,4 +299,8 @@ ConnectionRow.propTypes = {
|
||||
state: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
}),
|
||||
autoPing: PropTypes.shape({
|
||||
on: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function ProviderDetailPage() {
|
||||
const [providerStrategy, setProviderStrategy] = useState(null);
|
||||
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
||||
const [thinkingMode, setThinkingMode] = useState("auto");
|
||||
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
|
||||
const [suggestedModels, setSuggestedModels] = useState([]);
|
||||
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
||||
const [disabledModelIds, setDisabledModelIds] = useState([]);
|
||||
@@ -258,6 +259,8 @@ export default function ProviderDetailPage() {
|
||||
// Load per-provider thinking config
|
||||
const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {};
|
||||
setThinkingMode(thinkingCfg.mode || "auto");
|
||||
const apCfg = settingsData.claudeAutoPing || {};
|
||||
setAutoPing({ enabled: apCfg.enabled === true, connections: apCfg.connections || {} });
|
||||
if (nodesRes.ok) {
|
||||
let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
|
||||
|
||||
@@ -370,6 +373,23 @@ export default function ProviderDetailPage() {
|
||||
saveThinkingConfig(mode);
|
||||
};
|
||||
|
||||
const saveAutoPing = async (next) => {
|
||||
setAutoPing(next);
|
||||
try {
|
||||
await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ claudeAutoPing: next }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error saving auto-ping config:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoPingConnection = (connectionId, on) => {
|
||||
saveAutoPing({ ...autoPing, connections: { ...autoPing.connections, [connectionId]: on } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConnections();
|
||||
fetchAliases();
|
||||
@@ -793,6 +813,10 @@ export default function ProviderDetailPage() {
|
||||
onMoveUp={() => handleSwapPriority(index, index - 1)}
|
||||
onMoveDown={() => handleSwapPriority(index, index + 1)}
|
||||
onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
|
||||
autoPing={providerId === "claude" && conn.authType === "oauth" ? {
|
||||
on: autoPing.connections[conn.id] === true,
|
||||
onToggle: (on) => handleAutoPingConnection(conn.id, on),
|
||||
} : null}
|
||||
onUpdateProxy={async (proxyPoolId) => {
|
||||
try {
|
||||
const res = await fetch(`/api/providers/${conn.id}`, {
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function ProviderLimits() {
|
||||
const [loading, setLoading] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [autoPingMap, setAutoPingMap] = useState({});
|
||||
const [lastUpdated, setLastUpdated] = useState(null);
|
||||
const [hasHydratedAutoRefresh, setHasHydratedAutoRefresh] = useState(false);
|
||||
const [refreshingAll, setRefreshingAll] = useState(false);
|
||||
@@ -423,6 +424,31 @@ export default function ProviderLimits() {
|
||||
window.localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, String(autoRefresh));
|
||||
}, [autoRefresh, hasHydratedAutoRefresh]);
|
||||
|
||||
// Load Claude auto-ping per-connection map
|
||||
useEffect(() => {
|
||||
fetch("/api/settings", { cache: "no-store" })
|
||||
.then((r) => (r.ok ? r.json() : {}))
|
||||
.then((s) => setAutoPingMap(s?.claudeAutoPing?.connections || {}))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const toggleAutoPing = useCallback(async (connectionId, on) => {
|
||||
const next = { ...autoPingMap, [connectionId]: on };
|
||||
setAutoPingMap(next);
|
||||
try {
|
||||
const r = await fetch("/api/settings", { cache: "no-store" });
|
||||
const s = r.ok ? await r.json() : {};
|
||||
const cfg = { ...(s.claudeAutoPing || {}), connections: next };
|
||||
await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ claudeAutoPing: cfg }),
|
||||
});
|
||||
} catch {
|
||||
setAutoPingMap(autoPingMap);
|
||||
}
|
||||
}, [autoPingMap]);
|
||||
|
||||
// Auto-refresh interval
|
||||
useEffect(() => {
|
||||
if (!hasHydratedAutoRefresh || !autoRefresh) {
|
||||
@@ -793,6 +819,7 @@ export default function ProviderLimits() {
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
{/* Refresh all button */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -898,6 +925,18 @@ export default function ProviderLimits() {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{conn.provider === "claude" && conn.authType === "oauth" && (
|
||||
<Tooltip text="When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away.">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAutoPing(conn.id, !(autoPingMap[conn.id] === true))}
|
||||
aria-label="Toggle auto-ping"
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-colors hover:bg-black/5 dark:hover:bg-white/5 ${autoPingMap[conn.id] === true ? "text-primary" : "text-text-muted"}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">bolt</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip text="Refresh quota">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user