Skip to content
Open
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
136 changes: 134 additions & 2 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
loadAccounts,
log,
mergeAnthropicBetas,
type OAuthQuotaSnapshot,
PARALLEL_TOOL_CALLS_SYSTEM_PROMPT,
parseCache1hCommandAction,
parseCacheKeepCommandAction,
Expand Down Expand Up @@ -566,6 +567,106 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
})
}

function quotaBar(pct: number, width = 10): string {
const filled = Math.max(0, Math.min(Math.round((pct / 100) * width), width))
return '█'.repeat(filled) + '░'.repeat(width - filled)
}

function quotaLine(label: string, pct: number): string {
return `${label} ${quotaBar(pct)} ${String(Math.round(pct)).padStart(3)}%`
}

function formatResetIn(resetsAt: string | undefined): string {
if (!resetsAt) return ''
const ms = new Date(resetsAt).getTime() - Date.now()
if (ms <= 0) return 'resets now'
const mins = Math.floor(ms / 60_000)
if (mins < 60) return `resets ${mins}m`
const hrs = Math.floor(mins / 60)
const rm = mins % 60
return rm > 0 ? `resets ${hrs}h${rm}m` : `resets ${hrs}h`
}

function showQuotaToast(
quota: OAuthQuotaSnapshot | null,
fallbacks?: Array<{
id: string
label?: string
quota?: OAuthQuotaSnapshot
}>,
activeAccountId?: string,
) {
const sections: string[] = []
let globalMaxUsed = 0

// Main account
if (quota) {
const fh = quota.five_hour
const sd = quota.seven_day
if (fh || sd) {
const mainActive = activeAccountId === 'main'
const status = mainActive ? 'active' : 'idle'
const reset = formatResetIn(fh?.resetsAt)
const lines: string[] = [
`main · ${status}${reset ? ` (${reset})` : ''}`,
]
if (fh) {
lines.push(quotaLine('5h', fh.usedPercent))
globalMaxUsed = Math.max(globalMaxUsed, fh.usedPercent)
}
if (sd) {
lines.push(quotaLine('7d', sd.usedPercent))
globalMaxUsed = Math.max(globalMaxUsed, sd.usedPercent)
}
sections.push(lines.join('\n'))
}
}

// Fallback accounts
if (fallbacks?.length) {
for (const fb of fallbacks) {
const q = fb.quota
if (!q) continue
const fh = q.five_hour
const sd = q.seven_day
if (!fh && !sd) continue
const name = fb.label || 'alt'
const fbActive = activeAccountId === fb.id
const status = fbActive ? 'active' : 'idle'
const fbReset = formatResetIn(fh?.resetsAt)
const lines: string[] = [
`${name} · ${status}${fbReset ? ` (${fbReset})` : ''}`,
]
if (fh) {
lines.push(quotaLine('5h', fh.usedPercent))
globalMaxUsed = Math.max(globalMaxUsed, fh.usedPercent)
}
if (sd) {
lines.push(quotaLine('7d', sd.usedPercent))
globalMaxUsed = Math.max(globalMaxUsed, sd.usedPercent)
}
sections.push(lines.join('\n'))
}
}

if (!sections.length) return
const message = sections.join('\n\n')
const variant =
globalMaxUsed >= 90 ? 'error' : globalMaxUsed >= 70 ? 'warning' : 'info'

// biome-ignore lint/suspicious/noExplicitAny: SDK client.tui type not exposed to server plugins
void (client.tui as any)
?.showToast?.({
body: {
title: 'Claude Quota',
message,
variant,
duration: variant === 'error' ? 8000 : 5000,
},
})
?.catch?.(() => {})
}

return {
config: async (config: { command?: Record<string, unknown> }) => {
config.command = {
Expand Down Expand Up @@ -1557,6 +1658,32 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
trace.done('missing_access_error')
throw new Error('OAuth access token is missing after refresh')
}
/** Show quota toast from current QuotaManager state */
function showQuotaToastFromCache() {
const mainEntry = quotaManager.getMain()
if (!mainEntry) return
// Prefer the shared QuotaManager cache for fallback quota so the
// toast matches the sidebar and reflects background refreshes
// rather than the request-start storage snapshot.
const fallbacks = (storage?.accounts ?? [])
.filter((a) => a.enabled !== false)
.map((a) => ({
...a,
quota: quotaManager.getFallback(a.id)?.quota ?? a.quota,
}))
const mainPassesPolicy = quotaSnapshotPassesPolicy(
mainEntry.quota,
storage,
)
let activeId: string | undefined
if (mainPassesPolicy) {
activeId = 'main'
} else {
activeId = fallbacks[0]?.id
}
showQuotaToast(mainEntry.quota, fallbacks, activeId)
}

if (replayableRequest && mainQuotaRoutingEnabled(storage)) {
try {
const quotaStart = nowMs()
Expand All @@ -1566,10 +1693,15 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
let routingQuota = quotaManager.getMain(auth.access)?.quota
if (!routingQuota) {
routingQuota = await quotaManager.refreshMain(auth.access)
showQuotaToastFromCache()
} else if (quotaManager.needsRefresh(sessionRequestCount)) {
// Stale OR every-N request boundary — background refresh,
// return current snapshot to avoid blocking.
void quotaManager.refreshMain(auth.access).catch(() => {})
// return current snapshot to avoid blocking. Show the toast
// when the refresh completes.
void quotaManager
.refreshMain(auth.access)
.then(() => showQuotaToastFromCache())
.catch(() => {})
}
trace.mark('main_quota_for_routing', {
ms: roundMs(nowMs() - quotaStart),
Expand Down