@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
|
@@ -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
|
|
@@ -7,7 +7,7 @@ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid"
|
|
|
7
7
|
import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
|
|
8
8
|
/** @jsxImportSource @opentui/solid */
|
|
9
9
|
|
|
10
|
-
import { getSidebarState } from "../sidebar-state.js";
|
|
10
|
+
import { getCollapsedQuotaSummary, getSidebarState, resolveSessionSidebarRouting } from "../sidebar-state.js";
|
|
11
11
|
import { openUrl } from "../util/open-url";
|
|
12
12
|
export function buildCachekeepDialogOptions(payload) {
|
|
13
13
|
const enabled = payload.knobs.enabled === true;
|
|
@@ -101,7 +101,7 @@ function showSetCachekeepWindowPrompt(api, apply, state, render) {
|
|
|
101
101
|
onCancel: () => render(state)
|
|
102
102
|
}));
|
|
103
103
|
}
|
|
104
|
-
export function openCommandDialog(api, payload, apply) {
|
|
104
|
+
export function openCommandDialog(api, payload, apply, sessionId) {
|
|
105
105
|
if (payload.command === 'openai-routing') {
|
|
106
106
|
const current = payload.knobs.mode ?? 'main-first';
|
|
107
107
|
const DialogSelect = api.ui.DialogSelect;
|
|
@@ -203,7 +203,7 @@ export function openCommandDialog(api, payload, apply) {
|
|
|
203
203
|
}, {
|
|
204
204
|
title: 'Edit thresholds\u2026',
|
|
205
205
|
value: 'edit',
|
|
206
|
-
description: 'Set per-account
|
|
206
|
+
description: 'Set per-account primary,secondary cutoffs'
|
|
207
207
|
}],
|
|
208
208
|
onSelect: option => {
|
|
209
209
|
if (option.value === 'edit') {
|
|
@@ -293,7 +293,7 @@ export function openCommandDialog(api, payload, apply) {
|
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
if (payload.command === 'openai-account') {
|
|
296
|
-
openAccountDialog(api, apply);
|
|
296
|
+
openAccountDialog(api, apply, sessionId);
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -303,12 +303,13 @@ export function openCommandDialog(api, payload, apply) {
|
|
|
303
303
|
|
|
304
304
|
// -- Accounts dialog ---------------------------------------------------------
|
|
305
305
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (quota
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
export function formatQuotaWindows(quota) {
|
|
307
|
+
const windowSummary = getCollapsedQuotaSummary(quota ?? null).text;
|
|
308
|
+
if (windowSummary) return windowSummary;
|
|
309
|
+
if (quota?.resetCreditsAvailable !== undefined) {
|
|
310
|
+
return `resets: ${quota.resetCreditsAvailable}`;
|
|
311
|
+
}
|
|
312
|
+
return 'no quota data';
|
|
312
313
|
}
|
|
313
314
|
function osc52Copy(api, text) {
|
|
314
315
|
try {
|
|
@@ -321,7 +322,19 @@ function osc52Copy(api, text) {
|
|
|
321
322
|
}
|
|
322
323
|
return false;
|
|
323
324
|
}
|
|
324
|
-
function
|
|
325
|
+
export function buildAccountDialogRows(state, sessionId) {
|
|
326
|
+
const activeId = resolveSessionSidebarRouting(state, sessionId).activeId;
|
|
327
|
+
return [{
|
|
328
|
+
title: `main${activeId === 'main' ? ' \u2022 active' : ''}`,
|
|
329
|
+
value: 'main',
|
|
330
|
+
description: formatQuotaWindows(state.main.quota)
|
|
331
|
+
}, ...state.fallbacks.map(fb => ({
|
|
332
|
+
title: `${fb.label ?? fb.id}${activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
333
|
+
value: fb.id,
|
|
334
|
+
description: formatQuotaWindows(fb.quota)
|
|
335
|
+
}))];
|
|
336
|
+
}
|
|
337
|
+
function openAccountDialog(api, apply, sessionId) {
|
|
325
338
|
const DialogConfirm = api.ui.DialogConfirm;
|
|
326
339
|
function showL1() {
|
|
327
340
|
void getSidebarState().then(state => {
|
|
@@ -330,15 +343,7 @@ function openAccountDialog(api, apply) {
|
|
|
330
343
|
api.ui.dialog.replace(() => _$createComponent(DialogSelectInner, {
|
|
331
344
|
title: "OpenAI Accounts",
|
|
332
345
|
get options() {
|
|
333
|
-
return [{
|
|
334
|
-
title: `main${state.activeId === 'main' || !state.activeId ? ' \u2022 active' : ''}`,
|
|
335
|
-
value: 'main',
|
|
336
|
-
description: formatQuota5h7d(state.main.quota)
|
|
337
|
-
}, ...state.fallbacks.map(fb => ({
|
|
338
|
-
title: `${fb.label ?? fb.id}${state.activeId === fb.id ? ' \u2022 active' : ''}${!fb.enabled ? ' (disabled)' : ''}`,
|
|
339
|
-
value: fb.id,
|
|
340
|
-
description: formatQuota5h7d(fb.quota)
|
|
341
|
-
})), {
|
|
346
|
+
return [...buildAccountDialogRows(state, sessionId), {
|
|
342
347
|
title: 'Add account\u2026',
|
|
343
348
|
value: '__add__',
|
|
344
349
|
description: 'Sign in to a new OpenAI account'
|