@cortexkit/opencode-openai-auth 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.js +1 -1
- package/dist/codex-http.d.ts +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/core/accounts.d.ts +9 -3
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +2 -2
- package/dist/index-0gccjqxr.js +89 -0
- package/dist/{index-k5zwf3j1.js → index-1anzb8jh.js} +1 -1
- package/dist/index-1r02s323.js +6 -0
- package/dist/index-2ng03cby.js +2 -0
- package/dist/{index-e7m7qbne.js → index-6y6ss1kc.js} +1 -1
- package/dist/{index-1xmasv0x.js → index-b6kp9ydc.js} +1 -1
- package/dist/index-fbfpefx6.js +2 -0
- package/dist/index-y67qn95a.js +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +65 -65
- package/dist/model-costs.d.ts +29 -0
- package/dist/prompt-context.d.ts +1 -1
- package/dist/quota-normalize-8yzxqy78.js +1 -0
- package/dist/quota-normalize.d.ts +13 -4
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +47 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-h46jb3eg.js +1 -0
- package/dist/token-yzcb1p3e.js +1 -0
- package/dist/tui/command-dialogs.d.ts +8 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +18 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +1 -0
- package/package.json +6 -5
- package/src/core/refresh-file-lock.ts +293 -0
- package/src/sidebar-state.ts +393 -13
- package/src/tui/command-dialogs.tsx +43 -25
- package/src/tui-compiled/sidebar-state.ts +393 -13
- package/src/tui-compiled/tui/command-dialogs.tsx +25 -20
- package/src/tui-compiled/tui.tsx +228 -171
- package/src/tui.tsx +142 -64
- package/dist/index-5s67238f.js +0 -2
- package/dist/index-d9hq0tmc.js +0 -2
- package/dist/index-mrg2fms9.js +0 -2
- package/dist/index-qdh7pt5a.js +0 -3
- package/dist/index-y8d01tgn.js +0 -92
- package/dist/quota-normalize-dfdpwwm3.js +0 -1
- package/dist/token-01dcf92a.js +0 -1
- package/dist/token-util-rkqd82q9.js +0 -1
package/src/sidebar-state.ts
CHANGED
|
@@ -2,11 +2,79 @@ export interface QuotaWindow {
|
|
|
2
2
|
usedPercent: number
|
|
3
3
|
remainingPercent: number
|
|
4
4
|
resetsAt?: string
|
|
5
|
+
windowMinutes?: number
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export interface AccountQuota {
|
|
8
9
|
primary?: QuotaWindow
|
|
9
10
|
secondary?: QuotaWindow
|
|
11
|
+
resetCreditsAvailable?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type QuotaWindowKey = 'primary' | 'secondary'
|
|
15
|
+
|
|
16
|
+
const QUOTA_WINDOW_KEYS: readonly QuotaWindowKey[] = ['primary', 'secondary']
|
|
17
|
+
const LEGACY_WINDOW_MINUTES: Record<QuotaWindowKey, number> = {
|
|
18
|
+
primary: 300,
|
|
19
|
+
secondary: 10_080,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function compactUnit(value: number): string {
|
|
23
|
+
return Number.isInteger(value)
|
|
24
|
+
? String(value)
|
|
25
|
+
: String(Math.round(value * 10) / 10)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Derives a short human label ("5h", "1d", "7d") from a window length in
|
|
29
|
+
// minutes. Snapshots written before dynamic windows carry no length, so retain
|
|
30
|
+
// their historical primary=5h and secondary=7d meanings.
|
|
31
|
+
export function formatWindowLabel(
|
|
32
|
+
windowMinutes: number | undefined,
|
|
33
|
+
fallbackKey: QuotaWindowKey,
|
|
34
|
+
): string {
|
|
35
|
+
const minutes =
|
|
36
|
+
windowMinutes !== undefined &&
|
|
37
|
+
Number.isFinite(windowMinutes) &&
|
|
38
|
+
windowMinutes > 0
|
|
39
|
+
? windowMinutes
|
|
40
|
+
: LEGACY_WINDOW_MINUTES[fallbackKey]
|
|
41
|
+
if (minutes < 60) return `${compactUnit(minutes)}m`
|
|
42
|
+
if (minutes < 1_440) return `${compactUnit(minutes / 60)}h`
|
|
43
|
+
return `${compactUnit(minutes / 1_440)}d`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PresentQuotaWindow {
|
|
47
|
+
key: QuotaWindowKey
|
|
48
|
+
label: string
|
|
49
|
+
window: QuotaWindow
|
|
50
|
+
windowMs: number | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Present windows only — an absent slot means "not applicable", not
|
|
54
|
+
// "unknown", so it must never synthesize a placeholder row here.
|
|
55
|
+
export function getPresentQuotaWindows(
|
|
56
|
+
quota: AccountQuota | null,
|
|
57
|
+
): PresentQuotaWindow[] {
|
|
58
|
+
if (!quota) return []
|
|
59
|
+
const rows: PresentQuotaWindow[] = []
|
|
60
|
+
for (const key of QUOTA_WINDOW_KEYS) {
|
|
61
|
+
const window = quota[key]
|
|
62
|
+
if (!window) continue
|
|
63
|
+
const configuredMinutes = window.windowMinutes
|
|
64
|
+
const windowMinutes =
|
|
65
|
+
configuredMinutes !== undefined &&
|
|
66
|
+
Number.isFinite(configuredMinutes) &&
|
|
67
|
+
configuredMinutes > 0
|
|
68
|
+
? configuredMinutes
|
|
69
|
+
: LEGACY_WINDOW_MINUTES[key]
|
|
70
|
+
rows.push({
|
|
71
|
+
key,
|
|
72
|
+
label: formatWindowLabel(windowMinutes, key),
|
|
73
|
+
window,
|
|
74
|
+
windowMs: windowMinutes * 60_000,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return rows
|
|
10
78
|
}
|
|
11
79
|
|
|
12
80
|
export interface SidebarAccountState {
|
|
@@ -15,8 +83,17 @@ export interface SidebarAccountState {
|
|
|
15
83
|
quota: AccountQuota | null
|
|
16
84
|
killed: boolean
|
|
17
85
|
enabled: boolean
|
|
86
|
+
resetCredits?: number
|
|
18
87
|
}
|
|
19
88
|
|
|
89
|
+
export interface ActiveRoutingEntry {
|
|
90
|
+
activeId: string
|
|
91
|
+
route: string
|
|
92
|
+
updatedAt: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ActiveRoutingMap = Record<string, ActiveRoutingEntry>
|
|
96
|
+
|
|
20
97
|
export interface SidebarState {
|
|
21
98
|
main: {
|
|
22
99
|
quota: AccountQuota | null
|
|
@@ -25,10 +102,14 @@ export interface SidebarState {
|
|
|
25
102
|
quotaBackoffUntil?: number
|
|
26
103
|
refreshBackedOff?: boolean
|
|
27
104
|
refreshBackoffUntil?: number
|
|
105
|
+
resetCredits?: number
|
|
28
106
|
}
|
|
29
107
|
fallbacks: SidebarAccountState[]
|
|
108
|
+
/** @deprecated Compatibility field for readers that do not consume activeRouting. */
|
|
30
109
|
activeId: string | undefined
|
|
110
|
+
/** Machine-global routing mode and compatibility value for older readers. */
|
|
31
111
|
route: string
|
|
112
|
+
activeRouting?: ActiveRoutingMap
|
|
32
113
|
planType?: string
|
|
33
114
|
credits?: number
|
|
34
115
|
lastUpdated: number
|
|
@@ -38,6 +119,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
38
119
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
39
120
|
import { tmpdir } from 'node:os'
|
|
40
121
|
import { dirname, join } from 'node:path'
|
|
122
|
+
import { acquireRefreshFileLock } from './core/refresh-file-lock'
|
|
41
123
|
import { createLogger } from './logger'
|
|
42
124
|
|
|
43
125
|
const logSb = createLogger('sidebar')
|
|
@@ -46,6 +128,50 @@ const STATE_FILE_ENV = 'OPENCODE_OPENAI_AUTH_SIDEBAR_STATE_FILE'
|
|
|
46
128
|
const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-openai-auth')
|
|
47
129
|
const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, 'sidebar-state.json')
|
|
48
130
|
|
|
131
|
+
function normalizeResetCredits(value: unknown): number | undefined {
|
|
132
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
133
|
+
? value
|
|
134
|
+
: undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resetCreditsField(value: unknown): { resetCredits?: number } {
|
|
138
|
+
const credits = normalizeResetCredits(value)
|
|
139
|
+
return credits !== undefined ? { resetCredits: credits } : {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeActiveRouting(value: unknown): ActiveRoutingMap | undefined {
|
|
143
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const normalized: ActiveRoutingMap = {}
|
|
148
|
+
for (const [sessionId, rawEntry] of Object.entries(value)) {
|
|
149
|
+
if (
|
|
150
|
+
rawEntry === null ||
|
|
151
|
+
typeof rawEntry !== 'object' ||
|
|
152
|
+
Array.isArray(rawEntry)
|
|
153
|
+
) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const entry = rawEntry as Record<string, unknown>
|
|
157
|
+
if (
|
|
158
|
+
typeof entry.activeId !== 'string' ||
|
|
159
|
+
typeof entry.route !== 'string' ||
|
|
160
|
+
typeof entry.updatedAt !== 'number' ||
|
|
161
|
+
!Number.isFinite(entry.updatedAt)
|
|
162
|
+
) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
normalized[sessionId] = {
|
|
166
|
+
activeId: entry.activeId,
|
|
167
|
+
route: entry.route,
|
|
168
|
+
updatedAt: entry.updatedAt,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined
|
|
173
|
+
}
|
|
174
|
+
|
|
49
175
|
export function getSidebarStateFile(): string {
|
|
50
176
|
return process.env[STATE_FILE_ENV] || DEFAULT_STATE_FILE
|
|
51
177
|
}
|
|
@@ -99,6 +225,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
99
225
|
...(typeof m.refreshBackoffUntil === 'number'
|
|
100
226
|
? { refreshBackoffUntil: m.refreshBackoffUntil }
|
|
101
227
|
: {}),
|
|
228
|
+
...resetCreditsField(m.resetCredits),
|
|
102
229
|
}
|
|
103
230
|
} else {
|
|
104
231
|
main = { quota: null, killed: false }
|
|
@@ -123,6 +250,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
123
250
|
quota: ('quota' in e ? e.quota : null) as AccountQuota | null,
|
|
124
251
|
killed: typeof e.killed === 'boolean' ? e.killed : false,
|
|
125
252
|
enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
|
|
253
|
+
...resetCreditsField(e.resetCredits),
|
|
126
254
|
}))
|
|
127
255
|
: []
|
|
128
256
|
|
|
@@ -139,13 +267,14 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
139
267
|
// Optional top-level fields
|
|
140
268
|
const planType = typeof r.planType === 'string' ? r.planType : undefined
|
|
141
269
|
const credits = typeof r.credits === 'number' ? r.credits : undefined
|
|
142
|
-
|
|
270
|
+
const activeRouting = normalizeActiveRouting(r.activeRouting)
|
|
143
271
|
return {
|
|
144
272
|
main,
|
|
145
273
|
fallbacks,
|
|
146
274
|
activeId,
|
|
147
275
|
route,
|
|
148
276
|
lastUpdated,
|
|
277
|
+
...(activeRouting !== undefined ? { activeRouting } : {}),
|
|
149
278
|
...(planType !== undefined ? { planType } : {}),
|
|
150
279
|
...(credits !== undefined ? { credits } : {}),
|
|
151
280
|
}
|
|
@@ -160,9 +289,90 @@ export async function getSidebarState(): Promise<SidebarState> {
|
|
|
160
289
|
}
|
|
161
290
|
}
|
|
162
291
|
|
|
292
|
+
export const ACTIVE_ROUTING_MAX_AGE_MS = 60 * 60 * 1000
|
|
293
|
+
export const ACTIVE_ROUTING_MAX_ENTRIES = 128
|
|
294
|
+
|
|
295
|
+
export type SidebarRoutingAccount = {
|
|
296
|
+
id: string
|
|
297
|
+
enabled?: boolean
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function isUsableRoutingEntry(
|
|
301
|
+
entry: ActiveRoutingEntry,
|
|
302
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
303
|
+
now = Date.now(),
|
|
304
|
+
): boolean {
|
|
305
|
+
const fresh =
|
|
306
|
+
entry.updatedAt >= now - ACTIVE_ROUTING_MAX_AGE_MS && entry.updatedAt <= now
|
|
307
|
+
if (!fresh) return false
|
|
308
|
+
return (
|
|
309
|
+
entry.activeId === 'main' ||
|
|
310
|
+
accounts.some(
|
|
311
|
+
(account) => account.enabled !== false && account.id === entry.activeId,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function resolveSessionSidebarRouting(
|
|
317
|
+
state: SidebarState,
|
|
318
|
+
sessionId?: string,
|
|
319
|
+
now = Date.now(),
|
|
320
|
+
): { activeId: string; route: string } {
|
|
321
|
+
if (!sessionId) {
|
|
322
|
+
return { activeId: state.activeId ?? 'main', route: state.route }
|
|
323
|
+
}
|
|
324
|
+
const own = sessionId ? state.activeRouting?.[sessionId] : undefined
|
|
325
|
+
if (own && isUsableRoutingEntry(own, state.fallbacks, now)) {
|
|
326
|
+
return { activeId: own.activeId, route: own.route }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const fallback = state.fallbacks.find(
|
|
330
|
+
(account) => account.enabled && !account.killed,
|
|
331
|
+
)
|
|
332
|
+
return {
|
|
333
|
+
activeId:
|
|
334
|
+
state.route === 'fallback-first' && fallback ? fallback.id : 'main',
|
|
335
|
+
route: state.route,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function pruneActiveRouting(
|
|
340
|
+
activeRouting: ActiveRoutingMap | undefined,
|
|
341
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
342
|
+
now = Date.now(),
|
|
343
|
+
removedSessionId?: string,
|
|
344
|
+
): ActiveRoutingMap | undefined {
|
|
345
|
+
if (!activeRouting) return undefined
|
|
346
|
+
const kept = Object.entries(activeRouting).filter(
|
|
347
|
+
([sessionId, entry]) =>
|
|
348
|
+
sessionId !== removedSessionId &&
|
|
349
|
+
isUsableRoutingEntry(entry, accounts, now),
|
|
350
|
+
)
|
|
351
|
+
const bounded =
|
|
352
|
+
kept.length <= ACTIVE_ROUTING_MAX_ENTRIES
|
|
353
|
+
? kept
|
|
354
|
+
: kept
|
|
355
|
+
.sort((left, right) => right[1].updatedAt - left[1].updatedAt)
|
|
356
|
+
.slice(0, ACTIVE_ROUTING_MAX_ENTRIES)
|
|
357
|
+
return bounded.length > 0 ? Object.fromEntries(bounded) : undefined
|
|
358
|
+
}
|
|
359
|
+
|
|
163
360
|
// Serialization chain: concurrent calls are queued so a stale background
|
|
164
361
|
// write cannot land after a newer one and corrupt the file.
|
|
165
362
|
let sidebarWriteChain: Promise<void> = Promise.resolve()
|
|
363
|
+
const MAX_MERGE_ATTEMPTS = 3
|
|
364
|
+
const SIDEBAR_WRITE_LOCK_TTL_MS = 10_000
|
|
365
|
+
const SIDEBAR_WRITE_LOCK_WAIT_MS = 15_000
|
|
366
|
+
|
|
367
|
+
interface SidebarMergeHooks {
|
|
368
|
+
beforeRecheck?: () => void | Promise<void>
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function enqueueSidebarWrite(operation: () => Promise<void>): Promise<void> {
|
|
372
|
+
const result = sidebarWriteChain.then(operation)
|
|
373
|
+
sidebarWriteChain = result.catch(() => {})
|
|
374
|
+
return result
|
|
375
|
+
}
|
|
166
376
|
|
|
167
377
|
/**
|
|
168
378
|
* Write sidebar state to disk, serialized through a promise chain so
|
|
@@ -179,10 +389,177 @@ export function setSidebarState(
|
|
|
179
389
|
state: SidebarState,
|
|
180
390
|
file = getSidebarStateFile(),
|
|
181
391
|
): Promise<void> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
392
|
+
return enqueueSidebarWrite(() => doWriteSidebarState(state, file))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function readSidebarState(file: string): Promise<SidebarState> {
|
|
396
|
+
try {
|
|
397
|
+
return parseSidebarState(await readRawSidebar(file))
|
|
398
|
+
} catch {
|
|
399
|
+
return DEFAULT_SIDEBAR_STATE
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readRawSidebar(file: string): Promise<string> {
|
|
404
|
+
try {
|
|
405
|
+
return await readFile(file, 'utf8')
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if ((error as { code?: string }).code === 'ENOENT') return ''
|
|
408
|
+
throw error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function parseSidebarState(raw: string): SidebarState {
|
|
413
|
+
if (raw === '') return DEFAULT_SIDEBAR_STATE
|
|
414
|
+
try {
|
|
415
|
+
return normalizeSidebarState(JSON.parse(raw))
|
|
416
|
+
} catch {
|
|
417
|
+
return DEFAULT_SIDEBAR_STATE
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function acquireSidebarWriteLock(file: string) {
|
|
422
|
+
await mkdir(dirname(file), { recursive: true })
|
|
423
|
+
const deadline = Date.now() + SIDEBAR_WRITE_LOCK_WAIT_MS
|
|
424
|
+
while (Date.now() <= deadline) {
|
|
425
|
+
const lock = await acquireRefreshFileLock({
|
|
426
|
+
name: 'sidebar-write',
|
|
427
|
+
path: file,
|
|
428
|
+
ttlMs: SIDEBAR_WRITE_LOCK_TTL_MS,
|
|
429
|
+
renew: true,
|
|
430
|
+
})
|
|
431
|
+
if (lock) return lock
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
433
|
+
}
|
|
434
|
+
throw new Error('Timed out waiting for the sidebar state write lock')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function writeMergedSidebarState(
|
|
438
|
+
file: string,
|
|
439
|
+
merge: (latest: SidebarState) => SidebarState,
|
|
440
|
+
hooks?: SidebarMergeHooks,
|
|
441
|
+
): Promise<void> {
|
|
442
|
+
const lock = await acquireSidebarWriteLock(file)
|
|
443
|
+
try {
|
|
444
|
+
// The file lock serializes current writers. Rechecking also preserves data
|
|
445
|
+
// from older plugin processes that do not participate in this lock.
|
|
446
|
+
for (let attempt = 0; attempt < MAX_MERGE_ATTEMPTS; attempt += 1) {
|
|
447
|
+
const rawBefore = await readRawSidebar(file)
|
|
448
|
+
const next = merge(parseSidebarState(rawBefore))
|
|
449
|
+
if (attempt === 0) await hooks?.beforeRecheck?.()
|
|
450
|
+
const rawRecheck = await readRawSidebar(file)
|
|
451
|
+
if (rawRecheck !== rawBefore) continue
|
|
452
|
+
await doWriteSidebarState(next, file)
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const latest = await readSidebarState(file)
|
|
457
|
+
await doWriteSidebarState(merge(latest), file)
|
|
458
|
+
} finally {
|
|
459
|
+
await lock.release()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export type SidebarMachineState = Pick<
|
|
464
|
+
SidebarState,
|
|
465
|
+
'main' | 'fallbacks' | 'planType' | 'credits' | 'lastUpdated'
|
|
466
|
+
> & { route: string }
|
|
467
|
+
|
|
468
|
+
export function setSidebarMachineState(
|
|
469
|
+
machineState: SidebarMachineState,
|
|
470
|
+
file = getSidebarStateFile(),
|
|
471
|
+
hooks?: SidebarMergeHooks,
|
|
472
|
+
): Promise<void> {
|
|
473
|
+
return enqueueSidebarWrite(async () => {
|
|
474
|
+
await writeMergedSidebarState(
|
|
475
|
+
file,
|
|
476
|
+
(latest) => ({
|
|
477
|
+
...latest,
|
|
478
|
+
...machineState,
|
|
479
|
+
activeId: latest.activeId,
|
|
480
|
+
activeRouting: latest.activeRouting,
|
|
481
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
482
|
+
}),
|
|
483
|
+
hooks,
|
|
484
|
+
)
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function upsertSidebarActiveRouting(
|
|
489
|
+
input: { sessionId: string } & ActiveRoutingEntry,
|
|
490
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
491
|
+
file = getSidebarStateFile(),
|
|
492
|
+
hooks?: SidebarMergeHooks,
|
|
493
|
+
): Promise<void> {
|
|
494
|
+
return enqueueSidebarWrite(async () => {
|
|
495
|
+
await writeMergedSidebarState(
|
|
496
|
+
file,
|
|
497
|
+
(latest) => {
|
|
498
|
+
const activeRouting = pruneActiveRouting(
|
|
499
|
+
{
|
|
500
|
+
...latest.activeRouting,
|
|
501
|
+
[input.sessionId]: {
|
|
502
|
+
activeId: input.activeId,
|
|
503
|
+
route: input.route,
|
|
504
|
+
updatedAt: input.updatedAt,
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
accounts,
|
|
508
|
+
Date.now(),
|
|
509
|
+
)
|
|
510
|
+
return {
|
|
511
|
+
...latest,
|
|
512
|
+
activeId: input.activeId,
|
|
513
|
+
route: input.route,
|
|
514
|
+
activeRouting,
|
|
515
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
hooks,
|
|
519
|
+
)
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function setSidebarLegacyRouting(
|
|
524
|
+
input: ActiveRoutingEntry,
|
|
525
|
+
file = getSidebarStateFile(),
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
return enqueueSidebarWrite(async () => {
|
|
528
|
+
await writeMergedSidebarState(file, (latest) => ({
|
|
529
|
+
...latest,
|
|
530
|
+
activeId: input.activeId,
|
|
531
|
+
route: input.route,
|
|
532
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
533
|
+
}))
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function removeSidebarActiveRouting(
|
|
538
|
+
sessionId: string,
|
|
539
|
+
accounts: readonly SidebarRoutingAccount[],
|
|
540
|
+
file = getSidebarStateFile(),
|
|
541
|
+
hooks?: SidebarMergeHooks,
|
|
542
|
+
): Promise<void> {
|
|
543
|
+
return enqueueSidebarWrite(async () => {
|
|
544
|
+
await writeMergedSidebarState(
|
|
545
|
+
file,
|
|
546
|
+
(latest) => {
|
|
547
|
+
const now = Date.now()
|
|
548
|
+
const activeRouting = pruneActiveRouting(
|
|
549
|
+
latest.activeRouting,
|
|
550
|
+
accounts,
|
|
551
|
+
now,
|
|
552
|
+
sessionId,
|
|
553
|
+
)
|
|
554
|
+
return {
|
|
555
|
+
...latest,
|
|
556
|
+
activeRouting,
|
|
557
|
+
lastUpdated: Math.max(Date.now(), latest.lastUpdated + 1),
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
hooks,
|
|
561
|
+
)
|
|
562
|
+
})
|
|
186
563
|
}
|
|
187
564
|
|
|
188
565
|
async function doWriteSidebarState(
|
|
@@ -200,6 +577,7 @@ async function doWriteSidebarState(
|
|
|
200
577
|
pid: process.pid,
|
|
201
578
|
error: e instanceof Error ? e.message : String(e),
|
|
202
579
|
})
|
|
580
|
+
throw e
|
|
203
581
|
}
|
|
204
582
|
}
|
|
205
583
|
|
|
@@ -250,20 +628,22 @@ export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
|
|
|
250
628
|
} {
|
|
251
629
|
const primaryUsedPercent = quota?.primary?.usedPercent ?? null
|
|
252
630
|
const secondaryUsedPercent = quota?.secondary?.usedPercent ?? null
|
|
253
|
-
|
|
254
|
-
return { primaryUsedPercent, secondaryUsedPercent, text: null }
|
|
255
|
-
}
|
|
256
|
-
|
|
631
|
+
const rows = getPresentQuotaWindows(quota)
|
|
257
632
|
return {
|
|
258
633
|
primaryUsedPercent,
|
|
259
634
|
secondaryUsedPercent,
|
|
260
|
-
text:
|
|
635
|
+
text:
|
|
636
|
+
rows.length === 0
|
|
637
|
+
? null
|
|
638
|
+
: rows
|
|
639
|
+
.map(
|
|
640
|
+
({ label, window }) =>
|
|
641
|
+
`${label}: ${Math.round(window.usedPercent)}%`,
|
|
642
|
+
)
|
|
643
|
+
.join(' '),
|
|
261
644
|
}
|
|
262
645
|
}
|
|
263
646
|
|
|
264
|
-
export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
|
|
265
|
-
export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
|
|
266
|
-
|
|
267
647
|
const PACING_MIN_ELAPSED_MS = 5 * 60 * 1000
|
|
268
648
|
const PACING_MIN_ELAPSED_FRACTION = 0.01
|
|
269
649
|
const ON_PACE_DELTA = 1
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
|
|
3
3
|
import type { OpenDialogPayload } from '../rpc/protocol.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type AccountQuota,
|
|
6
|
+
getCollapsedQuotaSummary,
|
|
7
|
+
getSidebarState,
|
|
8
|
+
resolveSessionSidebarRouting,
|
|
9
|
+
type SidebarState,
|
|
10
|
+
} from '../sidebar-state.js'
|
|
5
11
|
import { openUrl } from '../util/open-url'
|
|
6
12
|
|
|
7
13
|
type ApplyFn = (
|
|
@@ -139,6 +145,7 @@ export function openCommandDialog(
|
|
|
139
145
|
api: TuiPluginApi,
|
|
140
146
|
payload: OpenDialogPayload,
|
|
141
147
|
apply: ApplyFn,
|
|
148
|
+
sessionId?: string,
|
|
142
149
|
) {
|
|
143
150
|
if (payload.command === 'openai-routing') {
|
|
144
151
|
const current = (payload.knobs.mode as string) ?? 'main-first'
|
|
@@ -248,7 +255,7 @@ export function openCommandDialog(
|
|
|
248
255
|
{
|
|
249
256
|
title: 'Edit thresholds\u2026',
|
|
250
257
|
value: 'edit',
|
|
251
|
-
description: 'Set per-account
|
|
258
|
+
description: 'Set per-account primary,secondary cutoffs',
|
|
252
259
|
},
|
|
253
260
|
]}
|
|
254
261
|
onSelect={(option) => {
|
|
@@ -345,7 +352,7 @@ export function openCommandDialog(
|
|
|
345
352
|
}
|
|
346
353
|
|
|
347
354
|
if (payload.command === 'openai-account') {
|
|
348
|
-
openAccountDialog(api, apply)
|
|
355
|
+
openAccountDialog(api, apply, sessionId)
|
|
349
356
|
return
|
|
350
357
|
}
|
|
351
358
|
|
|
@@ -355,18 +362,15 @@ export function openCommandDialog(
|
|
|
355
362
|
|
|
356
363
|
// -- Accounts dialog ---------------------------------------------------------
|
|
357
364
|
|
|
358
|
-
function
|
|
359
|
-
quota:
|
|
360
|
-
| { primary?: { usedPercent: number }; secondary?: { usedPercent: number } }
|
|
361
|
-
| null
|
|
362
|
-
| undefined,
|
|
365
|
+
export function formatQuotaWindows(
|
|
366
|
+
quota: AccountQuota | null | undefined,
|
|
363
367
|
): string {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (quota
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return
|
|
368
|
+
const windowSummary = getCollapsedQuotaSummary(quota ?? null).text
|
|
369
|
+
if (windowSummary) return windowSummary
|
|
370
|
+
if (quota?.resetCreditsAvailable !== undefined) {
|
|
371
|
+
return `resets: ${quota.resetCreditsAvailable}`
|
|
372
|
+
}
|
|
373
|
+
return 'no quota data'
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
function osc52Copy(api: TuiPluginApi, text: string): boolean {
|
|
@@ -386,7 +390,30 @@ function osc52Copy(api: TuiPluginApi, text: string): boolean {
|
|
|
386
390
|
return false
|
|
387
391
|
}
|
|
388
392
|
|
|
389
|
-
function
|
|
393
|
+
export function buildAccountDialogRows(
|
|
394
|
+
state: SidebarState,
|
|
395
|
+
sessionId?: string,
|
|
396
|
+
) {
|
|
397
|
+
const activeId = resolveSessionSidebarRouting(state, sessionId).activeId
|
|
398
|
+
return [
|
|
399
|
+
{
|
|
400
|
+
title: `main${activeId === 'main' ? ' \u2022 active' : ''}`,
|
|
401
|
+
value: 'main',
|
|
402
|
+
description: formatQuotaWindows(state.main.quota),
|
|
403
|
+
},
|
|
404
|
+
...state.fallbacks.map((fb) => ({
|
|
405
|
+
title: `${fb.label ?? fb.id}${activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
406
|
+
value: fb.id,
|
|
407
|
+
description: formatQuotaWindows(fb.quota),
|
|
408
|
+
})),
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function openAccountDialog(
|
|
413
|
+
api: TuiPluginApi,
|
|
414
|
+
apply: ApplyFn,
|
|
415
|
+
sessionId?: string,
|
|
416
|
+
) {
|
|
390
417
|
const DialogConfirm = api.ui.DialogConfirm
|
|
391
418
|
|
|
392
419
|
function showL1() {
|
|
@@ -397,16 +424,7 @@ function openAccountDialog(api: TuiPluginApi, apply: ApplyFn) {
|
|
|
397
424
|
<DialogSelectInner
|
|
398
425
|
title='OpenAI Accounts'
|
|
399
426
|
options={[
|
|
400
|
-
|
|
401
|
-
title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
|
|
402
|
-
value: 'main',
|
|
403
|
-
description: formatQuota5h7d(state.main.quota),
|
|
404
|
-
},
|
|
405
|
-
...state.fallbacks.map((fb) => ({
|
|
406
|
-
title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
407
|
-
value: fb.id,
|
|
408
|
-
description: formatQuota5h7d(fb.quota),
|
|
409
|
-
})),
|
|
427
|
+
...buildAccountDialogRows(state, sessionId),
|
|
410
428
|
{
|
|
411
429
|
title: 'Add account\u2026',
|
|
412
430
|
value: '__add__',
|