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
22 changes: 22 additions & 0 deletions packages/opencode/src/core/sticky-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getPresentQuotaWindows,
type QuotaWindow,
type QuotaWindowKey,
spendControlExhaustedResetAt,
} from '../sidebar-state'

export const QUOTA_STALENESS_MS = 15 * 60_000
Expand Down Expand Up @@ -90,6 +91,19 @@ export function decideStickyBreak(input: {
}
}

// A reached credit budget is confirmed exhaustion on its own axis, so a warm
// pin migrates exactly as it does for an exhausted window. Judged by the same
// shared signal admission uses, so the two never disagree on what "spent"
// means. A stale, malformed, or missing reading falls through to retain.
const spendReset = spendControlExhaustedResetAt(input.quota, input.now)
if (spendReset) {
return {
action: 'migrate',
reason: 'exhausted',
resetsAt: spendReset.resetsAt,
}
}

if (
input.status === undefined ||
input.status === 0 ||
Expand Down Expand Up @@ -179,6 +193,14 @@ function candidateWeight(
)
},
)
// The credit budget is a third pressure axis on its own reset clock (a month,
// not 5h/7d), so its own resetsAt drives the spend rate. It has no configured
// reserve, and a malformed reading is ignored rather than allowed to zero the
// account's weight.
const spendControl = candidate.quota.spendControl
if (spendControl && Number.isFinite(spendControl.remainingPercent)) {
weights.push(sustainableWindowWeight(spendControl, 0, now))
}
const weight = weights.length > 0 ? Math.min(...weights) : 0
return weight > 0 ? { candidate, quotaCheckedAt, weight } : undefined
}
Expand Down
27 changes: 27 additions & 0 deletions packages/opencode/src/sidebar-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,27 @@ export function isUsableRoutingEntry(
)
}

// The credit budget's own exhaustion signal, shared by admission and sticky
// migration so both agree on what "spent" means. `reached` is the provider's
// authoritative boolean — the percentage is only a display approximation — and
// the check fails open on a missing or lapsed reset exactly like a rate-limit
// window, so a stale or corrupt reading never blocks.
export function spendControlExhaustedResetAt(
quota: AccountQuota | null | undefined,
now = Date.now(),
): { resetsAt: string; resetAtMs: number } | undefined {
const spendControl = quota?.spendControl
if (
spendControl?.reached !== true ||
typeof spendControl.resetsAt !== 'string'
) {
return undefined
}
const resetAtMs = Date.parse(spendControl.resetsAt)
if (!Number.isFinite(resetAtMs) || resetAtMs <= now) return undefined
return { resetsAt: spendControl.resetsAt, resetAtMs }
}

// Earliest future reset among the quota's exhausted windows, or undefined when
// no present window is exhausted. Every present window is evaluated — matching
// the admission policy, which rejects an account when ANY live window is below
Expand All @@ -552,6 +573,12 @@ export function exhaustedQuotaResetAt(
earliest = { resetsAt: window.resetsAt, resetAtMs }
}
}
// The credit budget is a third axis on its own reset clock (a month, not
// 5h/7d), judged by the same shared signal sticky migration uses.
const spendReset = spendControlExhaustedResetAt(quota, now)
if (spendReset && (!earliest || spendReset.resetAtMs < earliest.resetAtMs)) {
earliest = spendReset
}
return earliest
}

Expand Down
102 changes: 102 additions & 0 deletions packages/opencode/src/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5398,6 +5398,108 @@ describe('integration: active fallback routing', () => {
}
})

function spentCreditBudget(
resetsAt: string,
): NonNullable<SidebarState['main']['quota']>['spendControl'] {
return {
limit: 2500,
used: 2500,
remaining: 0,
usedPercent: 100,
remainingPercent: 0,
resetsAt,
reached: true,
}
}

it('admission quota skips a fallback whose credit budget is spent', async () => {
const now = Date.now()
const reset = new Date(now + 7 * 24 * 3600_000).toISOString()
const creditReset = new Date(now + 30 * 24 * 3600_000).toISOString()
seedAdmissionAccounts(['work-alt', 'client-alt'])
const seenAuth: string[] = []
const originalFetch = globalThis.fetch
globalThis.fetch = mockAdmissionFetch(seenAuth)

let hooks: Hooks | undefined
try {
const loaded = await loadFetchOverride(
createMockPluginInput(),
now + 3600_000,
)
hooks = loaded.hooks
await drainSidebarWrites()
writeAdmissionSidebarState({
fallbackIds: ['work-alt', 'client-alt'],
fallbackQuotas: {
'work-alt': {
...admissionQuota(20, reset, now),
spendControl: spentCreditBudget(creditReset),
},
'client-alt': admissionQuota(20, reset, now),
},
fallbackAccountIds: {
'work-alt': 'chatgpt-work-alt',
'client-alt': 'chatgpt-client-alt',
},
activeId: 'work-alt',
})

const response = await loaded.fetchOverride(
'https://api.openai.com/v1/responses',
requestInit(),
)

expect(response.status).toBe(200)
expect(seenAuth).toEqual(['Bearer client-alt-token'])
} finally {
globalThis.fetch = originalFetch
await hooks?.dispose?.()
}
})

it('admission quota preserves the last fallback when every credit budget is spent', async () => {
const now = Date.now()
const reset = new Date(now + 7 * 24 * 3600_000).toISOString()
const creditReset = new Date(now + 30 * 24 * 3600_000).toISOString()
seedAdmissionAccounts(['work-alt'])
const seenAuth: string[] = []
const originalFetch = globalThis.fetch
globalThis.fetch = mockAdmissionFetch(seenAuth)

let hooks: Hooks | undefined
try {
const loaded = await loadFetchOverride(
createMockPluginInput(),
now + 3600_000,
)
hooks = loaded.hooks
await drainSidebarWrites()
writeAdmissionSidebarState({
fallbackIds: ['work-alt'],
fallbackQuotas: {
'work-alt': {
...admissionQuota(20, reset, now),
spendControl: spentCreditBudget(creditReset),
},
},
fallbackAccountIds: { 'work-alt': 'chatgpt-work-alt' },
mainQuota: admissionQuota(100, reset, now),
})

const response = await loaded.fetchOverride(
'https://api.openai.com/v1/responses',
requestInit(),
)

expect(response.status).toBe(200)
expect(seenAuth).toEqual(['Bearer work-alt-token'])
} finally {
globalThis.fetch = originalFetch
await hooks?.dispose?.()
}
})

it('admission quota skips a file-exhausted fallback with an empty process quota cache', async () => {
const now = Date.now()
const reset = new Date(now + 7 * 24 * 3600_000).toISOString()
Expand Down
60 changes: 60 additions & 0 deletions packages/opencode/src/tests/sidebar-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,66 @@ describe('isQuotaExhausted / exhaustedQuotaResetAt', () => {
const quota: AccountQuota = { secondary: windowAt(30, future) }
expect(isQuotaExhausted(quota, now)).toBe(false)
})

const spendControlAt = (
reached: boolean,
resetsAt?: string,
): AccountQuota['spendControl'] => ({
limit: 2500,
used: reached ? 2500 : 500,
remaining: reached ? 0 : 2000,
usedPercent: reached ? 100 : 20,
remainingPercent: reached ? 0 : 80,
...(resetsAt === undefined ? {} : { resetsAt }),
reached,
})

test('a reached credit budget with a future reset exhausts the account', () => {
const quota: AccountQuota = {
primary: windowAt(20, future),
spendControl: spendControlAt(true, laterFuture),
}
expect(isQuotaExhausted(quota, now)).toBe(true)
expect(exhaustedQuotaResetAt(quota, now)).toEqual({
resetsAt: laterFuture,
resetAtMs: Date.parse(laterFuture),
})
})

test('a healthy credit budget does not exhaust the account', () => {
const quota: AccountQuota = {
primary: windowAt(20, future),
spendControl: spendControlAt(false, laterFuture),
}
expect(isQuotaExhausted(quota, now)).toBe(false)
})

test('the credit reset competes with window resets for the earliest', () => {
const quota: AccountQuota = {
primary: windowAt(100, laterFuture),
spendControl: spendControlAt(true, future),
}
expect(exhaustedQuotaResetAt(quota, now)).toEqual({
resetsAt: future,
resetAtMs: Date.parse(future),
})
})

test.each([
['missing reset', spendControlAt(true)],
['malformed reset', spendControlAt(true, 'not-a-date')],
['reset already past', spendControlAt(true, past)],
])(
'fails open on a reached credit budget with %s',
(_label, spendControl) => {
const quota: AccountQuota = {
primary: windowAt(20, future),
spendControl,
}
expect(isQuotaExhausted(quota, now)).toBe(false)
expect(exhaustedQuotaResetAt(quota, now)).toBeUndefined()
},
)
})
test('machine write ranks a fresh secondary window above an older incoming primary', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'oai-sb-secondary-fresh-'))
Expand Down
Loading
Loading