From b66b5c68cedf179cd26bd3c8479113379249ca53 Mon Sep 17 00:00:00 2001
From: Emirhan
Date: Mon, 29 Jun 2026 15:10:42 +0700
Subject: [PATCH] feat(quota): add opt-in Codex auto-ping
Generalize Claude 5h auto-ping into a provider-generic scheduler and add
opt-in Codex auto-ping that warms the next 5h window via a tiny gpt-5.5
request when session.resetAt slides. Default off, per-connection toggle,
failure cooldown, blocking-quota skip, drains stream before success.
Closes #2107
Co-authored-by: Cursor
---
.../dashboard/providers/[id]/ConnectionRow.js | 6 +-
.../dashboard/providers/[id]/page.js | 16 +-
.../usage/components/ProviderLimits/index.js | 90 +++--
src/app/api/settings/route.js | 11 +
src/shared/constants/config.js | 32 +-
src/shared/services/claudeAutoPing.js | 117 ------
src/shared/services/initializeApp.js | 4 +-
src/shared/services/quotaAutoPing.js | 298 +++++++++++++++
tests/unit/quota-auto-ping.test.js | 351 ++++++++++++++++++
9 files changed, 757 insertions(+), 168 deletions(-)
delete mode 100644 src/shared/services/claudeAutoPing.js
create mode 100644 src/shared/services/quotaAutoPing.js
create mode 100644 tests/unit/quota-auto-ping.test.js
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js
index 15096050..0b5bea19 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/ConnectionRow.js
@@ -23,6 +23,9 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
: hasLegacyProxy
? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`
: "";
+ const autoPingTooltip = autoPing?.provider === "codex"
+ ? "Auto-starts the next 5h Codex window after reset by sending a tiny gpt-5.5 request. Consumes a small amount of quota."
+ : "When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away.";
let maskedProxyUrl = "";
if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) {
@@ -244,7 +247,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
)}
{autoPing && (
-
+
) : null}
- {isCodex && (
-
- Reset eligible: {resetCreditCount}
-
- )}
{conn.provider === "kiro" && (
@@ -995,41 +1008,42 @@ export default function ProviderLimits() {
{isCodex && (
-
- 0
- ? "border-primary/30 bg-primary/5 text-primary"
- : "border-black/10 bg-black/[0.02] text-text-muted dark:border-white/10 dark:bg-white/[0.03]"
- }`}
- >
- restart_alt
- {resetCreditCount}
-
-
- )}
- {isCodex && resetCreditCount > 0 && (
-
+ 0
+ ? `Use one Codex reset credit. Available: ${resetCreditCount}`
+ : "No Codex reset credits available"
+ }
+ >
)}
- {conn.provider === "claude" && conn.authType === "oauth" && (
-
+ {AUTO_PING_SETTINGS_KEYS[conn.provider] && conn.authType === "oauth" && (
+
diff --git a/src/app/api/settings/route.js b/src/app/api/settings/route.js
index ee2682ca..ccbaee3a 100644
--- a/src/app/api/settings/route.js
+++ b/src/app/api/settings/route.js
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getSettings, updateSettings } from "@/lib/localDb";
import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy";
import { resetComboRotation } from "open-sse/services/combo.js";
+import { runQuotaAutoPingTick } from "@/shared/services/quotaAutoPing";
import bcrypt from "bcryptjs";
export const dynamic = "force-dynamic";
@@ -96,6 +97,16 @@ export async function PATCH(request) {
resetComboRotation();
}
+ if (
+ Object.prototype.hasOwnProperty.call(body, "claudeAutoPing") ||
+ Object.prototype.hasOwnProperty.call(body, "codexAutoPing")
+ ) {
+ // Run once immediately after opt-in changes so users don't wait for the next scheduler tick.
+ runQuotaAutoPingTick().catch((error) => {
+ console.warn("[AutoPing] settings-triggered tick failed:", error.message);
+ });
+ }
+
const { password, oidcClientSecret, ...safeSettings } = settings;
safeSettings.oidcConfigured = !!(safeSettings.oidcIssuerUrl && safeSettings.oidcClientId && oidcClientSecret);
return NextResponse.json(safeSettings, { headers: SETTINGS_RESPONSE_HEADERS });
diff --git a/src/shared/constants/config.js b/src/shared/constants/config.js
index ebf5d120..0650d086 100644
--- a/src/shared/constants/config.js
+++ b/src/shared/constants/config.js
@@ -62,16 +62,34 @@ export const CONSOLE_LOG_CONFIG = {
// Client-side store TTL: how long fetched data stays fresh before re-fetching
export const CLIENT_STORE_TTL_MS = 60000;
-// Claude auto-ping: keep 5h window warm by sending a tiny request right after reset
-export const CLAUDE_AUTOPING_CONFIG = {
- settingsKey: "claudeAutoPing", // settings table field
+// Quota auto-ping: keep 5h windows warm by sending a tiny request right after reset.
+export const QUOTA_AUTOPING_CONFIG = {
tickIntervalMs: 60000, // scheduler tick
pingLeadMs: 5000, // fire once reset passes (within tolerance)
- pingModel: "claude-haiku-4-5-20251001", // cheapest model
- pingText: "hi",
- pingMaxTokens: 1,
refreshAheadMs: 300000, // refetch usage when within 5min of reset
- fiveHourKey: "session (5h)", // quota key returned by usage handler
+ failureCooldownMs: 900000, // avoid failed ping spam while upstream/auth is unhealthy
+ providers: {
+ claude: {
+ settingsKey: "claudeAutoPing", // preserve existing settings contract
+ quotaKey: "session (5h)", // quota key returned by usage handler
+ pingModel: "claude-haiku-4-5-20251001",
+ pingText: "hi",
+ pingMaxTokens: 1,
+ },
+ codex: {
+ settingsKey: "codexAutoPing",
+ quotaKey: "session",
+ pingWhenResetAtSlides: true,
+ resetAtDriftMs: 30000,
+ minPingIntervalMs: 600000,
+ skipWhenBlockingQuotaExhausted: true,
+ // Free and Plus Codex accounts both expose gpt-5.5; avoid fallback probes that waste requests.
+ pingModel: "gpt-5.5",
+ pingText: "hi",
+ pingInstructions: "Reply with OK.",
+ pingReasoningEffort: "none",
+ },
+ },
};
// Re-export from providers.js for backward compatibility
diff --git a/src/shared/services/claudeAutoPing.js b/src/shared/services/claudeAutoPing.js
deleted file mode 100644
index 14abc127..00000000
--- a/src/shared/services/claudeAutoPing.js
+++ /dev/null
@@ -1,117 +0,0 @@
-// 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();
-}
diff --git a/src/shared/services/initializeApp.js b/src/shared/services/initializeApp.js
index 234c90d6..e2914e0d 100644
--- a/src/shared/services/initializeApp.js
+++ b/src/shared/services/initializeApp.js
@@ -14,7 +14,7 @@ import {
WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, VIRTUAL_IFACE_REGEX,
} from "@/lib/tunnel";
import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager";
-import { startClaudeAutoPing } from "@/shared/services/claudeAutoPing";
+import { startQuotaAutoPing } from "@/shared/services/quotaAutoPing";
import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache";
// Inject correct paths and DB hooks into manager.js (CJS) from ESM context
@@ -89,7 +89,7 @@ export async function initializeApp() {
startWatchdog();
startNetworkMonitor();
autoStartMitm();
- startClaudeAutoPing();
+ startQuotaAutoPing();
} catch (error) {
console.error("[InitApp] Error:", error);
}
diff --git a/src/shared/services/quotaAutoPing.js b/src/shared/services/quotaAutoPing.js
new file mode 100644
index 00000000..694a5e2b
--- /dev/null
+++ b/src/shared/services/quotaAutoPing.js
@@ -0,0 +1,298 @@
+// Quota auto-ping scheduler: warms 5h windows by sending tiny opt-in requests 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 { getCodexUsage } from "open-sse/services/usage/codex.js";
+import { getExecutor } from "open-sse/executors/index.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 { QUOTA_AUTOPING_CONFIG } from "@/shared/constants/config";
+
+const C = QUOTA_AUTOPING_CONFIG;
+const CLAUDE_PING_URL = "https://api.anthropic.com/v1/messages?beta=true";
+
+const providerHandlers = {
+ claude: {
+ getUsage: getClaudeUsage,
+ sendPing: sendClaudePing,
+ },
+ codex: {
+ getUsage: getCodexUsage,
+ sendPing: sendCodexPing,
+ },
+};
+
+// Survive Next.js hot reload and keep one scheduler per server process.
+const g = (global.__quotaAutoPing ??= {
+ interval: null,
+ running: false,
+ resetCache: {},
+ failureCache: {},
+});
+
+function cacheKey(provider, connectionId) {
+ return `${provider}:${connectionId}`;
+}
+
+function normalizeResetKey(resetAt) {
+ const ms = new Date(resetAt).getTime();
+ if (!Number.isFinite(ms)) return resetAt;
+ return new Date(Math.floor(ms / 60000) * 60000).toISOString();
+}
+
+function getResetDriftMs(previousResetAt, nextResetAt) {
+ const previousMs = new Date(previousResetAt).getTime();
+ const nextMs = new Date(nextResetAt).getTime();
+ if (!Number.isFinite(previousMs) || !Number.isFinite(nextMs)) return 0;
+ return nextMs - previousMs;
+}
+
+function toFiniteNumber(value, fallback = null) {
+ if (typeof value === "number" && Number.isFinite(value)) return value;
+ if (typeof value === "string" && value.trim()) {
+ const parsed = Number(value);
+ if (Number.isFinite(parsed)) return parsed;
+ }
+ return fallback;
+}
+
+function isQuotaExhausted(quota) {
+ if (!quota || quota.unlimited === true) return false;
+ const remaining = toFiniteNumber(quota.remaining);
+ if (remaining !== null) return remaining <= 0;
+
+ const used = toFiniteNumber(quota.used);
+ const total = toFiniteNumber(quota.total);
+ return total !== null && total > 0 && used !== null && used >= total;
+}
+
+function wasPingedRecently(connection, intervalMs, nowMs = Date.now()) {
+ if (!intervalMs) return false;
+ const lastPingAtMs = new Date(connection.lastPingAt).getTime();
+ return Number.isFinite(lastPingAtMs) && nowMs - lastPingAtMs < intervalMs;
+}
+
+function isBlockingQuotaName(name, sessionKey) {
+ if (name === sessionKey) return false;
+ return !String(name).toLowerCase().includes("session");
+}
+
+function hasExhaustedBlockingQuota(quotas, sessionKey) {
+ return Object.entries(quotas || {}).some(([name, quota]) => isBlockingQuotaName(name, sessionKey) && isQuotaExhausted(quota));
+}
+
+function shouldPingForReset(providerConfig, cachedReset, resetAt, now) {
+ if (providerConfig.pingWhenResetAtSlides) {
+ return Boolean(cachedReset) && getResetDriftMs(cachedReset, resetAt) >= (providerConfig.resetAtDriftMs || 0);
+ }
+
+ const resetMs = new Date(resetAt).getTime();
+ return Number.isFinite(resetMs) && now >= resetMs - C.pingLeadMs;
+}
+
+function buildProxyOptions(cfg) {
+ return {
+ connectionProxyEnabled: cfg.connectionProxyEnabled === true,
+ connectionProxyUrl: cfg.connectionProxyUrl || "",
+ connectionNoProxy: cfg.connectionNoProxy || "",
+ vercelRelayUrl: cfg.vercelRelayUrl || "",
+ strictProxy: false,
+ };
+}
+
+async function sendClaudePing(connection, providerConfig, proxyOptions, deps) {
+ const res = await deps.proxyAwareFetch(CLAUDE_PING_URL, {
+ method: "POST",
+ headers: {
+ ...CLAUDE_CLI_SPOOF_HEADERS,
+ "Authorization": `Bearer ${connection.accessToken}`,
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ model: providerConfig.pingModel,
+ max_tokens: providerConfig.pingMaxTokens,
+ messages: [{ role: "user", content: providerConfig.pingText }],
+ }),
+ }, proxyOptions);
+ return res.ok;
+}
+
+function buildCodexPingInput(text) {
+ return [{
+ type: "message",
+ role: "user",
+ content: [{ type: "input_text", text }],
+ }];
+}
+
+async function drainResponseBody(response) {
+ if (typeof response?.text === "function") {
+ await response.text();
+ return;
+ }
+
+ const reader = response?.body?.getReader?.();
+ if (!reader) return;
+
+ try {
+ while (true) {
+ const { done } = await reader.read();
+ if (done) return;
+ }
+ } finally {
+ reader.releaseLock?.();
+ }
+}
+
+async function sendCodexPing(connection, providerConfig, proxyOptions, deps) {
+ const executor = deps.getExecutor("codex");
+ const { response } = await executor.execute({
+ model: providerConfig.pingModel,
+ stream: true,
+ credentials: {
+ accessToken: connection.accessToken,
+ connectionId: connection.id,
+ providerSpecificData: connection.providerSpecificData,
+ },
+ proxyOptions,
+ log: console,
+ body: {
+ model: providerConfig.pingModel,
+ input: buildCodexPingInput(providerConfig.pingText),
+ instructions: providerConfig.pingInstructions,
+ reasoning: providerConfig.pingReasoningEffort
+ ? { effort: providerConfig.pingReasoningEffort, summary: "auto" }
+ : undefined,
+ store: false,
+ stream: true,
+ },
+ });
+ if (!response.ok) {
+ try { await response.body?.cancel?.(); } catch { /* noop */ }
+ return false;
+ }
+
+ // Codex only starts the 5h window after the streaming response completes.
+ await drainResponseBody(response);
+ return true;
+}
+
+function shouldSkipAfterFailure(state, key, nowMs = Date.now()) {
+ const failedAt = state.failureCache[key];
+ return failedAt && nowMs - failedAt < C.failureCooldownMs;
+}
+
+async function pingConnection(conn, provider, providerConfig, handler, deps, state = g) {
+ const key = cacheKey(provider, conn.id);
+
+ // resetAt is stable for time-based windows; Codex polls every tick because inactive windows slide forward.
+ const cachedReset = state.resetCache[key];
+ if (!providerConfig.pingWhenResetAtSlides && cachedReset && Date.now() < new Date(cachedReset).getTime() - C.refreshAheadMs) return;
+
+ // Avoid hammering provider auth/quota endpoints if a ping failed recently.
+ if (shouldSkipAfterFailure(state, key)) return;
+
+ const proxyCfg = await deps.resolveConnectionProxyConfig(conn.providerSpecificData);
+ const proxyOptions = buildProxyOptions(proxyCfg);
+
+ let connection = conn;
+ try {
+ const r = await deps.refreshAndUpdateCredentials(connection, false, proxyOptions);
+ connection = r.connection;
+ } catch (e) {
+ state.failureCache[key] = Date.now();
+ console.warn(`[AutoPing] ${provider}:${conn.id}: refresh failed: ${e.message}`);
+ return;
+ }
+
+ const usage = await handler.getUsage(connection.accessToken, proxyOptions);
+ const quotas = usage?.quotas || {};
+ const quota = quotas?.[providerConfig.quotaKey];
+ const resetAt = quota?.resetAt;
+ if (!resetAt) return;
+
+ state.resetCache[key] = resetAt;
+
+ if (providerConfig.skipWhenBlockingQuotaExhausted && hasExhaustedBlockingQuota(quotas, providerConfig.quotaKey)) return;
+ if (isQuotaExhausted(quota)) return;
+
+ const now = Date.now();
+ const resetKey = normalizeResetKey(resetAt);
+ const lastPingedResetKey = connection.lastPingedResetKey || normalizeResetKey(connection.lastPingedResetAt);
+
+ // Claude waits for reset. Codex pings only when resetAt slides, which means the 5h window is inactive.
+ if (!shouldPingForReset(providerConfig, cachedReset, resetAt, now)) return;
+ if (wasPingedRecently(connection, providerConfig.minPingIntervalMs, now)) return;
+ if (lastPingedResetKey === resetKey) return;
+
+ const ok = await handler.sendPing(connection, providerConfig, proxyOptions, deps);
+ if (!ok) {
+ // Do not mark reset as pinged unless upstream accepted the tiny request.
+ state.failureCache[key] = Date.now();
+ console.warn(`[AutoPing] ${provider}:${connection.id}: ping failed (reset ${resetAt})`);
+ return;
+ }
+
+ delete state.failureCache[key];
+ await deps.updateProviderConnection(connection.id, {
+ lastPingedResetAt: resetAt,
+ lastPingedResetKey: resetKey,
+ lastPingAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ });
+ console.log(`[AutoPing] ${provider}:${connection.id}: ping sent (reset ${resetAt})`);
+}
+
+function createDefaultDeps() {
+ return {
+ getSettings,
+ getProviderConnections,
+ updateProviderConnection,
+ resolveConnectionProxyConfig,
+ refreshAndUpdateCredentials,
+ proxyAwareFetch,
+ getExecutor,
+ };
+}
+
+export async function runQuotaAutoPingTick(deps = createDefaultDeps(), state = g) {
+ if (state.running) return;
+ state.running = true;
+ try {
+ const settings = await deps.getSettings();
+
+ for (const [provider, providerConfig] of Object.entries(C.providers)) {
+ const handler = providerHandlers[provider];
+ if (!handler) continue;
+
+ const enabledMap = settings?.[providerConfig.settingsKey]?.connections || {};
+ if (Object.keys(enabledMap).length === 0) continue;
+
+ const conns = await deps.getProviderConnections({ provider, isActive: true });
+ const targets = conns.filter((conn) => conn.authType === "oauth" && enabledMap[conn.id] === true);
+ for (const conn of targets) {
+ try {
+ await pingConnection(conn, provider, providerConfig, handler, deps, state);
+ } catch (e) {
+ state.failureCache[cacheKey(provider, conn.id)] = Date.now();
+ console.warn(`[AutoPing] ${provider}:${conn.id}: ${e.message}`);
+ }
+ }
+ }
+ } catch (e) {
+ console.warn("[AutoPing] tick error:", e.message);
+ } finally {
+ state.running = false;
+ }
+}
+
+export function startQuotaAutoPing() {
+ if (g.interval) return;
+ console.log("[AutoPing] scheduler started");
+ runQuotaAutoPingTick().catch(() => {});
+ g.interval = setInterval(() => { runQuotaAutoPingTick().catch(() => {}); }, C.tickIntervalMs);
+ if (g.interval.unref) g.interval.unref();
+}
diff --git a/tests/unit/quota-auto-ping.test.js b/tests/unit/quota-auto-ping.test.js
new file mode 100644
index 00000000..de601df5
--- /dev/null
+++ b/tests/unit/quota-auto-ping.test.js
@@ -0,0 +1,351 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("open-sse/index.js", () => ({}), { virtual: true });
+
+vi.mock("@/lib/localDb", () => ({
+ getSettings: vi.fn(),
+ getProviderConnections: vi.fn(),
+ updateProviderConnection: vi.fn(),
+}));
+
+vi.mock("@/lib/network/connectionProxy", () => ({
+ resolveConnectionProxyConfig: vi.fn(),
+}));
+
+vi.mock("@/app/api/usage/[connectionId]/route.js", () => ({
+ refreshAndUpdateCredentials: vi.fn(),
+}));
+
+vi.mock("@/shared/constants/config", () => ({
+ QUOTA_AUTOPING_CONFIG: {
+ tickIntervalMs: 60000,
+ pingLeadMs: 5000,
+ refreshAheadMs: 300000,
+ failureCooldownMs: 900000,
+ providers: {
+ claude: {
+ settingsKey: "claudeAutoPing",
+ quotaKey: "session (5h)",
+ pingModel: "claude-haiku-4-5-20251001",
+ pingText: "hi",
+ pingMaxTokens: 1,
+ },
+ codex: {
+ settingsKey: "codexAutoPing",
+ quotaKey: "session",
+ pingWhenResetAtSlides: true,
+ resetAtDriftMs: 30000,
+ minPingIntervalMs: 600000,
+ skipWhenBlockingQuotaExhausted: true,
+ pingModel: "gpt-5.5",
+ pingText: "hi",
+ pingInstructions: "Reply with OK.",
+ pingReasoningEffort: "none",
+ },
+ },
+ },
+}));
+
+vi.mock("open-sse/providers/shared.js", () => ({
+ CLAUDE_CLI_SPOOF_HEADERS: { "anthropic-version": "2023-06-01" },
+}));
+
+vi.mock("open-sse/services/usage/shared.js", () => ({
+ U: () => ({ baseUrl: "https://chatgpt.com/backend-api/codex/responses" }),
+}));
+
+vi.mock("open-sse/utils/proxyFetch.js", () => ({
+ proxyAwareFetch: vi.fn(),
+}));
+
+vi.mock("open-sse/services/usage/claude.js", () => ({
+ getClaudeUsage: vi.fn(),
+}));
+
+vi.mock("open-sse/services/usage/codex.js", () => ({
+ getCodexUsage: vi.fn(),
+}));
+
+vi.mock("open-sse/executors/index.js", () => ({
+ getExecutor: vi.fn(),
+}));
+
+describe("quota auto-ping", () => {
+ let runQuotaAutoPingTick;
+ let deps;
+ let state;
+ let getCodexUsage;
+ let getClaudeUsage;
+ let getExecutor;
+ let codexResponseText;
+
+ beforeEach(async () => {
+ vi.resetModules();
+ vi.clearAllMocks();
+ vi.useRealTimers();
+
+ ({ getCodexUsage } = await import("open-sse/services/usage/codex.js"));
+ ({ getClaudeUsage } = await import("open-sse/services/usage/claude.js"));
+ ({ getExecutor } = await import("open-sse/executors/index.js"));
+ ({ runQuotaAutoPingTick } = await import("../../src/shared/services/quotaAutoPing.js"));
+
+ deps = {
+ getSettings: vi.fn(),
+ getProviderConnections: vi.fn(),
+ updateProviderConnection: vi.fn(),
+ resolveConnectionProxyConfig: vi.fn().mockResolvedValue({}),
+ refreshAndUpdateCredentials: vi.fn(async (connection) => ({ connection, refreshed: false })),
+ proxyAwareFetch: vi.fn().mockResolvedValue({ ok: true }),
+ getExecutor: vi.fn(() => ({
+ execute: vi.fn().mockResolvedValue({ response: { ok: true, text: codexResponseText } }),
+ })),
+ };
+ codexResponseText = vi.fn().mockResolvedValue("");
+ getExecutor.mockReturnValue({
+ execute: vi.fn().mockResolvedValue({ response: { ok: true, text: codexResponseText } }),
+ });
+ state = { running: false, resetCache: {}, failureCache: {} };
+ vi.setSystemTime(new Date("2026-01-01T12:00:00.000Z"));
+ });
+
+ it("does not ping Codex when setting is absent", async () => {
+ deps.getSettings.mockResolvedValue({});
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getProviderConnections).not.toHaveBeenCalled();
+ expect(deps.proxyAwareFetch).not.toHaveBeenCalled();
+ });
+
+ it("does not ping Codex on the first resetAt observation", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 1, resetAt: "2026-01-01T13:00:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ expect(state.resetCache["codex:codex-1"]).toBe("2026-01-01T13:00:00.000Z");
+ });
+
+ it("sends Codex ping when session resetAt slides", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 1, total: 100, remaining: 99, resetAt: "2026-01-01T17:01:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ const executor = deps.getExecutor.mock.results[0].value;
+ expect(executor.execute).toHaveBeenCalledTimes(1);
+ expect(deps.updateProviderConnection).toHaveBeenCalledWith("codex-1", expect.objectContaining({
+ lastPingedResetAt: "2026-01-01T17:01:00.000Z",
+ lastPingedResetKey: "2026-01-01T17:01:00.000Z",
+ }));
+ });
+
+ it("does not ping Codex when resetAt is stable", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 1, total: 100, remaining: 99, resetAt: "2026-01-01T17:00:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ });
+
+ it("does not repeat Codex ping inside the minimum ping interval", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex"
+ ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token", lastPingAt: "2026-01-01T11:55:00.000Z" }]
+ : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 1, total: 100, remaining: 99, resetAt: "2026-01-01T17:01:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ });
+
+ it("does not ping Codex just because reported usage is zero", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 0, resetAt: "2026-01-01T17:00:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ expect(state.resetCache["codex:codex-1"]).toBe("2026-01-01T17:00:00.000Z");
+ });
+
+ it("does not ping Codex when weekly quota is exhausted", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: {
+ session: { used: 0, total: 100, remaining: 100, resetAt: "2026-01-01T17:01:00.000Z" },
+ weekly: { used: 100, total: 100, remaining: 0, resetAt: "2026-01-03T12:00:00.000Z" },
+ },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ });
+
+ it("does not ping Codex when monthly quota is exhausted", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: {
+ session: { used: 0, total: 100, remaining: 100, resetAt: "2026-01-01T17:01:00.000Z" },
+ monthly: { used: 100, total: 100, remaining: 0, resetAt: "2026-02-01T00:00:00.000Z" },
+ },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ });
+
+ it("does not ping Codex when session quota is exhausted", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token" }] : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 100, total: 100, remaining: 0, resetAt: "2026-01-01T17:01:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ expect(deps.updateProviderConnection).not.toHaveBeenCalled();
+ });
+
+ it("sends one tiny gpt-5.5 Codex request through the executor", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex"
+ ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token", providerSpecificData: { workspaceId: "ws-1" } }]
+ : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T17:00:00.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 1, total: 100, remaining: 99, resetAt: "2026-01-01T17:01:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ const executor = deps.getExecutor.mock.results[0].value;
+ expect(deps.getExecutor).toHaveBeenCalledWith("codex");
+ expect(executor.execute).toHaveBeenCalledWith(expect.objectContaining({
+ model: "gpt-5.5",
+ stream: true,
+ credentials: expect.objectContaining({
+ accessToken: "token",
+ connectionId: "codex-1",
+ providerSpecificData: { workspaceId: "ws-1" },
+ }),
+ body: {
+ model: "gpt-5.5",
+ input: [{
+ type: "message",
+ role: "user",
+ content: [{ type: "input_text", text: "hi" }],
+ }],
+ instructions: "Reply with OK.",
+ reasoning: { effort: "none", summary: "auto" },
+ store: false,
+ stream: true,
+ },
+ }));
+ expect(codexResponseText).toHaveBeenCalledTimes(1);
+ expect(deps.updateProviderConnection).toHaveBeenCalledWith("codex-1", expect.objectContaining({
+ lastPingedResetAt: "2026-01-01T17:01:00.000Z",
+ lastPingedResetKey: "2026-01-01T17:01:00.000Z",
+ }));
+ });
+
+ it("does not ping same Codex reset twice when seconds drift", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex"
+ ? [{ id: "codex-1", provider: "codex", authType: "oauth", accessToken: "token", lastPingedResetAt: "2026-01-01T11:59:44.000Z" }]
+ : []
+ ));
+ state.resetCache["codex:codex-1"] = "2026-01-01T11:59:44.000Z";
+ getCodexUsage.mockResolvedValue({
+ quotas: { session: { used: 0, total: 100, remaining: 100, resetAt: "2026-01-01T11:59:47.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ });
+
+ it("skips non-OAuth Codex connections", async () => {
+ deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "codex" ? [{ id: "codex-1", provider: "codex", authType: "apikey", accessToken: "token" }] : []
+ ));
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(getCodexUsage).not.toHaveBeenCalled();
+ expect(deps.getExecutor).not.toHaveBeenCalled();
+ });
+
+ it("keeps Claude session quota key behavior", async () => {
+ deps.getSettings.mockResolvedValue({ claudeAutoPing: { connections: { "claude-1": true } } });
+ deps.getProviderConnections.mockImplementation(async ({ provider }) => (
+ provider === "claude" ? [{ id: "claude-1", provider: "claude", authType: "oauth", accessToken: "token" }] : []
+ ));
+ getClaudeUsage.mockResolvedValue({
+ quotas: { "session (5h)": { resetAt: "2026-01-01T11:59:00.000Z" } },
+ });
+
+ await runQuotaAutoPingTick(deps, state);
+
+ expect(deps.proxyAwareFetch).toHaveBeenCalledTimes(1);
+ expect(JSON.parse(deps.proxyAwareFetch.mock.calls[0][1].body)).toMatchObject({
+ model: "claude-haiku-4-5-20251001",
+ max_tokens: 1,
+ messages: [{ role: "user", content: "hi" }],
+ });
+ });
+});