Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions packages/opencode/src/core/background-quota-refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
type RefreshAllQuotaDeps,
type RefreshAllQuotaResult,
refreshAllQuota,
} from './refresh-all-quota'

export const BACKGROUND_QUOTA_REFRESH_INTERVAL_MS = 5 * 60_000
export const BACKGROUND_QUOTA_REFRESH_JITTER_MS = 30_000
export const BACKGROUND_QUOTA_FRESHNESS_MS = 4 * 60_000

type TimerHandle = ReturnType<typeof setInterval>

interface BackgroundQuotaRefreshOptions {
setIntervalFn?: (callback: () => void, intervalMs: number) => TimerHandle
clearIntervalFn?: (timer: TimerHandle) => void
random?: () => number
onError?: (error: unknown) => void
}

type RefreshAllQuotaFn = (
deps: RefreshAllQuotaDeps,
) => Promise<RefreshAllQuotaResult[]>

export function refreshQuotaInBackground(
deps: RefreshAllQuotaDeps,
refreshFn: RefreshAllQuotaFn = refreshAllQuota,
): Promise<RefreshAllQuotaResult[]> {
return refreshFn({
...deps,
respectBackoff: true,
skipFresherThanMs: BACKGROUND_QUOTA_FRESHNESS_MS,
})
}

export class BackgroundQuotaRefresh {
private readonly setIntervalFn: NonNullable<
BackgroundQuotaRefreshOptions['setIntervalFn']
>
private readonly clearIntervalFn: NonNullable<
BackgroundQuotaRefreshOptions['clearIntervalFn']
>
private readonly random: () => number
private onError: ((error: unknown) => void) | undefined
private run: (() => Promise<void>) | undefined
private timer: TimerHandle | undefined
private tickPromise: Promise<void> | undefined
private generation = 0

constructor(options: BackgroundQuotaRefreshOptions = {}) {
this.setIntervalFn = options.setIntervalFn ?? setInterval
this.clearIntervalFn = options.clearIntervalFn ?? clearInterval
this.random = options.random ?? Math.random
this.onError = options.onError
}

start(
run: () => Promise<void>,
onError: ((error: unknown) => void) | undefined = this.onError,
): number {
const generation = ++this.generation
this.run = run
this.onError = onError
if (this.timer) return generation

const jitter = Math.round(
(this.random() * 2 - 1) * BACKGROUND_QUOTA_REFRESH_JITTER_MS,
)
this.timer = this.setIntervalFn(() => {
const currentRun = this.run
if (!currentRun || this.tickPromise) return
const tickPromise = currentRun()
.catch((error) => {
try {
this.onError?.(error)
} catch {}
})
.finally(() => {
if (this.tickPromise === tickPromise) this.tickPromise = undefined
})
this.tickPromise = tickPromise
}, BACKGROUND_QUOTA_REFRESH_INTERVAL_MS + jitter)
if ('unref' in this.timer) this.timer.unref()
return generation
}

stop(generation?: number): void {
if (generation !== undefined && generation !== this.generation) return
this.run = undefined
if (!this.timer) return
this.clearIntervalFn(this.timer)
this.timer = undefined
}
}
130 changes: 90 additions & 40 deletions packages/opencode/src/core/refresh-all-quota.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getSidebarState, type SidebarState } from '../sidebar-state'
import type {
FallbackAccountManager,
isOAuthAccount,
Expand Down Expand Up @@ -50,6 +51,8 @@ export interface RefreshAllQuotaDeps {
isOAuthAccountFn: typeof isOAuthAccount
whamFn?: typeof whamUsageFn
respectBackoff?: boolean
skipFresherThanMs?: number
readSidebarState?: () => Promise<SidebarState>
}

export interface RefreshAllQuotaResult {
Expand All @@ -65,53 +68,87 @@ export async function refreshAllQuota(
if (!whamFn) throw new Error('whamFn is required for refreshAllQuota')

const results: RefreshAllQuotaResult[] = []
const freshnessMs = deps.skipFresherThanMs
let quotaUpdated = false
let sharedSidebarState: SidebarState | undefined
if (freshnessMs !== undefined) {
try {
sharedSidebarState = await (deps.readSidebarState ?? getSidebarState)()
} catch {}
}
const sharedFallbackQuota = new Map(
sharedSidebarState?.fallbacks.map((account) => [
account.id,
account.quota,
]) ?? [],
)
const isFresh = (...checkedAts: unknown[]) =>
freshnessMs !== undefined &&
checkedAts.some(
(checkedAt) =>
typeof checkedAt === 'number' &&
Number.isFinite(checkedAt) &&
checkedAt <= deps.now() &&
deps.now() - checkedAt < freshnessMs,
Comment thread
iceteaSA marked this conversation as resolved.
)

// --- MAIN ---
try {
let auth = await deps.getAuth()
if (auth.type === 'oauth') {
if (!auth.access || (auth.expires ?? 0) < deps.now()) {
const tokens = await deps.codexRefreshFn({
refreshToken: auth.refresh ?? '',
fetchImpl: deps.fetchImpl,
now: deps.now,
})
await deps.client.auth.set({
path: { id: 'openai' },
body: {
type: 'oauth',
access: tokens.access,
refresh: tokens.refresh,
expires: tokens.expires,
},
})
auth = { ...auth, access: tokens.access, expires: tokens.expires }
}

if (auth.access) {
if (deps.respectBackoff && deps.quotaManager.isBackedOff()) {
results.push({ account: 'main', ok: true })
} else {
const snap = await whamFn({
accessToken: auth.access,
const freshMainQuota = isFresh(
deps.quotaManager.peekMainForPolicy(deps.storageMainAccountId)
?.checkedAt,
sharedSidebarState?.main.quota?.primary?.checkedAt,
sharedSidebarState?.main.quota?.checkedAt,
)
if (freshMainQuota) {
results.push({ account: 'main', ok: true })
} else {
if (!auth.access || (auth.expires ?? 0) < deps.now()) {
const tokens = await deps.codexRefreshFn({
refreshToken: auth.refresh ?? '',
fetchImpl: deps.fetchImpl,
now: deps.now,
accountId: deps.storageMainAccountId,
})
deps.quotaManager.setMain(
auth.access,
{
quota: snap,
refreshAfter: deps.now() + 5 * 60 * 1000,
checkedAt: deps.now(),
await deps.client.auth.set({
path: { id: 'openai' },
body: {
type: 'oauth',
access: tokens.access,
refresh: tokens.refresh,
expires: tokens.expires,
},
undefined,
true,
)
results.push({ account: 'main', ok: true })
})
auth = { ...auth, access: tokens.access, expires: tokens.expires }
}

if (auth.access) {
if (deps.respectBackoff && deps.quotaManager.isBackedOff()) {
results.push({ account: 'main', ok: true })
} else {
const snap = await whamFn({
accessToken: auth.access,
fetchImpl: deps.fetchImpl,
now: deps.now,
accountId: deps.storageMainAccountId,
})
deps.quotaManager.setMain(
auth.access,
{
quota: snap,
refreshAfter: deps.now() + 5 * 60 * 1000,
checkedAt: deps.now(),
},
undefined,
true,
)
quotaUpdated = true
results.push({ account: 'main', ok: true })
}
} else {
results.push({ account: 'main', ok: false, error: 'no access token' })
}
} else {
results.push({ account: 'main', ok: false, error: 'no access token' })
}
} else {
results.push({
Expand All @@ -135,6 +172,17 @@ export async function refreshAllQuota(
if (acct.enabled === false || !deps.isOAuthAccountFn(acct)) continue

try {
if (
isFresh(
deps.quotaManager.peekFallbackForPolicy(acct.id)?.checkedAt,
sharedFallbackQuota.get(acct.id)?.primary?.checkedAt,
sharedFallbackQuota.get(acct.id)?.checkedAt,
)
) {
results.push({ account: acct.id, ok: true })
continue
}

if (
deps.respectBackoff &&
deps.quotaManager.isFallbackBackedOff(
Expand Down Expand Up @@ -178,6 +226,7 @@ export async function refreshAllQuota(
refreshed.access,
true,
)
quotaUpdated = true
results.push({ account: acct.id, ok: true })
} catch (e) {
results.push({
Expand All @@ -189,9 +238,10 @@ export async function refreshAllQuota(
}
}

// Refresh sidebar after all fetches
const freshStorage = await deps.loadAccounts(deps.configPath)
await deps.writeSidebarState(deps.quotaManager, freshStorage)
if (freshnessMs === undefined || quotaUpdated) {
const freshStorage = await deps.loadAccounts(deps.configPath)
await deps.writeSidebarState(deps.quotaManager, freshStorage)
}

return results
}
Loading