mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(codex): restore five-hour usage window
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ProviderRateLimits> {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
} {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1232,10 +1232,12 @@ function VerboseProviderUsage({
|
||||
return window !== null
|
||||
})
|
||||
|
||||
const meterWindow = p.session ?? p.weekly
|
||||
|
||||
return (
|
||||
<>
|
||||
{p.session && !compact ? (
|
||||
<MiniBar usedPct={clampUsedPercent(p.session.usedPercent)} display={display} />
|
||||
{meterWindow && !compact ? (
|
||||
<MiniBar usedPct={clampUsedPercent(meterWindow.usedPercent)} display={display} />
|
||||
) : null}
|
||||
{visibleWindows.map((window, index) => (
|
||||
<React.Fragment key={window.key}>
|
||||
|
||||
@@ -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%')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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(
|
||||
<ProviderSegment p={limits} compact={false} display="used" />
|
||||
)
|
||||
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user