@jeffusion/bungee-core 5.10.0 → 5.12.0
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/dist/main.js +31436 -17327
- package/dist/plugins/ai-transformer/index.js.map +2 -2
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
- package/dist/plugins/chatgpt-oauth/control.js +850 -47
- package/dist/plugins/chatgpt-oauth/control.js.map +16 -7
- package/dist/plugins/chatgpt-oauth/index.js +691 -16
- package/dist/plugins/chatgpt-oauth/index.js.map +15 -7
- package/dist/plugins/chatgpt-oauth/manifest.json +117 -7
- package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +25 -15
- package/dist/plugins/chatgpt-oauth/ui/ChatgptQuotaWidget.svelte +7 -4
- package/dist/plugins/chatgpt-oauth/ui/account-model.js +16 -7
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
- package/dist/plugins/key-access/index.js.map +2 -2
- package/dist/plugins/key-rate-limit/index.js.map +2 -2
- package/dist/plugins/local-accounts/control.js.map +2 -2
- package/dist/plugins/local-accounts/index.js.map +2 -2
- package/dist/plugins/model-mapping/control.js +72 -17741
- package/dist/plugins/model-mapping/control.js.map +6 -58
- package/dist/plugins/model-mapping/index.js +32 -17433
- package/dist/plugins/model-mapping/index.js.map +6 -58
- package/dist/plugins/model-mapping/manifest.json +30 -14
- package/dist/plugins/models-dev/control.js +1042 -0
- package/dist/plugins/models-dev/control.js.map +16 -0
- package/dist/plugins/models-dev/index.js +15418 -0
- package/dist/plugins/models-dev/index.js.map +134 -0
- package/dist/plugins/models-dev/manifest.json +199 -0
- package/dist/plugins/models-dev/ui/ModelsDevSettings.svelte +120 -0
- package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
- package/dist/plugins/signature-repair/index.js.map +2 -2
- package/dist/plugins/token-budget/control.js +524 -9
- package/dist/plugins/token-budget/control.js.map +12 -4
- package/dist/plugins/token-budget/index.js +519 -12
- package/dist/plugins/token-budget/index.js.map +13 -6
- package/dist/plugins/token-budget/manifest.json +16 -2
- package/dist/plugins/token-metering/index.js +93 -2
- package/dist/plugins/token-metering/index.js.map +12 -6
- package/dist/plugins/token-stats/control.js +174 -312
- package/dist/plugins/token-stats/control.js.map +7 -7
- package/dist/plugins/token-stats/index.js +283 -486
- package/dist/plugins/token-stats/index.js.map +16 -12
- package/dist/plugins/token-stats/manifest.json +99 -12
- package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +76 -0
- package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +87 -0
- package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +136 -80
- package/dist/plugins/token-stats/ui/client-model-search.test.ts +58 -0
- package/dist/plugins/token-stats/ui/client-model-search.ts +16 -0
- package/dist/plugins/token-stats/ui/model-search.ts +45 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +114 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.ts +17 -0
- package/package.json +2 -2
|
@@ -41,6 +41,25 @@
|
|
|
41
41
|
}
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
|
+
"services": {
|
|
45
|
+
"provides": [
|
|
46
|
+
{
|
|
47
|
+
"id": "chatgpt-oauth.credentials.v1",
|
|
48
|
+
"version": 1,
|
|
49
|
+
"process": "control",
|
|
50
|
+
"kind": "rpc"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"consumes": [
|
|
54
|
+
{
|
|
55
|
+
"id": "chatgpt-oauth.credentials.v1",
|
|
56
|
+
"version": 1,
|
|
57
|
+
"process": "worker",
|
|
58
|
+
"plugin": "chatgpt-oauth",
|
|
59
|
+
"kind": "rpc"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
44
63
|
"contributes": {
|
|
45
64
|
"nativeWidgets": [
|
|
46
65
|
{
|
|
@@ -60,6 +79,42 @@
|
|
|
60
79
|
"execution": "control",
|
|
61
80
|
"capability": "config.read"
|
|
62
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"path": "/accounts/codex",
|
|
84
|
+
"methods": [
|
|
85
|
+
"GET"
|
|
86
|
+
],
|
|
87
|
+
"handler": "listCodexAccounts",
|
|
88
|
+
"execution": "control",
|
|
89
|
+
"capability": "config.read"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"path": "/accounts/siwc",
|
|
93
|
+
"methods": [
|
|
94
|
+
"GET"
|
|
95
|
+
],
|
|
96
|
+
"handler": "listSiwcAccounts",
|
|
97
|
+
"execution": "control",
|
|
98
|
+
"capability": "config.read"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "/accounts/siwc/draft",
|
|
102
|
+
"methods": [
|
|
103
|
+
"POST"
|
|
104
|
+
],
|
|
105
|
+
"handler": "createSiwcDraft",
|
|
106
|
+
"execution": "control",
|
|
107
|
+
"capability": "config.write"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"path": "/login/siwc",
|
|
111
|
+
"methods": [
|
|
112
|
+
"POST"
|
|
113
|
+
],
|
|
114
|
+
"handler": "startSiwcLogin",
|
|
115
|
+
"execution": "control",
|
|
116
|
+
"capability": "config.write"
|
|
117
|
+
},
|
|
63
118
|
{
|
|
64
119
|
"path": "/accounts/usage",
|
|
65
120
|
"methods": [
|
|
@@ -191,7 +246,7 @@
|
|
|
191
246
|
{
|
|
192
247
|
"id": "chatgpt",
|
|
193
248
|
"label": "ChatGPT OAuth",
|
|
194
|
-
"listAccounts": "
|
|
249
|
+
"listAccounts": "listCodexAccounts",
|
|
195
250
|
"createDraft": "createDraft",
|
|
196
251
|
"credentialPolicy": {
|
|
197
252
|
"allowedOrigins": [
|
|
@@ -243,6 +298,51 @@
|
|
|
243
298
|
"Chatgpt-Account-Id"
|
|
244
299
|
]
|
|
245
300
|
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "chatgpt-siwc",
|
|
304
|
+
"label": "Sign in with ChatGPT",
|
|
305
|
+
"listAccounts": "listSiwcAccounts",
|
|
306
|
+
"createDraft": "createSiwcDraft",
|
|
307
|
+
"credentialPolicy": {
|
|
308
|
+
"allowedOrigins": [
|
|
309
|
+
"https://api.openai.com"
|
|
310
|
+
],
|
|
311
|
+
"allowedRequests": [
|
|
312
|
+
{
|
|
313
|
+
"pathname": "/v1/responses",
|
|
314
|
+
"methods": [
|
|
315
|
+
"POST"
|
|
316
|
+
],
|
|
317
|
+
"outboundHeaders": {
|
|
318
|
+
"passthrough": [],
|
|
319
|
+
"set": {
|
|
320
|
+
"Accept": "text/event-stream",
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
"User-Agent": "Bungee/5.11.0",
|
|
323
|
+
"Originator": "Bungee"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"pathname": "/v1/models",
|
|
329
|
+
"methods": [
|
|
330
|
+
"GET"
|
|
331
|
+
],
|
|
332
|
+
"outboundHeaders": {
|
|
333
|
+
"passthrough": [],
|
|
334
|
+
"set": {
|
|
335
|
+
"Accept": "application/json",
|
|
336
|
+
"User-Agent": "Bungee/5.11.0",
|
|
337
|
+
"Originator": "Bungee"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
],
|
|
342
|
+
"allowedHeaderNames": [
|
|
343
|
+
"Authorization"
|
|
344
|
+
]
|
|
345
|
+
}
|
|
246
346
|
}
|
|
247
347
|
],
|
|
248
348
|
"settings": "/accounts",
|
|
@@ -397,8 +497,13 @@
|
|
|
397
497
|
"ui.accountIdentity": "Account: {label}",
|
|
398
498
|
"ui.credentialsHelp": "The plugin keeps credentials; they are not written to service configuration.",
|
|
399
499
|
"ui.loginMethod": "Sign-in method",
|
|
400
|
-
"ui.device": "
|
|
401
|
-
"ui.pkce": "
|
|
500
|
+
"ui.device": "Codex device code",
|
|
501
|
+
"ui.pkce": "Codex browser sign-in",
|
|
502
|
+
"ui.siwc": "Sign in with ChatGPT",
|
|
503
|
+
"ui.siwcHelp": "Open the authorization URL, sign in, then paste the complete callback URL from the browser address bar. The final loopback page may fail to load; you can still copy its URL.",
|
|
504
|
+
"ui.siwcUsage": "Check usage in ChatGPT settings",
|
|
505
|
+
"ui.siwcUsageHelp": "This sign-in method has no public usage or reset-credit query. Open ChatGPT settings to check usage.",
|
|
506
|
+
"ui.authMethod": "Sign-in: {method}",
|
|
402
507
|
"ui.deviceHelp": "Enter the device code on the verification page. This window updates automatically after authorization.",
|
|
403
508
|
"ui.pkceHelp": "Sign in in your browser, then paste the complete final localhost callback URL here.",
|
|
404
509
|
"ui.statusLoading": "Checking login status",
|
|
@@ -472,7 +577,7 @@
|
|
|
472
577
|
"errors.request_cancelled": "The request was cancelled.",
|
|
473
578
|
"errors.unknown": "The operation did not complete. Refresh the status before retrying; do not resubmit authorization.",
|
|
474
579
|
"metadata.name": "ChatGPT OAuth",
|
|
475
|
-
"plugin.description": "Use ChatGPT
|
|
580
|
+
"plugin.description": "Use ChatGPT sign-in and Codex accounts as separate managed upstreams",
|
|
476
581
|
"config.accountRef.label": "Account reference",
|
|
477
582
|
"config.accountRef.description": "The managed ChatGPT account reference; leading and trailing whitespace is not allowed. Codex compatibility is managed automatically by the plugin."
|
|
478
583
|
},
|
|
@@ -604,8 +709,13 @@
|
|
|
604
709
|
"ui.accountIdentity": "账号:{label}",
|
|
605
710
|
"ui.credentialsHelp": "凭据由插件保管,不会写入服务配置。",
|
|
606
711
|
"ui.loginMethod": "登录方式",
|
|
607
|
-
"ui.device": "设备码登录",
|
|
608
|
-
"ui.pkce": "
|
|
712
|
+
"ui.device": "Codex 设备码登录",
|
|
713
|
+
"ui.pkce": "Codex 浏览器登录",
|
|
714
|
+
"ui.siwc": "使用 ChatGPT 登录",
|
|
715
|
+
"ui.siwcHelp": "打开授权地址完成登录,再从浏览器地址栏复制完整回调地址并粘贴到此处。最后的本地回调页面可能无法打开,仍可复制其地址。",
|
|
716
|
+
"ui.siwcUsage": "去 ChatGPT 设置查看用量",
|
|
717
|
+
"ui.siwcUsageHelp": "此登录方式没有公开的用量或重置额度查询。请在 ChatGPT 设置中查看用量。",
|
|
718
|
+
"ui.authMethod": "登录方式:{method}",
|
|
609
719
|
"ui.deviceHelp": "在验证页面输入设备码,完成后这里会自动更新。",
|
|
610
720
|
"ui.pkceHelp": "打开浏览器登录,将最终跳转的完整 localhost 回调地址粘贴到此处。",
|
|
611
721
|
"ui.statusLoading": "正在查询登录状态",
|
|
@@ -679,7 +789,7 @@
|
|
|
679
789
|
"errors.request_cancelled": "请求已取消。",
|
|
680
790
|
"errors.unknown": "操作未完成,请刷新状态后重试。请勿重复提交授权。",
|
|
681
791
|
"metadata.name": "ChatGPT OAuth",
|
|
682
|
-
"plugin.description": "将 ChatGPT
|
|
792
|
+
"plugin.description": "将 ChatGPT 登录和 Codex 账号用作独立的受管理上游",
|
|
683
793
|
"config.accountRef.label": "账号引用",
|
|
684
794
|
"config.accountRef.description": "受管理的 ChatGPT 账号引用;首尾不允许有空白字符。Codex 兼容版本由插件自动管理。"
|
|
685
795
|
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { Input } from '$components/ui/input';
|
|
35
35
|
import * as DropdownMenu from '$components/ui/dropdown-menu';
|
|
36
36
|
import { Close as ResetClose } from '$components/ui/dialog';
|
|
37
|
-
import { loginStates, accountStates, terminal, verificationUrl, loginStatus, parseLoginStart, accountSummary, accountUsage, resetOutcome, errorText, errorCode } from './account-model.js';
|
|
37
|
+
import { loginStates, accountStates, terminal, verificationUrl, loginStatus, parseLoginStart, accountSummary, accountUsage, resetOutcome, errorText, errorCode, accountSourceId, canQueryUsage, loginMethods } from './account-model.js';
|
|
38
38
|
|
|
39
39
|
type Account = ReturnType<typeof accountSummary>;
|
|
40
40
|
type AccountUsage = ReturnType<typeof accountUsage>;
|
|
@@ -50,12 +50,13 @@
|
|
|
50
50
|
const control = <T,>(method: 'GET' | 'POST', path: string, body?: unknown) => requestPluginControl<T>(pluginName, path, method, body, lifetime.signal);
|
|
51
51
|
let accounts = $state<Account[]>([]), refreshing = $state(false), notice = $state('');
|
|
52
52
|
let accountRefreshGeneration = 0;
|
|
53
|
-
let loginOpen = $state(false), kind = $state('
|
|
53
|
+
let loginOpen = $state(false), kind = $state('siwc'), reauthRef = $state<string | undefined>();
|
|
54
54
|
let session = $state.raw<Session | null>(null), status = $state(''), callback = $state(''), loginNotice = $state('');
|
|
55
55
|
let starting = $state(false), submitting = $state(false), cancelling = $state(false), polling = $state(false);
|
|
56
56
|
let pollGeneration = 0, pollingGeneration: number | undefined;
|
|
57
57
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
58
58
|
let active = $derived(session !== null && !terminal(status));
|
|
59
|
+
let methods = $derived(loginMethods(reauthRef ? accounts.find(account => account.id === reauthRef) : undefined));
|
|
59
60
|
let url = $derived(active ? verificationUrl(session?.verificationUri ?? session?.authorizationUrl, kind) : null);
|
|
60
61
|
let action = $state(''), actionAccount = $state<Account | null>(null), actionOpen = $state(false);
|
|
61
62
|
let actionBusy = $state(false), label = $state(''), actionNotice = $state(''), referenceNotice = $state('');
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
let pendingResetByAccount = $state<Record<string, PendingReset>>({});
|
|
83
84
|
|
|
84
85
|
function eligibleForUsage(account: Account) {
|
|
85
|
-
return account
|
|
86
|
+
return canQueryUsage(account);
|
|
86
87
|
}
|
|
87
88
|
function usageFor(account: Account): UsageSnapshot {
|
|
88
89
|
return usageByAccount[account.id] ?? { state: 'loading' };
|
|
@@ -144,7 +145,7 @@
|
|
|
144
145
|
}
|
|
145
146
|
function canReset(account: Account, credit: Credit) {
|
|
146
147
|
const snapshot = usageFor(account);
|
|
147
|
-
return account.available && 'usage' in snapshot && snapshot.resetCredits.state === 'fresh'
|
|
148
|
+
return eligibleForUsage(account) && account.available && 'usage' in snapshot && snapshot.resetCredits.state === 'fresh'
|
|
148
149
|
&& !!snapshot.resetCredits.value && snapshot.resetCredits.value.availableCount > 0 && credit.status === 'available';
|
|
149
150
|
}
|
|
150
151
|
function resetTypeLabel(resetType: unknown) {
|
|
@@ -187,6 +188,7 @@
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
async function refreshUsage(account: Account) {
|
|
191
|
+
if (!eligibleForUsage(account)) return;
|
|
190
192
|
const generation = nextUsageGeneration(account.id);
|
|
191
193
|
usageRefreshing[account.id] = true;
|
|
192
194
|
usageErrors[account.id] = false;
|
|
@@ -237,7 +239,7 @@
|
|
|
237
239
|
if (reauthRef !== accountRef) loginNotice = 'ui.otherSession';
|
|
238
240
|
return;
|
|
239
241
|
}
|
|
240
|
-
invalidatePolling(); session = null; status = ''; callback = ''; loginNotice = '';
|
|
242
|
+
invalidatePolling(); session = null; status = ''; callback = ''; loginNotice = ''; reauthRef = accountRef; kind = loginMethods(accountRef ? accounts.find(account => account.id === accountRef) : undefined)[0];
|
|
241
243
|
}
|
|
242
244
|
function clearSecrets() {
|
|
243
245
|
callback = '';
|
|
@@ -249,7 +251,7 @@
|
|
|
249
251
|
if (pollingGeneration === generation) return;
|
|
250
252
|
clearTimeout(timer); timer = undefined; const current = session.sessionId; pollingGeneration = generation; polling = true;
|
|
251
253
|
try {
|
|
252
|
-
const next = loginStatus(await control('GET', `/login/status?sessionId=${encodeURIComponent(current)}`), current);
|
|
254
|
+
const next = loginStatus(await control('GET', `/login/status?sessionId=${encodeURIComponent(current)}`), current, kind);
|
|
253
255
|
if (generation !== pollGeneration || session?.sessionId !== current || lifetime.signal.aborted) return;
|
|
254
256
|
const firstSuccess = status !== 'success' && next.state === 'success';
|
|
255
257
|
status = String(next.state); loginNotice = '';
|
|
@@ -335,7 +337,7 @@
|
|
|
335
337
|
}
|
|
336
338
|
async function confirmReset(event: SubmitEvent) {
|
|
337
339
|
event.preventDefault();
|
|
338
|
-
if (!resetAccount || !resetCredit || resetBusy || !resetRequestId) return;
|
|
340
|
+
if (!resetAccount || !eligibleForUsage(resetAccount) || !resetCredit || resetBusy || !resetRequestId) return;
|
|
339
341
|
resetBusy = true; resetNotice = '';
|
|
340
342
|
try {
|
|
341
343
|
const result = resetOutcome(await control('POST', '/accounts/usage/reset', {
|
|
@@ -356,7 +358,7 @@
|
|
|
356
358
|
}
|
|
357
359
|
async function confirmAction(event: SubmitEvent) {
|
|
358
360
|
event.preventDefault();
|
|
359
|
-
if (!actionAccount || actionBusy || action === 'references') return;
|
|
361
|
+
if (!actionAccount || actionBusy || action === 'references' || (action === 'auto-reset' && actionAccount.authType === 'siwc')) return;
|
|
360
362
|
if (action === 'rename' && !label.trim()) { actionNotice = 'ui.nameRequired'; return; }
|
|
361
363
|
actionBusy = true;
|
|
362
364
|
const generation = referenceGeneration;
|
|
@@ -383,7 +385,7 @@
|
|
|
383
385
|
if (!useAccount?.available || servicesLoading || serviceError) return;
|
|
384
386
|
const existing = services.find(service => service._uid === serviceChoice);
|
|
385
387
|
if (serviceChoice !== 'new' && !existing) { serviceError = 'ui.serviceMissing'; return; }
|
|
386
|
-
const handoff = { sourcePlugin: pluginName, sourceId:
|
|
388
|
+
const handoff = { sourcePlugin: pluginName, sourceId: accountSourceId(useAccount), accountRef: useAccount.id,
|
|
387
389
|
mode: existing ? 'existing' as const : 'new' as const, ...(existing ? { serviceId: existing._uid } : {}) };
|
|
388
390
|
useOpen = false; loginOpen = false; clearSecrets();
|
|
389
391
|
await tick(); // Flush the dialog close before navigation unmounts the account page.
|
|
@@ -405,7 +407,7 @@
|
|
|
405
407
|
async function continueToRoute() {
|
|
406
408
|
if (!routeAccount?.available || routesLoading || routeError || selectedRoute?.service) return;
|
|
407
409
|
if (routeChoice !== 'new' && !selectedRoute?._uid) { routeError = 'ui.routeMissing'; return; }
|
|
408
|
-
const handoff = { sourcePlugin: pluginName, sourceId:
|
|
410
|
+
const handoff = { sourcePlugin: pluginName, sourceId: accountSourceId(routeAccount), accountRef: routeAccount.id,
|
|
409
411
|
mode: selectedRoute ? 'existing' as const : 'new' as const,
|
|
410
412
|
...(selectedRoute ? { routeId: selectedRoute._uid } : {}) };
|
|
411
413
|
routeOpen = false; loginOpen = false; clearSecrets();
|
|
@@ -457,8 +459,15 @@
|
|
|
457
459
|
{#if !account.email}<p class="text-sm text-zinc-400">{t('ui.noEmail')}</p>{/if}
|
|
458
460
|
{#if account.plan}<p class="text-sm text-zinc-300" data-testid="account-type">{t('ui.accountType', { plan: account.plan })}</p>{/if}
|
|
459
461
|
{#if typeof account.expiresAt === 'number'}<p class="tabular-nums text-xs text-zinc-400">{t('ui.credentialExpiry', { date: dateText(account.expiresAt) })}</p>{/if}
|
|
460
|
-
<p class="text-xs text-zinc-400"
|
|
462
|
+
<p class="text-xs text-zinc-400">{t('ui.authMethod', { method: account.authType === 'siwc' ? t('ui.siwc') : 'Codex' })}</p>
|
|
463
|
+
{#if account.authType !== 'siwc'}<p class="text-xs text-zinc-400" data-testid="auto-reset-status">{t(account.autoResetCredits ? 'ui.autoResetEnabled' : 'ui.autoResetDisabled')}</p>{/if}
|
|
461
464
|
</div>
|
|
465
|
+
{#if account.authType === 'siwc'}
|
|
466
|
+
<div class="space-y-2" data-testid="siwc-usage">
|
|
467
|
+
<p class="text-sm text-zinc-400">{t('ui.siwcUsageHelp')}</p>
|
|
468
|
+
<Button href="https://chatgpt.com" target="_blank" rel="noopener noreferrer" variant="outline" size="sm">{@render actionIcon(ExternalLink)}{t('ui.siwcUsage')}</Button>
|
|
469
|
+
</div>
|
|
470
|
+
{:else}
|
|
462
471
|
<div class="flex flex-wrap items-center gap-2"><span class="nx-field-label">{t('ui.usageLabel')}</span><Button variant="ghost" size="sm" class="aspect-square px-0 [&>span]:mr-0" disabled={!eligibleForUsage(account) || usageRefreshing[account.id]} aria-busy={!!usageRefreshing[account.id]} aria-label={t('ui.refreshUsage')} title={t('ui.refreshUsage')} onclick={() => { if (!usageRefreshing[account.id]) void refreshUsage(account); }}>{@render actionIcon(RefreshCw, usageRefreshing[account.id])}</Button>{#if status !== 'fresh'}<span role="status" class={`text-xs ${status === 'stale' || status === 'partial' ? 'text-amber-300' : 'text-zinc-400'}`}>{t(`ui.usage.${status}`)}</span>{/if}</div>
|
|
463
472
|
{#if 'usage' in snapshot}
|
|
464
473
|
{#if usageErrors[account.id]}<p role="status" class="text-sm text-amber-300">{t('ui.usageFailed')}</p>{/if}
|
|
@@ -500,6 +509,7 @@
|
|
|
500
509
|
</div>
|
|
501
510
|
{:else}<p class="text-sm text-zinc-400">{t('ui.creditDetailsUnavailable')}</p>{/if}
|
|
502
511
|
</div>
|
|
512
|
+
{/if}
|
|
503
513
|
<div class="flex flex-wrap items-center justify-end gap-2" data-testid="account-actions">
|
|
504
514
|
<Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openUse(account)}>{@render actionIcon(Server)}{t('ui.useService')}</Button>
|
|
505
515
|
<Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openRoute(account)}>{@render actionIcon(RouteIcon)}{t('ui.useRoute')}</Button>
|
|
@@ -552,9 +562,9 @@
|
|
|
552
562
|
{#snippet body()}
|
|
553
563
|
{#if !active && !starting}
|
|
554
564
|
<RadioGroup.Root bind:value={kind} aria-label={t('ui.loginMethod')} class="flex flex-wrap gap-4">
|
|
555
|
-
{#each
|
|
565
|
+
{#each methods as method}<div class="flex items-center gap-2"><RadioGroup.Item id={`${id}-${method}`} value={method} /><label for={`${id}-${method}`} class="nx-field-label">{t(`ui.${method}`)}</label></div>{/each}
|
|
556
566
|
</RadioGroup.Root>
|
|
557
|
-
<p class="text-sm text-zinc-400">{t(kind === 'device' ? 'ui.deviceHelp' : 'ui.pkceHelp')}</p>
|
|
567
|
+
<p class="text-sm text-zinc-400">{t(kind === 'device' ? 'ui.deviceHelp' : kind === 'siwc' ? 'ui.siwcHelp' : 'ui.pkceHelp')}</p>
|
|
558
568
|
{/if}
|
|
559
569
|
{#if session}
|
|
560
570
|
<div class="flex flex-wrap items-center justify-between gap-2"><StatusBadge variant={status === 'success' ? 'active' : ['failed', 'expired'].includes(status) ? 'fault' : 'standby'}>{t(loginStates[status as keyof typeof loginStates] ?? 'ui.statusLoading')}</StatusBadge><span class="text-sm text-zinc-400">{t('ui.sessionExpiry', { date: dateText(session.expiresAt) })}</span></div>
|
|
@@ -565,9 +575,9 @@
|
|
|
565
575
|
<label class="space-y-1.5"><span class="nx-field-label">{t('ui.verificationUrl')}</span><Input readonly value={url} /></label>
|
|
566
576
|
<div class="flex flex-wrap gap-2"><Button href={url} target="_blank" rel="noopener noreferrer" variant="outline">{@render actionIcon(ExternalLink)}{t('ui.openVerification')}</Button><Button variant="ghost" onclick={() => copy(url ?? '')}>{@render actionIcon(Copy)}{t('ui.copyUrl')}</Button></div>
|
|
567
577
|
{/if}
|
|
568
|
-
{#if kind === 'pkce' && status === 'pending'}
|
|
578
|
+
{#if (kind === 'pkce' || kind === 'siwc') && status === 'pending'}
|
|
569
579
|
<form onsubmit={submitCallback} class="space-y-3">
|
|
570
|
-
<label class="block space-y-1.5"><span class="nx-field-label">{t('ui.callbackUrl')}</span><Input bind:value={callback} autocomplete="off" spellcheck={false} placeholder=
|
|
580
|
+
<label class="block space-y-1.5"><span class="nx-field-label">{t('ui.callbackUrl')}</span><Input bind:value={callback} autocomplete="off" spellcheck={false} placeholder={kind === 'siwc' ? 'http://127.0.0.1:1455/auth/callback?…' : 'http://localhost:…/auth/callback?…'} /></label>
|
|
571
581
|
<p class="text-sm text-zinc-400">{t('ui.callbackHelp')}</p>
|
|
572
582
|
<Button type="submit" disabled={submitting || !callback.trim()} aria-busy={submitting}>{@render actionIcon(Send, submitting)}{t('ui.submitCallback')}</Button>
|
|
573
583
|
</form>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { getPluginText } from '$utils/plugin-i18n';
|
|
7
7
|
import type { NativeWidgetHeaderChange } from '$components/native-widgets/widget-header';
|
|
8
8
|
import { BCarouselList, LoadingIndicator, MetricBar, StatusBadge } from '$components/industrial';
|
|
9
|
-
import { accountSummary, accountUsage, errorText } from './account-model.js';
|
|
9
|
+
import { accountSummary, accountUsage, errorText, canQueryUsage } from './account-model.js';
|
|
10
10
|
|
|
11
11
|
let { pluginName = 'chatgpt-oauth', onHeaderChange }: { pluginName?: string; selectedRange?: string; onHeaderChange?: NativeWidgetHeaderChange } = $props();
|
|
12
12
|
type Row = { account: ReturnType<typeof accountSummary>; usage?: ReturnType<typeof accountUsage>; error?: string };
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
const account = accountSummary(value);
|
|
74
74
|
if (!account.id || seen.has(account.id)) throw new Error('invalid_response');
|
|
75
75
|
seen.add(account.id);
|
|
76
|
-
next.push({ account, usage: account
|
|
76
|
+
next.push({ account, usage: canQueryUsage(account) ? rows.find(row => row.account.id === account.id)?.usage : undefined });
|
|
77
77
|
} catch { notice = 'ui.widgetPartialAccounts'; }
|
|
78
78
|
}
|
|
79
79
|
rows = next; loaded = true;
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
const worker = async () => {
|
|
82
82
|
while (latest() && cursor < next.length) {
|
|
83
83
|
const index = cursor++, row = next[index];
|
|
84
|
-
if (row.account
|
|
84
|
+
if (!canQueryUsage(row.account)) continue;
|
|
85
85
|
try {
|
|
86
86
|
const usage = accountUsage(await get(`/accounts/usage?accountRef=${encodeURIComponent(row.account.id)}`));
|
|
87
87
|
if (latest()) rows[index] = { account: row.account, usage };
|
|
@@ -121,11 +121,14 @@
|
|
|
121
121
|
</div>
|
|
122
122
|
{#if account.email && account.email !== account.label}<p class="break-all text-xs text-zinc-400">{account.email}</p>{/if}
|
|
123
123
|
{#if account.plan}<p class="text-xs text-zinc-400">{t('ui.accountType', { plan: account.plan })}</p>{/if}
|
|
124
|
+
{#if account.authType !== 'siwc'}
|
|
124
125
|
<div class="flex flex-wrap items-baseline gap-x-2 text-xs text-zinc-400"><span>{t('ui.resetCredits')} · <span class="nx-display tabular-nums text-zinc-100" data-testid="quota-count">{creditCount(row) ?? '—'}</span></span>{#if state !== 'fresh'}<span data-testid="quota-state" class={state === 'stale' || state === 'partial' ? 'text-amber-300' : 'text-zinc-400'}>{t(`ui.usage.${state}`)}</span>{/if}</div>
|
|
125
126
|
{#if row.error}<p class="text-xs text-amber-300">{t(row.error)}</p>{/if}
|
|
127
|
+
{/if}
|
|
126
128
|
</div>
|
|
127
129
|
<div class="min-w-0 space-y-1.5">
|
|
128
|
-
{#if account.
|
|
130
|
+
{#if account.authType === 'siwc'}<a href="https://chatgpt.com" target="_blank" rel="noopener noreferrer" class="text-xs text-nexus-400 underline underline-offset-4" data-testid="siwc-usage-link">{t('ui.siwcUsage')}</a>
|
|
131
|
+
{:else if account.status !== 'active'}<p class="text-xs text-zinc-400">{t('ui.usageSkipped')}</p>
|
|
129
132
|
{:else if !row.usage && busy && !row.error}<LoadingIndicator size="xs" centered={false} height="none" label={t('ui.usageLoading')} />
|
|
130
133
|
{:else}
|
|
131
134
|
{#each [row.usage?.usage.value?.primary, row.usage?.usage.value?.secondary].filter(Boolean) as window}
|
|
@@ -8,7 +8,7 @@ export function verificationUrl(value, kind) {
|
|
|
8
8
|
if (typeof value !== 'string' || value !== value.trim() || value.length > 8192 || /[\u0000-\u001f\u007f]/.test(value)) return null;
|
|
9
9
|
try {
|
|
10
10
|
const url = new URL(value);
|
|
11
|
-
const path = kind === 'device' ? '/codex/device' : '/oauth/authorize';
|
|
11
|
+
const path = kind === 'device' ? '/codex/device' : kind === 'pkce' ? '/oauth/authorize' : kind === 'siwc' ? '/api/accounts/authorize' : null;
|
|
12
12
|
if (url.origin !== 'https://auth.openai.com' || url.pathname !== path || url.username || url.password || url.hash) return null;
|
|
13
13
|
return url.href;
|
|
14
14
|
} catch { return null; }
|
|
@@ -17,7 +17,7 @@ export function verificationUrl(value, kind) {
|
|
|
17
17
|
* @param {unknown} input @param {string} kind @param {number} [now] */
|
|
18
18
|
export function parseLoginStart(input, kind, now = Date.now()) {
|
|
19
19
|
const value = record(input);
|
|
20
|
-
if (!['device', 'pkce'].includes(kind) || typeof value.sessionId !== 'string' || !value.sessionId
|
|
20
|
+
if (!['device', 'pkce', 'siwc'].includes(kind) || typeof value.sessionId !== 'string' || !value.sessionId
|
|
21
21
|
|| value.sessionId !== value.sessionId.trim() || value.sessionId.length > 128 || /[\u0000-\u001f\u007f]/.test(value.sessionId)
|
|
22
22
|
|| typeof value.expiresAt !== 'number' || !Number.isFinite(value.expiresAt) || value.expiresAt <= now
|
|
23
23
|
|| value.expiresAt > now + 30 * 60 * 1000) throw new Error('invalid_response');
|
|
@@ -38,11 +38,11 @@ function record(value) {
|
|
|
38
38
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid_response');
|
|
39
39
|
return /** @type {Record<string, unknown>} */ (value);
|
|
40
40
|
}
|
|
41
|
-
/** @param {unknown} input @param {string} sessionId */
|
|
42
|
-
export function loginStatus(input, sessionId) {
|
|
41
|
+
/** @param {unknown} input @param {string} sessionId @param {string} [expectedKind] */
|
|
42
|
+
export function loginStatus(input, sessionId, expectedKind) {
|
|
43
43
|
const value = record(input);
|
|
44
44
|
if (value.sessionId !== sessionId || typeof value.state !== 'string' || !Object.hasOwn(loginStates, value.state)
|
|
45
|
-
|| typeof value.kind !== 'string' || !['device', 'pkce'].includes(value.kind) || !Number.isFinite(value.expiresAt)) throw new Error('invalid_response');
|
|
45
|
+
|| typeof value.kind !== 'string' || !['device', 'pkce', 'siwc'].includes(value.kind) || (expectedKind !== undefined && value.kind !== expectedKind) || !Number.isFinite(value.expiresAt)) throw new Error('invalid_response');
|
|
46
46
|
return value;
|
|
47
47
|
}
|
|
48
48
|
/** @param {unknown} input */
|
|
@@ -51,6 +51,8 @@ export function accountSummary(input) {
|
|
|
51
51
|
if (typeof value.id !== 'string' || typeof value.label !== 'string' || typeof value.status !== 'string' || !Object.hasOwn(accountStates, value.status)
|
|
52
52
|
|| typeof value.available !== 'boolean') throw new Error('invalid_response');
|
|
53
53
|
const identity = value.identity === undefined ? {} : record(value.identity);
|
|
54
|
+
const authType = value.authType === undefined ? 'codex' : value.authType;
|
|
55
|
+
if (authType !== 'codex' && authType !== 'siwc') throw new Error('invalid_response');
|
|
54
56
|
if (value.autoResetCredits !== undefined && typeof value.autoResetCredits !== 'boolean') throw new Error('invalid_response');
|
|
55
57
|
let pendingAutoReset;
|
|
56
58
|
if (value.pendingAutoReset !== undefined) {
|
|
@@ -62,12 +64,19 @@ export function accountSummary(input) {
|
|
|
62
64
|
|| pending.completed !== false) throw new Error('invalid_response');
|
|
63
65
|
pendingAutoReset = { creditId, redeemRequestId, expiresAt };
|
|
64
66
|
}
|
|
65
|
-
return { id: value.id, label: value.label, status: value.status, available: value.available,
|
|
66
|
-
autoResetCredits: value.autoResetCredits === true, pendingAutoReset,
|
|
67
|
+
return { id: value.id, label: value.label, status: value.status, available: value.available, authType,
|
|
68
|
+
autoResetCredits: authType === 'codex' && value.autoResetCredits === true, pendingAutoReset: authType === 'codex' ? pendingAutoReset : undefined,
|
|
67
69
|
expiresAt: Number.isFinite(value.expiresAt) ? value.expiresAt : undefined,
|
|
68
70
|
email: typeof identity.email === 'string' ? identity.email : undefined,
|
|
69
71
|
plan: typeof identity.planType === 'string' ? identity.planType : undefined };
|
|
70
72
|
}
|
|
73
|
+
/** @param {{authType?: string}} account */
|
|
74
|
+
export const accountSourceId = account => account.authType === 'siwc' ? 'chatgpt-siwc' : 'chatgpt';
|
|
75
|
+
/** @param {{authType?: string, status: string}} account */
|
|
76
|
+
export const canQueryUsage = account => account.authType !== 'siwc' && account.status === 'active';
|
|
77
|
+
/** New accounts default to SIWC; reauthentication stays in its original family.
|
|
78
|
+
* @param {{authType?: string}|undefined} [account] */
|
|
79
|
+
export const loginMethods = account => account === undefined ? ['siwc', 'device', 'pkce'] : account.authType === 'siwc' ? ['siwc'] : ['device', 'pkce'];
|
|
71
80
|
/** @param {unknown} input */
|
|
72
81
|
function usageWindow(input) {
|
|
73
82
|
if (input === undefined || input === null) return undefined;
|