From 1dca7c71429a4eb9b11dc01d7dd4ccf3013193c8 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:27:41 -0700 Subject: [PATCH 1/2] fix(codex): restore five-hour usage window --- .../rate-limits/codex-fetcher-backend.test.ts | 40 ++++++++ src/main/rate-limits/codex-fetcher.test.ts | 47 ++++++++++ src/main/rate-limits/codex-fetcher.ts | 92 ++++++++++++++----- ...x-rate-limit-window-classification.test.ts | 4 +- .../codex-rate-limit-window-classification.ts | 34 +++---- .../src/components/status-bar/StatusBar.tsx | 6 +- .../status-bar/UsageRosterPanel.test.tsx | 4 + .../status-bar/inline-usage-bars.test.tsx | 25 +++++ 8 files changed, 207 insertions(+), 45 deletions(-) diff --git a/src/main/rate-limits/codex-fetcher-backend.test.ts b/src/main/rate-limits/codex-fetcher-backend.test.ts index 9102e54ab093..3fefe5e3a2fd 100644 --- a/src/main/rate-limits/codex-fetcher-backend.test.ts +++ b/src/main/rate-limits/codex-fetcher-backend.test.ts @@ -97,6 +97,46 @@ describe('Codex backend rate-limit requests', () => { ) }) + it('classifies a sole seven-day backend primary window as weekly', async () => { + readFileMock.mockResolvedValue( + JSON.stringify({ + tokens: { access_token: 'access-token', account_id: 'account-id' } + }) + ) + vi.mocked(fetch) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + plan_type: 'plus', + rate_limit: { + primary_window: { + used_percent: 37, + limit_window_seconds: 7 * 24 * 60 * 60, + reset_at: 1_800_000_000 + }, + secondary_window: null + } + }) + } as Response) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ available_count: 0, credits: [] }) + } as Response) + + await expect( + fetchCodexRateLimits({ + codexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\account\\home' + }) + ).resolves.toMatchObject({ + session: null, + weekly: { usedPercent: 37, windowMinutes: 10_080, resetsAt: 1_800_000_000_000 }, + status: 'ok' + }) + + expect(childSpawnMock).not.toHaveBeenCalled() + expect(ptySpawnMock).not.toHaveBeenCalled() + }) + it('aborts callers while sharing one stalled backend auth read', async () => { let resolveRead!: (content: string) => void readFileMock.mockImplementation( diff --git a/src/main/rate-limits/codex-fetcher.test.ts b/src/main/rate-limits/codex-fetcher.test.ts index a00d86ffc9d3..11057a1fc813 100644 --- a/src/main/rate-limits/codex-fetcher.test.ts +++ b/src/main/rate-limits/codex-fetcher.test.ts @@ -385,6 +385,53 @@ describe('fetchCodexRateLimits', () => { }) }) + it('fills a restored backend 5-hour window when RPC only reports weekly usage', async () => { + const rpcChild = makeRpcChild() + childSpawnMock.mockReturnValue(rpcChild) + respondToRpcRateLimitRead(rpcChild, { + primary: { usedPercent: 22, windowDurationMins: 10080 }, + secondary: null + }) + readFileMock.mockResolvedValue( + JSON.stringify({ + tokens: { access_token: 'access-token', account_id: 'account-id' } + }) + ) + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + plan_type: 'plus', + rate_limit: { + primary_window: { + used_percent: 7, + limit_window_seconds: 5 * 60 * 60, + reset_at: 1_800_000_000 + }, + secondary_window: { + used_percent: 23, + limit_window_seconds: 7 * 24 * 60 * 60, + reset_at: 1_800_100_000 + } + }, + rate_limit_reset_credits: { + available_count: 1, + credits: [{ status: 'available', expires_at: '2027-01-15T12:00:00Z' }] + } + }) + } as Response) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(1) + await vi.advanceTimersByTimeAsync(1) + + await expect(resultPromise).resolves.toMatchObject({ + session: { usedPercent: 7, windowMinutes: 300, resetsAt: 1_800_000_000_000 }, + weekly: { usedPercent: 23, windowMinutes: 10080, resetsAt: 1_800_100_000_000 }, + status: 'ok' + }) + expect(fetch).toHaveBeenCalledTimes(1) + }) + it('does not map a duplicate session-duration window into the weekly slot', async () => { const rpcChild = makeRpcChild() childSpawnMock.mockReturnValue(rpcChild) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 28a97a37da62..f4664b2a1660 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -15,8 +15,8 @@ import { classifyCodexRateLimitWindows, CODEX_SESSION_WINDOW_MINUTES, CODEX_WEEKLY_WINDOW_MINUTES, - type CodexRpcRateLimits, - type CodexRpcRateWindow + type CodexRateLimitWindowsSnapshot, + type CodexRateWindowSnapshot } from './codex-rate-limit-window-classification' import { resolveCodexCommand } from '../codex-cli/command' import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic' @@ -83,7 +83,7 @@ type RateLimitResetCredits = { // Why: the Codex app-server wraps rate limit data as { rateLimits: { primary, secondary, ... } }. type RpcRateLimitsResponse = { - rateLimits?: CodexRpcRateLimits | null + rateLimits?: CodexRateLimitWindowsSnapshot | null rateLimitResetCredits?: { availableCount?: number totalEarnedCount?: number @@ -454,7 +454,7 @@ export async function consumeCodexRateLimitResetCredit(options: { } function mapRpcWindow( - raw: CodexRpcRateWindow | null | undefined, + raw: CodexRateWindowSnapshot | null | undefined, expectedWindowMinutes: number ): RateLimitWindow | null { if (!raw || typeof raw.usedPercent !== 'number' || !Number.isFinite(raw.usedPercent)) { @@ -489,27 +489,30 @@ function mapRpcWindow( } } -function mapBackendUsageWindow( - raw: BackendRateLimitWindow | null | undefined, - fallbackWindowMinutes: number -): RateLimitWindow | null { - const limitWindowSeconds = raw?.limit_window_seconds - // Why: match Codex backend-client's window_minutes_from_seconds — actual bucket duration, rounding partial minutes up. - const windowMinutes = +function backendWindowToSnapshot( + raw: BackendRateLimitWindow | null | undefined +): CodexRateWindowSnapshot | null { + if (!raw) { + return null + } + const limitWindowSeconds = raw.limit_window_seconds + const windowDurationMins = typeof limitWindowSeconds === 'number' && Number.isFinite(limitWindowSeconds) && limitWindowSeconds > 0 ? Math.ceil(limitWindowSeconds / 60) - : fallbackWindowMinutes - return mapRpcWindow( - raw - ? { - usedPercent: raw.used_percent, - resetsAt: raw.reset_at - } - : undefined, - windowMinutes - ) + : undefined + return { usedPercent: raw.used_percent, windowDurationMins, resetsAt: raw.reset_at } +} + +function snapshotWindowMinutes( + snapshot: CodexRateWindowSnapshot | null, + fallbackWindowMinutes: number +): number { + const duration = snapshot?.windowDurationMins + return typeof duration === 'number' && Number.isFinite(duration) && duration > 0 + ? duration + : fallbackWindowMinutes } async function fetchViaBackend( @@ -534,10 +537,20 @@ async function fetchViaBackend( if (typeof payload.plan_type !== 'string') { return null } + const classified = classifyCodexRateLimitWindows({ + primary: backendWindowToSnapshot(payload.rate_limit?.primary_window), + secondary: backendWindowToSnapshot(payload.rate_limit?.secondary_window) + }) return { provider: 'codex', - session: mapBackendUsageWindow(payload.rate_limit?.primary_window, 300), - weekly: mapBackendUsageWindow(payload.rate_limit?.secondary_window, 10080), + session: mapRpcWindow( + classified.session, + snapshotWindowMinutes(classified.session, CODEX_SESSION_WINDOW_MINUTES) + ), + weekly: mapRpcWindow( + classified.weekly, + snapshotWindowMinutes(classified.weekly, CODEX_WEEKLY_WINDOW_MINUTES) + ), // Surfaced for the status-bar Usage row (e.g. "Codex · Plus"). planType: payload.plan_type, ...(payload.rate_limit_reset_credits !== undefined @@ -552,6 +565,33 @@ async function fetchViaBackend( } } +async function withBackendSessionWindow( + limits: ProviderRateLimits, + options?: FetchCodexRateLimitsOptions +): Promise { + if (options?.signal?.aborted || limits.session || !limits.weekly) { + return limits + } + try { + const backend = await fetchViaBackend(options) + if (!backend?.session) { + return limits + } + return { + ...limits, + session: backend.session, + weekly: backend.weekly ?? limits.weekly, + planType: backend.planType ?? limits.planType, + ...(backend.rateLimitResetCredits !== undefined + ? { rateLimitResetCredits: backend.rateLimitResetCredits } + : {}), + updatedAt: backend.updatedAt + } + } catch { + return limits + } +} + // --------------------------------------------------------------------------- // RPC fetch — spawn `codex -s read-only -a untrusted app-server` // --------------------------------------------------------------------------- @@ -1133,7 +1173,8 @@ export async function fetchCodexRateLimits( return abortedCodexRateLimitResult() } if (rpcResult.status === 'ok' || rpcResult.status === 'unavailable') { - const withResetCredits = await withBackendRateLimitResetCredits(rpcResult, options) + const withSession = await withBackendSessionWindow(rpcResult, options) + const withResetCredits = await withBackendRateLimitResetCredits(withSession, options) return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits } if (isCodexAuthError(rpcResult.error)) { @@ -1169,7 +1210,8 @@ export async function fetchCodexRateLimits( if (options?.signal?.aborted) { return abortedCodexRateLimitResult() } - const withResetCredits = await withBackendRateLimitResetCredits(ptyResult, options) + const withSession = await withBackendSessionWindow(ptyResult, options) + const withResetCredits = await withBackendRateLimitResetCredits(withSession, options) return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits } catch (err) { if (options?.signal?.aborted) { diff --git a/src/main/rate-limits/codex-rate-limit-window-classification.test.ts b/src/main/rate-limits/codex-rate-limit-window-classification.test.ts index 6dff322bc380..826f1844ebee 100644 --- a/src/main/rate-limits/codex-rate-limit-window-classification.test.ts +++ b/src/main/rate-limits/codex-rate-limit-window-classification.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest' import { classifyCodexRateLimitWindows, - type CodexRpcRateLimits + type CodexRateLimitWindowsSnapshot } from './codex-rate-limit-window-classification' -function usedPercentByWindow(result: CodexRpcRateLimits | null): { +function usedPercentByWindow(result: CodexRateLimitWindowsSnapshot | null): { session: number | null weekly: number | null } { diff --git a/src/main/rate-limits/codex-rate-limit-window-classification.ts b/src/main/rate-limits/codex-rate-limit-window-classification.ts index 9c260cc52ae8..cdf667b6306a 100644 --- a/src/main/rate-limits/codex-rate-limit-window-classification.ts +++ b/src/main/rate-limits/codex-rate-limit-window-classification.ts @@ -4,27 +4,27 @@ export const CODEX_WEEKLY_WINDOW_MINUTES = 10080 // Why: tolerate the one-minute drift seen in older Codex bucket lengths without absorbing other durations. const CODEX_WINDOW_DURATION_TOLERANCE_MINUTES = 1 -export type CodexRpcRateWindow = { +export type CodexRateWindowSnapshot = { usedPercent?: unknown windowDurationMins?: unknown resetsAt?: unknown } -export type CodexRpcRateLimits = { - primary?: CodexRpcRateWindow | null - secondary?: CodexRpcRateWindow | null +export type CodexRateLimitWindowsSnapshot = { + primary?: CodexRateWindowSnapshot | null + secondary?: CodexRateWindowSnapshot | null } -type MappableCodexRpcRateWindow = CodexRpcRateWindow & { usedPercent: number } +type MappableCodexRateWindowSnapshot = CodexRateWindowSnapshot & { usedPercent: number } type CodexRateLimitWindowKind = 'session' | 'weekly' | null -function isMappableCodexRpcRateWindow( - raw: CodexRpcRateWindow | null | undefined -): raw is MappableCodexRpcRateWindow { +function isMappableCodexRateWindowSnapshot( + raw: CodexRateWindowSnapshot | null | undefined +): raw is MappableCodexRateWindowSnapshot { return typeof raw?.usedPercent === 'number' && Number.isFinite(raw.usedPercent) } -function classifyWindowDuration(raw: MappableCodexRpcRateWindow): CodexRateLimitWindowKind { +function classifyWindowDuration(raw: MappableCodexRateWindowSnapshot): CodexRateLimitWindowKind { const duration = raw.windowDurationMins if (typeof duration !== 'number' || !Number.isFinite(duration)) { return null @@ -40,14 +40,16 @@ function classifyWindowDuration(raw: MappableCodexRpcRateWindow): CodexRateLimit return null } -export function classifyCodexRateLimitWindows(result: CodexRpcRateLimits | null | undefined): { - session: MappableCodexRpcRateWindow | null - weekly: MappableCodexRpcRateWindow | null +export function classifyCodexRateLimitWindows( + result: CodexRateLimitWindowsSnapshot | null | undefined +): { + session: MappableCodexRateWindowSnapshot | null + weekly: MappableCodexRateWindowSnapshot | null } { - const primary = isMappableCodexRpcRateWindow(result?.primary) ? result.primary : null - const secondary = isMappableCodexRpcRateWindow(result?.secondary) ? result.secondary : null - let session: MappableCodexRpcRateWindow | null = null - let weekly: MappableCodexRpcRateWindow | null = null + const primary = isMappableCodexRateWindowSnapshot(result?.primary) ? result.primary : null + const secondary = isMappableCodexRateWindowSnapshot(result?.secondary) ? result.secondary : null + let session: MappableCodexRateWindowSnapshot | null = null + let weekly: MappableCodexRateWindowSnapshot | null = null for (const window of [primary, secondary]) { if (!window) { diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 2c321685ff66..02978a47912d 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -1232,10 +1232,12 @@ function VerboseProviderUsage({ return window !== null }) + const meterWindow = p.session ?? p.weekly + return ( <> - {p.session && !compact ? ( - + {meterWindow && !compact ? ( + ) : null} {visibleWindows.map((window, index) => ( diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx index 54d1fb6cf9d3..b1addca4dab5 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx @@ -129,6 +129,10 @@ describe('UsageRow', () => { expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce() expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset]) expect(markup).toContain('Resets in 2m') + expect(markup).toContain('5h') + expect(markup).toContain('25%') + expect(markup).toContain('wk') + expect(markup).toContain('10%') }) }) diff --git a/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx b/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx index a956695c805c..a6b78d6f8492 100644 --- a/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx +++ b/src/renderer/src/components/status-bar/inline-usage-bars.test.tsx @@ -99,6 +99,31 @@ describe('InlineUsageBars', () => { expect(markup).toContain('32% used now') }) + it('keeps the footer meter for weekly-only Codex usage', async () => { + const { ProviderSegment } = await import('./StatusBar') + const limits: ProviderRateLimits = { + provider: 'codex', + session: null, + weekly: { + usedPercent: 37, + windowMinutes: 10_080, + resetsAt: null, + resetDescription: null + }, + updatedAt: Date.now(), + error: null, + status: 'ok' + } + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('w-[48px] h-[6px]') + expect(markup).toContain('width:37%') + expect(markup).toContain('37% used wk') + }) + it('shows remaining copy and remaining meter fill', async () => { mocks.usagePercentageDisplay = 'remaining' const { InlineUsageBars } = await import('./StatusBar') From 0c4f51f9875b590f5f7a30b7d5d13bf8c3990e74 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:46:33 -0700 Subject: [PATCH 2/2] fix(codex): reuse backend reset credit metadata --- .../codex-fetcher-session-supplement.test.ts | 174 ++++++++++++++++++ src/main/rate-limits/codex-fetcher.ts | 21 ++- 2 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 src/main/rate-limits/codex-fetcher-session-supplement.test.ts diff --git a/src/main/rate-limits/codex-fetcher-session-supplement.test.ts b/src/main/rate-limits/codex-fetcher-session-supplement.test.ts new file mode 100644 index 000000000000..9bef59e96bd5 --- /dev/null +++ b/src/main/rate-limits/codex-fetcher-session-supplement.test.ts @@ -0,0 +1,174 @@ +import { EventEmitter } from 'node:events' +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' + +const { childSpawnMock, readFileMock } = vi.hoisted(() => ({ + childSpawnMock: vi.fn(), + readFileMock: vi.fn() +})) + +vi.mock('node:child_process', () => ({ spawn: childSpawnMock })) +vi.mock('node:fs/promises', () => ({ readFile: readFileMock })) +vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: () => 'codex' })) +vi.mock('node-pty', () => ({ spawn: vi.fn() })) +vi.mock('./codex-auth-presence', () => ({ + probeCodexAuthPresence: vi.fn(async () => 'present') +})) + +import { fetchCodexRateLimits } from './codex-fetcher' + +function makeRpcChild(rateLimitResetCredits?: unknown) { + const child = new EventEmitter() as EventEmitter & { + stdout: EventEmitter + stderr: EventEmitter + stdin: { write: ReturnType } + kill: ReturnType + } + child.stdout = new EventEmitter() + child.stderr = new EventEmitter() + child.kill = vi.fn() + child.stdin = { + write: vi.fn((line: string) => { + const message = JSON.parse(line) as { id?: number; method?: string } + if (message.method === 'initialize') { + setTimeout(() => { + child.stdout.emit( + 'data', + Buffer.from(`${JSON.stringify({ jsonrpc: '2.0', id: message.id, result: {} })}\n`) + ) + }, 0) + } + if (message.method === 'account/rateLimits/read') { + setTimeout(() => { + child.stdout.emit( + 'data', + Buffer.from( + `${JSON.stringify({ + jsonrpc: '2.0', + id: message.id, + result: { + rateLimits: { + primary: { usedPercent: 22, windowDurationMins: 10_080 } + }, + ...(rateLimitResetCredits !== undefined ? { rateLimitResetCredits } : {}) + } + })}\n` + ) + ) + }, 0) + } + }) + } + return child +} + +function usageResponse(rateLimitResetCredits?: unknown): Response { + return { + ok: true, + json: async () => ({ + plan_type: 'pro', + rate_limit: { + primary_window: { + used_percent: 23, + limit_window_seconds: 7 * 24 * 60 * 60 + } + }, + ...(rateLimitResetCredits !== undefined + ? { rate_limit_reset_credits: rateLimitResetCredits } + : {}) + }) + } as Response +} + +function dedicatedCreditsResponse(): Response { + return { + ok: true, + json: async () => ({ + available_count: 2, + credits: [ + { + status: 'available', + expires_at: '2027-01-15T12:00:00Z', + granted_at: '2027-01-08T12:00:00Z' + } + ] + }) + } as Response +} + +async function fetchWeeklyOnly(rateLimitResetCredits?: unknown) { + childSpawnMock.mockReturnValue(makeRpcChild(rateLimitResetCredits)) + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(1) + await vi.advanceTimersByTimeAsync(1) + return resultPromise +} + +describe('Codex backend session supplement credits', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.clearAllMocks() + readFileMock.mockResolvedValue( + JSON.stringify({ + tokens: { access_token: 'access-token', account_id: 'account-id' } + }) + ) + vi.stubGlobal('fetch', vi.fn()) + }) + + afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + }) + + it('reuses complete zero-credit metadata from a weekly-only usage response', async () => { + vi.mocked(fetch).mockResolvedValue(usageResponse({ available_count: 0 })) + + await expect(fetchWeeklyOnly()).resolves.toMatchObject({ + session: null, + weekly: { usedPercent: 22, windowMinutes: 10_080 }, + rateLimitResetCredits: { availableCount: 0, nextExpiresAt: null } + }) + expect(fetch).toHaveBeenCalledTimes(1) + }) + + it.each([ + { name: 'null', credits: null }, + { name: 'invalid', credits: {} } + ])('preserves complete RPC credits when usage metadata is $name', async ({ credits }) => { + vi.mocked(fetch).mockResolvedValue(usageResponse(credits)) + + await expect( + fetchWeeklyOnly({ + availableCount: 1, + nextExpiresAt: 1_800_000_000 + }) + ).resolves.toMatchObject({ + rateLimitResetCredits: { + availableCount: 1, + nextExpiresAt: 1_800_000_000_000 + } + }) + expect(fetch).toHaveBeenCalledTimes(1) + }) + + it.each([ + { name: 'absent', credits: undefined }, + { name: 'incomplete', credits: { available_count: 2 } } + ])('uses the dedicated credits endpoint when usage metadata is $name', async ({ credits }) => { + vi.mocked(fetch) + .mockResolvedValueOnce(usageResponse(credits)) + .mockResolvedValueOnce(dedicatedCreditsResponse()) + + await expect(fetchWeeklyOnly()).resolves.toMatchObject({ + rateLimitResetCredits: { + availableCount: 2, + nextExpiresAt: Date.parse('2027-01-15T12:00:00Z') + } + }) + expect(fetch).toHaveBeenCalledTimes(2) + expect(vi.mocked(fetch).mock.calls.map(([url]) => url)).toEqual([ + 'https://chatgpt.com/backend-api/wham/usage', + 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits' + ]) + }) +}) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index f4664b2a1660..d28fdee015e5 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -292,6 +292,12 @@ function mapBackendRateLimitResetCredits( } } +function hasCompleteRateLimitResetCredits( + credits: RateLimitResetCredits | null | undefined +): boolean { + return Boolean(credits && (credits.availableCount === 0 || credits.nextExpiresAt != null)) +} + function createBackendRequestSignal( callerSignal?: AbortSignal, timeoutMs = BACKEND_TIMEOUT_MS @@ -392,8 +398,7 @@ async function withBackendRateLimitResetCredits( if ( options?.signal?.aborted || limits.provider !== 'codex' || - (limits.rateLimitResetCredits?.nextExpiresAt !== undefined && - limits.rateLimitResetCredits.nextExpiresAt !== null) + hasCompleteRateLimitResetCredits(limits.rateLimitResetCredits) ) { return limits } @@ -574,17 +579,21 @@ async function withBackendSessionWindow( } try { const backend = await fetchViaBackend(options) - if (!backend?.session) { + if (!backend) { return limits } + const rateLimitResetCredits = backend.rateLimitResetCredits ?? limits.rateLimitResetCredits + if (!backend.session) { + return rateLimitResetCredits === limits.rateLimitResetCredits + ? limits + : { ...limits, rateLimitResetCredits } + } return { ...limits, session: backend.session, weekly: backend.weekly ?? limits.weekly, planType: backend.planType ?? limits.planType, - ...(backend.rateLimitResetCredits !== undefined - ? { rateLimitResetCredits: backend.rateLimitResetCredits } - : {}), + ...(rateLimitResetCredits !== undefined ? { rateLimitResetCredits } : {}), updatedAt: backend.updatedAt } } catch {