From e3266f83e8db7f1805dcc9495978aa641f015fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AB=A0=E6=B0=B8=E7=90=AA?= <2017600918@qq.com> Date: Thu, 7 May 2026 00:04:21 +0800 Subject: [PATCH 1/2] fix: fetch quota on manual refresh button click Previously, clicking the refresh button on an account card only refreshed the OAuth token without querying upstream quota data. Now, after a successful token refresh, we also call the Codex usage API to fetch and cache the current quota (7-day and 5-hour windows), so users can see usage data even without making proxy requests. Changes: - POST /auth/accounts/:id/refresh now fetches quota after token refresh - GET /auth/accounts?quota=fresh actively fetches quota for all accounts - Added tls.disable_websocket config option for proxies that don't support WebSocket - Proxy auto-detection now respects empty proxy_url config Co-Authored-By: Claude Opus 4.7 --- src/config-schema.ts | 1 + src/routes/accounts.ts | 50 +++++++++++++++++++++++++++++- src/routes/messages.ts | 2 +- src/routes/responses.ts | 4 ++- src/routes/shared/proxy-handler.ts | 4 ++- src/tls/proxy.ts | 10 ++++-- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/config-schema.ts b/src/config-schema.ts index 56415251..5a469e4e 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -57,6 +57,7 @@ export const ConfigSchema = z.object({ tls: z.object({ proxy_url: z.string().nullable().default(null), force_http11: z.boolean().default(false), + disable_websocket: z.boolean().default(false), }).default({}), quota: z.object({ refresh_interval_minutes: z.number().min(0).default(5), diff --git a/src/routes/accounts.ts b/src/routes/accounts.ts index d9fe9560..e2d297f0 100644 --- a/src/routes/accounts.ts +++ b/src/routes/accounts.ts @@ -142,7 +142,31 @@ export function createAccountRoutes(pool: AccountPool, scheduler: RefreshSchedul c.status(404); return c.json({ error: "Account not found" }); } - return c.json(result); + + // If token refresh succeeded, also fetch quota from upstream + let quota = null; + if (result.result === "alive") { + const entry = pool.getEntry(id); + if (entry?.token) { + try { + const usage = await new CodexApi( + entry.token, + entry.accountId, + cookieJar, + id, + proxyPool?.resolveProxyUrl(id), + ).getUsage(); + quota = toQuota(usage); + // Cache the quota data in the pool + pool.updateCachedQuota(id, quota); + } catch (err) { + // Quota fetch failure shouldn't block the refresh response + console.warn(`[AccountRefresh] Failed to fetch quota for ${id}:`, err instanceof Error ? err.message : err); + } + } + } + + return c.json({ ...result, quota }); }); app.patch("/auth/accounts/:id/label", async (c) => { @@ -155,6 +179,30 @@ export function createAccountRoutes(pool: AccountPool, scheduler: RefreshSchedul }); app.get("/auth/accounts", async (c) => { + const quotaParam = c.req.query("quota"); + + // If quota=fresh, actively fetch quota from upstream for all active accounts + if (quotaParam === "fresh") { + const entries = pool.getAllEntries().filter((e) => e.status === "active" && e.token); + await Promise.allSettled( + entries.map(async (entry) => { + try { + const usage = await new CodexApi( + entry.token, + entry.accountId, + cookieJar, + entry.id, + proxyPool?.resolveProxyUrl(entry.id), + ).getUsage(); + pool.updateCachedQuota(entry.id, toQuota(usage)); + } catch (err) { + // Silently ignore per-account failures + console.warn(`[AccountList] Failed to refresh quota for ${entry.id}:`, err instanceof Error ? err.message : err); + } + }), + ); + } + const accounts = querySvc.listFresh(); return c.json({ accounts }); }); diff --git a/src/routes/messages.ts b/src/routes/messages.ts index d0c6e00b..a797d7c4 100644 --- a/src/routes/messages.ts +++ b/src/routes/messages.ts @@ -134,7 +134,7 @@ export function createMessagesRoutes( injectHostedWebSearch: !allowUnauthenticated, mapClaudeCodeWebSearch: !allowUnauthenticated && clientConversationId !== null, }); - if (!allowUnauthenticated) { + if (!allowUnauthenticated && !config.tls.disable_websocket) { codexRequest.useWebSocket = true; } const wantThinking = req.thinking?.type === "enabled" || req.thinking?.type === "adaptive"; diff --git a/src/routes/responses.ts b/src/routes/responses.ts index d3be72ea..92bdb9a5 100644 --- a/src/routes/responses.ts +++ b/src/routes/responses.ts @@ -601,7 +601,9 @@ export function createResponsesRoutes( store: false, }; - codexRequest.useWebSocket = true; + if (!config.tls.disable_websocket) { + codexRequest.useWebSocket = true; + } if (typeof body.previous_response_id === "string") { codexRequest.previous_response_id = body.previous_response_id; } diff --git a/src/routes/shared/proxy-handler.ts b/src/routes/shared/proxy-handler.ts index 15a13474..f5bd2bd4 100644 --- a/src/routes/shared/proxy-handler.ts +++ b/src/routes/shared/proxy-handler.ts @@ -323,7 +323,9 @@ export async function handleProxyRequest( }); if (resumeEval.active) { req.codexRequest.previous_response_id = implicitPrevRespId!; - req.codexRequest.useWebSocket = true; + if (!config.tls.disable_websocket) { + req.codexRequest.useWebSocket = true; + } req.codexRequest.input = req.codexRequest.input.slice(continuationInputStart); const implicitTurnState = affinityMap.lookupTurnState(implicitPrevRespId!); if (implicitTurnState) req.codexRequest.turnState = implicitTurnState; diff --git a/src/tls/proxy.ts b/src/tls/proxy.ts index 9139c3a9..59116f82 100644 --- a/src/tls/proxy.ts +++ b/src/tls/proxy.ts @@ -75,9 +75,13 @@ async function detectLocalProxy(): Promise { */ export async function initProxy(): Promise { const config = getConfig(); - if (config.tls.proxy_url) { - _proxyUrl = config.tls.proxy_url; - console.log(`[Proxy] Using configured proxy: ${_proxyUrl}`); + if (config.tls.proxy_url !== null && config.tls.proxy_url !== undefined) { + _proxyUrl = config.tls.proxy_url || null; + if (_proxyUrl) { + console.log(`[Proxy] Using configured proxy: ${_proxyUrl}`); + } else { + console.log("[Proxy] Proxy disabled by config (empty proxy_url)"); + } return; } _proxyUrl = await detectLocalProxy(); From 54c743acd26059531433f54fcfa66300ccdebd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AB=A0=E6=B0=B8=E7=90=AA?= <2017600918@qq.com> Date: Thu, 7 May 2026 00:44:17 +0800 Subject: [PATCH 2/2] fix: resolve websocket config reference Use getConfig() when checking disable_websocket during implicit resume so the branch builds cleanly. Co-Authored-By: Claude Opus 4.7 --- src/routes/shared/proxy-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/shared/proxy-handler.ts b/src/routes/shared/proxy-handler.ts index f5bd2bd4..d82dfe5f 100644 --- a/src/routes/shared/proxy-handler.ts +++ b/src/routes/shared/proxy-handler.ts @@ -323,7 +323,7 @@ export async function handleProxyRequest( }); if (resumeEval.active) { req.codexRequest.previous_response_id = implicitPrevRespId!; - if (!config.tls.disable_websocket) { + if (!getConfig().tls.disable_websocket) { req.codexRequest.useWebSocket = true; } req.codexRequest.input = req.codexRequest.input.slice(continuationInputStart);