Skip to content
Merged
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
32 changes: 32 additions & 0 deletions mutations.toml
Original file line number Diff line number Diff line change
Expand Up @@ -981,3 +981,35 @@ expect_message = "error: expect(received).toEqual(expected)"
broad_command = ["bun", "scripts/run-mutation-bun.ts", "--broad", "packages/core", "tmp/mutations/core.xml"]
broad_report = "tmp/mutations/core.xml"
broad_id = "{name}"

[[control]]
id = "quota-call-pause-default-waits"
guards = "Without an injected pause, QuotaManager waits on a real timer so physical quota GETs start at least one second apart."
file = "packages/core/src/quota-manager.ts"
old = "opts.pauseBetweenQuotaCalls ?? pauseOnRealTimer"
new = "opts.pauseBetweenQuotaCalls ?? (async () => {}) // NON-VACUITY BREAK: the default pause skips the wait"
test_file = "packages/core/src/tests/quota-dispatch.test.ts"
runner = "command"
command = ["bun", "scripts/run-mutation-bun.ts", "packages/core/src/tests/quota-dispatch.test.ts", "{test}"]
test_count_pattern = "CKDEV executed {count} tests"
expect_red = ["physical quota GET starts stay one second apart when the first GET fails"]
expect_message = "error: expect(received).toBeGreaterThanOrEqual(expected)"
broad_command = ["bun", "scripts/run-mutation-bun.ts", "--broad", "packages/core", "tmp/mutations/core.xml"]
broad_report = "tmp/mutations/core.xml"
broad_id = "{name}"

[[control]]
id = "quota-call-gap-one-second"
guards = "Physical quota GETs from one QuotaManager start at least one second apart; a call inside that gap first waits out the rest of it."
file = "packages/core/src/quota-manager.ts"
old = "private static readonly API_CALL_GAP_MS = 1_000"
new = "private static readonly API_CALL_GAP_MS = 0 // NON-VACUITY BREAK: no spacing between quota calls"
test_file = "packages/core/src/tests/quota-dispatch.test.ts"
runner = "command"
command = ["bun", "scripts/run-mutation-bun.ts", "packages/core/src/tests/quota-dispatch.test.ts", "{test}"]
test_count_pattern = "CKDEV executed {count} tests"
expect_red = ["physical quota GET starts stay one second apart when the first GET fails", "the second physical quota GET first requests the rest of the one-second gap"]
expect_message = '/error: expect\(received\)\.(toBeGreaterThanOrEqual|toEqual)\(expected\)/'
broad_command = ["bun", "scripts/run-mutation-bun.ts", "--broad", "packages/core", "tmp/mutations/core.xml"]
broad_report = "tmp/mutations/core.xml"
broad_id = "{name}"
29 changes: 24 additions & 5 deletions packages/core/src/quota-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ import { tokenFingerprint } from './token-fingerprint.ts'
// Capture real setTimeout before tests can mock globalThis.setTimeout
const nativeSetTimeout = globalThis.setTimeout

/**
* The default pause between physical quota calls: a real timer that does not
* keep the process alive. It returns the timer's promise directly, without an
* async wrapper, so awaiting it takes the same turns as an inline timer.
*/
function pauseOnRealTimer(ms: number): Promise<void> {
return new Promise<void>((resolve) => {
const id = nativeSetTimeout(resolve, ms)
if (typeof id === 'object' && 'unref' in id) id.unref()
})
}

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -81,6 +93,14 @@ export type QuotaManagerOptions = {
dispatch: QuotaRequestDispatch,
) => Promise<OAuthQuotaSnapshot>
now?: () => number
/**
* Waits before a physical usage GET until the minimum gap since the previous
* one has passed. It receives the milliseconds still to wait, measured with
* `now`, and is called only when that remainder is positive. Defaults to a
* real timer that does not keep the process alive. Tests pass one that
* records the requested wait and resolves at once.
*/
pauseBetweenQuotaCalls?: (ms: number) => Promise<void>
onMainQuotaFetched?: (
quota: OAuthQuotaSnapshot,
checkedAt: number,
Expand Down Expand Up @@ -180,6 +200,7 @@ export class QuotaManager {
private readonly fetchImpl: typeof fetch
private readonly fetchQuotaSnapshot: QuotaManagerOptions['fetchQuotaSnapshot']
private readonly now: () => number
private readonly pauseBetweenQuotaCalls: (ms: number) => Promise<void>
private readonly onMainQuotaFetched: QuotaManagerOptions['onMainQuotaFetched']
private readonly onApiError: QuotaManagerOptions['onApiError']
private readonly onFallbackQuotaFetched: QuotaManagerOptions['onFallbackQuotaFetched']
Expand All @@ -190,6 +211,8 @@ export class QuotaManager {
this.fetchImpl = opts.fetchImpl ?? fetch
this.fetchQuotaSnapshot = opts.fetchQuotaSnapshot
this.now = opts.now ?? Date.now
this.pauseBetweenQuotaCalls =
opts.pauseBetweenQuotaCalls ?? pauseOnRealTimer
this.onMainQuotaFetched = opts.onMainQuotaFetched
this.onApiError = opts.onApiError
this.onFallbackQuotaFetched = opts.onFallbackQuotaFetched
Expand Down Expand Up @@ -945,11 +968,7 @@ export class QuotaManager {
if (this.lastApiCallAt !== undefined) {
const remaining =
QuotaManager.API_CALL_GAP_MS - (this.now() - this.lastApiCallAt)
if (remaining > 0)
await new Promise<void>((resolve) => {
const id = nativeSetTimeout(resolve, remaining)
if (typeof id === 'object' && 'unref' in id) id.unref()
})
if (remaining > 0) await this.pauseBetweenQuotaCalls(remaining)
}
if (this.isClosed) throw new Error('QuotaManager is closed')
// Authorization can precede the pacing wait. If custody changed or the
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/tests/quota-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,45 @@ test('physical quota GET starts stay one second apart when the first GET fails',
expect(starts[1]! - starts[0]!).toBeGreaterThanOrEqual(1_000)
})

test('the second physical quota GET first requests the rest of the one-second gap', async () => {
let clock = 5_000
const pauses: number[] = []
const starts: number[] = []
const manager = ownManager(
new QuotaManager({
storage: null,
now: () => clock,
// Record the requested wait and let the clock pass it, so no real
// time elapses and the gap is read from the request itself.
pauseBetweenQuotaCalls: async (ms) => {
pauses.push(ms)
clock += ms
},
fetchImpl: Object.assign(
async () => {
starts.push(clock)
return Response.json(quotaResponse)
},
{ preconnect: fetch.preconnect },
),
}),
)
await manager.refreshFallbackWithMetadata(
'paced-first',
'OAT-synthetic-paced-first',
undefined,
)
expect(pauses).toEqual([])
clock += 300
await manager.refreshFallbackWithMetadata(
'paced-second',
'OAT-synthetic-paced-second',
undefined,
)
expect(pauses).toEqual([700])
expect(starts).toEqual([5_000, 6_000])
})

test('a quota lock loser neither sends a GET nor advances physical dispatch spacing', async () => {
const held = await acquireRefreshFileLock({
name: 'opencode-fallback-quota-refresh-held',
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ import {
type QuotaHeaderFeedPublishEntry,
QuotaHeaderFeedRegistry,
QuotaManager,
type QuotaManagerOptions,
type QuotaState,
quotaSnapshotCheckedAt,
quotaSnapshotHasStandardWindows,
Expand Down Expand Up @@ -1142,6 +1143,8 @@ type PluginRuntimeOverrides = Partial<{
claustrumScopedConnect: () => Promise<NativeCustodyClient>
nativeLocal: NativeAccountRuntimeOptions['local']
cacheKeepAggregateRefreshIntervalMs: number
/** Replaces the real-time wait between physical quota usage calls. */
quotaCallPause: QuotaManagerOptions['pauseBetweenQuotaCalls']
}>

function getConfiguredClaustrumConnectionFile(): string | undefined {
Expand Down Expand Up @@ -1809,6 +1812,7 @@ const anthropicAuthPlugin = async (
// rejected refresh token became usable, so it must not clear refresh errors.
const quotaManager = new QuotaManager({
storage: initialStorage,
pauseBetweenQuotaCalls: runtimeOverrides.quotaCallPause,
fetchQuotaSnapshot: (request, dispatch) => {
assertNativeEnvironment()
const routeId = request.kind === 'main' ? 'main' : request.accountId
Expand Down
12 changes: 9 additions & 3 deletions packages/opencode/src/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12035,9 +12035,15 @@ describe('auth.loader', () => {
(storage) =>
storage?.main?.profile?.accountIdentity === storage?.mainAccountId,
)
// Each generation is a refresh of the same account in another process,
// which rotates the pool's main access token.
for (let generation = 1; generation <= 66; generation++) {
// Each iteration simulates another process refreshing the same main
// account, which only rotates its access token. Profile hydration does
// not look at tokens: once the stored profile is fresh for this account
// identity the display skips the account, and a completed check is
// remembered under the route ID and account identity (storage row and
// login epoch are only consulted while a route's identity is unknown).
// Nothing there counts or caps rotations, so three rotations in a row
// cover what more would: none may cause a further profile fetch.
for (let generation = 1; generation <= 3; generation++) {
await refreshPoolMainElsewhere({
access: `sk-ant-oat01-token-${generation}`,
refresh: `refresh-sk-ant-oat01-token-${generation}`,
Expand Down
42 changes: 39 additions & 3 deletions packages/opencode/src/tests/provider-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ async function localFixture(paid: boolean, paidKey = PAID_KEY) {
async function createPlugin(
owner: BodyOwner,
client?: NativeCustodyClient | undefined,
quotaCallPause?: (ms: number) => Promise<void>,
) {
const creation = AnthropicAuthPlugin(
{
Expand All @@ -486,6 +487,7 @@ async function createPlugin(
{
scopedRosterPollIntervalMs: 0,
cacheKeepAggregateRefreshIntervalMs: 0,
...(quotaCallPause ? { quotaCallPause } : {}),
...(client ? { claustrumScopedConnect: async () => client } : {}),
},
)
Expand All @@ -496,6 +498,34 @@ async function createPlugin(
return plugin
}

/**
* Stands in for the plugin's wait between quota usage polls. A route that hits
* a rate limit polls usage again soon after the startup poll, and the plugin
* spaces those polls one second apart. This records each requested wait and
* returns at once, so the test spends no real time on that spacing.
*/
function recordedQuotaPause() {
const requested: number[] = []
return {
requested,
pause: async (ms: number) => {
requested.push(ms)
},
}
}

/**
* Each requested wait is what remains of the one-second gap since the previous
* poll, so it is positive and never longer than that gap. Whether a wait is
* requested at all depends on how long setup took, so none may be requested.
*/
function expectQuotaPollGaps(requested: readonly number[]) {
for (const ms of requested) {
expect(ms).toBeGreaterThan(0)
expect(ms).toBeLessThanOrEqual(1_000)
}
}

/** Sends one model request through the plugin's own request handler. */
async function sendNative(
owner: BodyOwner,
Expand Down Expand Up @@ -940,9 +970,11 @@ test('OC1 usage-limit echo still reaches the paid route, whose error omits the p
? jsonError(400, 'invalid_request_error', key)
: jsonError(429, 'rate_limit_error', key)
}
const plugin = await createPlugin(owner)
const quotaPause = recordedQuotaPause()
const plugin = await createPlugin(owner, undefined, quotaPause.pause)
const seen = await visible(sendNative(owner, plugin, 'oc1-paid-error'))
expect(modelRequests).toEqual([LOCAL_ROTATED, PAID_KEY])
expectQuotaPollGaps(quotaPause.requested)
expect(seen.status, seen.text).toBe(400)
expectScrubbed(seen.text, [PAID_KEY, LOCAL_ROTATED])
expect(seen.text).toContain('invalid_request_error')
Expand Down Expand Up @@ -1409,10 +1441,12 @@ async function streamedLimitRoute(primary: (key: string) => Response) {
? eventStream(healthy('paid route answered'))
: primary(key)
}
const plugin = await createPlugin(owner)
const quotaPause = recordedQuotaPause()
const plugin = await createPlugin(owner, undefined, quotaPause.pause)
const seen = await visible(
sendNative(owner, plugin, `oc1-limit-${Date.now()}`),
)
expectQuotaPollGaps(quotaPause.requested)
return { routes, usagePolls, seen }
}

Expand Down Expand Up @@ -2006,10 +2040,12 @@ test('OC1 API lastUsed storage failure is detached, drained and warns without ch
armed = true
return eventStream(body)
}
const plugin = await createPlugin(owner)
const quotaPause = recordedQuotaPause()
const plugin = await createPlugin(owner, undefined, quotaPause.pause)
const response = await sendNative(owner, plugin, 'oc1-api-lastused-io')
expect(response.status).toBe(200)
expect(await response.text()).toBe(body)
expectQuotaPollGaps(quotaPause.requested)
await entered.wait
let disposed = false
const disposing = Promise.resolve(plugin.dispose?.()).then(() => {
Expand Down
14 changes: 14 additions & 0 deletions packages/pi/src/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
type ProviderErrorResponseScope,
parseSseFrame,
QuotaManager,
type QuotaManagerOptions,
type QuotaState,
quotaBackoffActive,
quotaSnapshotHasStandardWindows,
Expand Down Expand Up @@ -108,6 +109,18 @@ const quotaManagers = new Map<
{ identityKey: string; manager: QuotaManager }
>()
const PI_SERVICE_CACHE_LIMIT = 16
/**
* Tests only: replaces the real-time wait between quota usage polls in quota
* managers created after it is set. Unset, managers keep Core's real timer.
*/
let quotaCallPauseForTest: QuotaManagerOptions['pauseBetweenQuotaCalls']

/** Set or clear the test replacement for the wait between quota usage polls. */
export function __setPiQuotaCallPauseForTest(
pause: QuotaManagerOptions['pauseBetweenQuotaCalls'],
) {
quotaCallPauseForTest = pause
}

function setBoundedService<T>(map: Map<string, T>, key: string, value: T) {
map.delete(key)
Expand Down Expand Up @@ -141,6 +154,7 @@ function getPiRoutingServices(
if (!quotaManager) {
quotaManager = new QuotaManager({
storage,
pauseBetweenQuotaCalls: quotaCallPauseForTest,
fetchQuotaSnapshot: async (request, dispatch) => {
const id = request.kind === 'main' ? 'main' : request.accountId
if (!id) throw new Error('Native quota request has no account identity')
Expand Down
Loading
Loading