api9nin / src /shared /services /claudeAutoPing.js
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
4.18 kB
// Claude auto-ping scheduler: warms the 5h window by sending a tiny request right after reset.
import "open-sse/index.js";
import { getSettings, getProviderConnections, updateProviderConnection } from "@/lib/localDb";
import { getClaudeUsage } from "open-sse/services/usage/claude.js";
import { CLAUDE_CLI_SPOOF_HEADERS } from "open-sse/providers/shared.js";
import { proxyAwareFetch } from "open-sse/utils/proxyFetch.js";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { refreshAndUpdateCredentials } from "@/app/api/usage/[connectionId]/route.js";
import { CLAUDE_AUTOPING_CONFIG } from "@/shared/constants/config";
const C = CLAUDE_AUTOPING_CONFIG;
const PING_URL = "https://api.anthropic.com/v1/messages?beta=true";
const g = (global.__claudeAutoPing ??= { interval: null, running: false, resetCache: {} });
function buildProxyOptions(cfg) {
return {
connectionProxyEnabled: cfg.connectionProxyEnabled === true,
connectionProxyUrl: cfg.connectionProxyUrl || "",
connectionNoProxy: cfg.connectionNoProxy || "",
vercelRelayUrl: cfg.vercelRelayUrl || "",
strictProxy: false,
};
}
// Send minimal "hi" to start a fresh 5h window
async function sendPing(accessToken, proxyOptions) {
const res = await proxyAwareFetch(PING_URL, {
method: "POST",
headers: {
...CLAUDE_CLI_SPOOF_HEADERS,
"Authorization": `Bearer ${accessToken}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: C.pingModel,
max_tokens: C.pingMaxTokens,
messages: [{ role: "user", content: C.pingText }],
}),
}, proxyOptions);
return res.ok;
}
async function pingConnection(conn) {
// Cached resetAt is stable for the whole 5h window; skip usage poll until near reset
const cachedReset = g.resetCache[conn.id];
if (cachedReset && Date.now() < new Date(cachedReset).getTime() - C.refreshAheadMs) return;
const proxyCfg = await resolveConnectionProxyConfig(conn.providerSpecificData);
const proxyOptions = buildProxyOptions(proxyCfg);
// Refresh token if needed, then read 5h reset time
let connection = conn;
try {
const r = await refreshAndUpdateCredentials(connection, false, proxyOptions);
connection = r.connection;
} catch (e) {
console.warn(`[AutoPing] ${conn.id}: refresh failed: ${e.message}`);
return;
}
const usage = await getClaudeUsage(connection.accessToken, proxyOptions);
const resetAt = usage?.quotas?.[C.fiveHourKey]?.resetAt;
if (!resetAt) return;
// Cache resetAt to gate future ticks
g.resetCache[conn.id] = resetAt;
const resetMs = new Date(resetAt).getTime();
const now = Date.now();
// Only ping once per reset cycle, right after window flips
if (now < resetMs - C.pingLeadMs) return;
if (connection.lastPingedResetAt === resetAt) return;
const ok = await sendPing(connection.accessToken, proxyOptions);
await updateProviderConnection(connection.id, {
lastPingedResetAt: resetAt,
lastPingAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
console.log(`[AutoPing] ${connection.id}: ping ${ok ? "sent" : "failed"} (reset ${resetAt})`);
}
async function tick() {
if (g.running) return;
g.running = true;
try {
const settings = await getSettings();
const enabledMap = settings[C.settingsKey]?.connections || {};
if (Object.keys(enabledMap).length === 0) return;
const conns = await getProviderConnections({ provider: "claude", isActive: true });
// Only ping connections the user explicitly enabled
const targets = conns.filter((c) => c.authType === "oauth" && enabledMap[c.id] === true);
if (targets.length === 0) return;
for (const conn of targets) {
try {
await pingConnection(conn);
} catch (e) {
console.warn(`[AutoPing] ${conn.id}: ${e.message}`);
}
}
} catch (e) {
console.warn("[AutoPing] tick error:", e.message);
} finally {
g.running = false;
}
}
export function startClaudeAutoPing() {
if (g.interval) return;
g.interval = setInterval(() => { tick().catch(() => {}); }, C.tickIntervalMs);
if (g.interval.unref) g.interval.unref();
}