@goodandready/dsh-subscriptions 0.6.26 → 0.6.28
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/lib/adapter.js +34 -3
- package/lib/client.js +195 -7
- package/lib/crypto.js +32 -13
- package/lib/gemini-schema.js +7 -5
- package/lib/history.js +104 -0
- package/lib/images.js +125 -0
- package/lib/import-auth.js +34 -3
- package/lib/messages.js +122 -34
- package/lib/refs.js +1 -0
- package/lib/routes/accounts.js +101 -84
- package/lib/runaway-guard.js +105 -0
- package/lib/stream-rotate.js +3 -1
- package/lib/tools-normalizer.js +118 -0
- package/lib/usage.js +21 -0
- package/lib/vault.js +3 -3
- package/lib/vendors/copilot.js +8 -0
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -118,6 +118,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
118
118
|
},
|
|
119
119
|
streamOnce: async function* (account, opts) {
|
|
120
120
|
const t0 = Date.now()
|
|
121
|
+
let firstChunkTime = null
|
|
122
|
+
let outputTokens = 0
|
|
123
|
+
let approxChars = 0
|
|
121
124
|
const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
122
125
|
const vendor = getVendor(provider)
|
|
123
126
|
// ponytail: capture x-ratelimit headers without touching vendors
|
|
@@ -145,7 +148,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
145
148
|
}
|
|
146
149
|
try {
|
|
147
150
|
if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
|
|
148
|
-
|
|
151
|
+
for await (const chunk of vendor.streamOnce({
|
|
149
152
|
blob,
|
|
150
153
|
options: opts,
|
|
151
154
|
fetchImpl: quotaFetch,
|
|
@@ -153,12 +156,37 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
153
156
|
config: deps.vendorConfig(provider),
|
|
154
157
|
signal: opts.signal,
|
|
155
158
|
saveBlob: (next) => deps.saveBlob(account.ref, next),
|
|
156
|
-
})
|
|
159
|
+
})) {
|
|
160
|
+
if (firstChunkTime === null) {
|
|
161
|
+
firstChunkTime = Date.now()
|
|
162
|
+
}
|
|
163
|
+
if (chunk && typeof chunk === 'object') {
|
|
164
|
+
if (chunk.type === 'usage' && chunk.usage) {
|
|
165
|
+
const u = toTokenUsage(chunk.usage)
|
|
166
|
+
if (u && typeof u.outputTokens === 'number' && u.outputTokens > 0) {
|
|
167
|
+
outputTokens = u.outputTokens
|
|
168
|
+
}
|
|
169
|
+
} else if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
170
|
+
approxChars += chunk.text.length
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
yield chunk
|
|
174
|
+
}
|
|
175
|
+
if (!outputTokens && approxChars > 0) {
|
|
176
|
+
outputTokens = Math.max(1, Math.round(approxChars / 4))
|
|
177
|
+
}
|
|
157
178
|
// stream completed successfully: clear cooldown and health penalties
|
|
158
179
|
if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
|
|
159
180
|
// #65: record in the request and cost history.
|
|
160
181
|
if (typeof deps.recordHistory === 'function') {
|
|
161
182
|
try {
|
|
183
|
+
const totalMs = Date.now() - t0
|
|
184
|
+
const ttftMs = firstChunkTime !== null ? Math.max(0, firstChunkTime - t0) : null
|
|
185
|
+
const genMs = (firstChunkTime !== null && totalMs > ttftMs) ? (totalMs - ttftMs) : 0
|
|
186
|
+
const tps = (outputTokens > 0 && genMs > 0)
|
|
187
|
+
? Math.round((outputTokens / (genMs / 1000)) * 10) / 10
|
|
188
|
+
: null
|
|
189
|
+
|
|
162
190
|
deps.recordHistory({
|
|
163
191
|
provider,
|
|
164
192
|
ref: account.ref,
|
|
@@ -166,7 +194,10 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
166
194
|
path: '/responses',
|
|
167
195
|
method: 'POST',
|
|
168
196
|
status: 200,
|
|
169
|
-
ms:
|
|
197
|
+
ms: totalMs,
|
|
198
|
+
ttftMs,
|
|
199
|
+
tps,
|
|
200
|
+
outputTokens: outputTokens || undefined,
|
|
170
201
|
kind: 'stream',
|
|
171
202
|
})
|
|
172
203
|
} catch { /* ignore quota capture failure */ }
|
package/lib/client.js
CHANGED
|
@@ -149,6 +149,7 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
'.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
|
|
150
150
|
'.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
151
151
|
'.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
|
|
152
|
+
'.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-default,#3b82f6);background:color-mix(in srgb,var(--dsw-alias-brand-default,#3b82f6) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-default,#3b82f6) 20%,transparent)}' +
|
|
152
153
|
'.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
|
|
153
154
|
'.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
|
|
154
155
|
'.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
|
|
@@ -321,6 +322,22 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
321
322
|
],
|
|
322
323
|
link: "https://aws.amazon.com",
|
|
323
324
|
},
|
|
325
|
+
copilot: {
|
|
326
|
+
title: "GitHub Copilot",
|
|
327
|
+
stepsEn: [
|
|
328
|
+
"1. Click \"Device Login\" (or \"Connect\") on the slot card.",
|
|
329
|
+
"2. A one-time code will appear on the card.",
|
|
330
|
+
"3. Open https://github.com/login/device, enter the code, and approve access for your GitHub account with Copilot subscription.",
|
|
331
|
+
"4. The plugin automatically captures tokens and enables Copilot models.",
|
|
332
|
+
],
|
|
333
|
+
stepsZh: [
|
|
334
|
+
"1. 点击卡片上的 \"设备码登录\" (Device Login) 或 \"连接\"。",
|
|
335
|
+
"2. 界面将展示一次性设备授权码。",
|
|
336
|
+
"3. 在浏览器打开 https://github.com/login/device 输入代码,并为您拥有 Copilot 订阅的 GitHub 账户授权。",
|
|
337
|
+
"4. 授权成功后,插件会自动捕获会话并激活 GitHub Copilot 官方模型。",
|
|
338
|
+
],
|
|
339
|
+
link: "https://github.com/login/device",
|
|
340
|
+
},
|
|
324
341
|
}
|
|
325
342
|
|
|
326
343
|
const en = {
|
|
@@ -503,7 +520,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
503
520
|
"healthQuarantine": "Quarantine",
|
|
504
521
|
"healthWarmup": "Warmup",
|
|
505
522
|
"healthActive": "Active",
|
|
506
|
-
"healthIdle": "Idle"
|
|
523
|
+
"healthIdle": "Idle",
|
|
524
|
+
"speedMetricsHint": "Time to first token (TTFT) and token generation speed",
|
|
525
|
+
"backupExport": "Export Backup",
|
|
526
|
+
"backupImport": "Import Backup",
|
|
527
|
+
"backupExportTitle": "Export Encrypted Backup",
|
|
528
|
+
"backupImportTitle": "Import Encrypted Backup",
|
|
529
|
+
"backupExportAction": "Export",
|
|
530
|
+
"backupImportAction": "Import",
|
|
531
|
+
"backupPassphrase": "Vault Passphrase (min 4 chars)",
|
|
532
|
+
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM and PBKDF2.",
|
|
533
|
+
"backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
|
|
534
|
+
"backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
|
|
535
|
+
"backupExportOk": "Backup exported successfully",
|
|
536
|
+
"backupImportOk": "Backup imported successfully",
|
|
537
|
+
"backupDownload": "Download .enc"
|
|
507
538
|
};
|
|
508
539
|
|
|
509
540
|
const zh = {
|
|
@@ -686,7 +717,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
686
717
|
"healthQuarantine": "隔离期",
|
|
687
718
|
"healthWarmup": "预热中",
|
|
688
719
|
"healthActive": "活跃",
|
|
689
|
-
"healthIdle": "空闲"
|
|
720
|
+
"healthIdle": "空闲",
|
|
721
|
+
"speedMetricsHint": "首字响应延迟 (TTFT) 与每秒生成速度 (Tokens/s)",
|
|
722
|
+
"backupExport": "导出备份",
|
|
723
|
+
"backupImport": "导入备份",
|
|
724
|
+
"backupExportTitle": "导出加密备份",
|
|
725
|
+
"backupImportTitle": "导入加密备份",
|
|
726
|
+
"backupExportAction": "导出",
|
|
727
|
+
"backupImportAction": "导入",
|
|
728
|
+
"backupPassphrase": "凭据库密码(至少 4 位)",
|
|
729
|
+
"backupExportHint": "使用 AES-256-GCM 与 PBKDF2 安全加密导出所有订阅账户与配置。",
|
|
730
|
+
"backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
|
|
731
|
+
"backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
|
|
732
|
+
"backupExportOk": "备份导出成功",
|
|
733
|
+
"backupImportOk": "备份导入成功",
|
|
734
|
+
"backupDownload": "下载 .enc 文件"
|
|
690
735
|
};
|
|
691
736
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
692
737
|
function cleanErrorMessage(raw) {
|
|
@@ -908,6 +953,64 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
908
953
|
const [telemetry, setTelemetry] = React.useState(null)
|
|
909
954
|
const [smoke, setSmoke] = React.useState({ testing: false, result: null })
|
|
910
955
|
const [updater, setUpdater] = React.useState({ checking: false, updating: false, status: null, message: '' })
|
|
956
|
+
const [backupModal, setBackupModal] = React.useState(null)
|
|
957
|
+
const [backupPass, setBackupPass] = React.useState('')
|
|
958
|
+
const [backupData, setBackupData] = React.useState('')
|
|
959
|
+
const [backupErr, setBackupErr] = React.useState('')
|
|
960
|
+
const [backupInfo, setBackupInfo] = React.useState('')
|
|
961
|
+
const [backupLoading, setBackupLoading] = React.useState(false)
|
|
962
|
+
|
|
963
|
+
const doExportBackup = async () => {
|
|
964
|
+
setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
|
|
965
|
+
try {
|
|
966
|
+
const res = await fetch('/dsh-subscriptions/export-backup', {
|
|
967
|
+
method: 'POST',
|
|
968
|
+
headers: { 'Content-Type': 'application/json' },
|
|
969
|
+
body: JSON.stringify({ passphrase: backupPass, algorithm: 'pbkdf2' }),
|
|
970
|
+
})
|
|
971
|
+
const d = await res.json().catch(() => ({}))
|
|
972
|
+
if (!res.ok || !d.ok) throw new Error((d.error && d.error.message) || ('HTTP ' + res.status))
|
|
973
|
+
setBackupData(d.vault)
|
|
974
|
+
setBackupInfo(t('backupExportOk') + ' (' + d.slotCount + ' slots)')
|
|
975
|
+
} catch (e) {
|
|
976
|
+
setBackupErr(cleanErrorMessage(e.message || e))
|
|
977
|
+
}
|
|
978
|
+
setBackupLoading(false)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const doImportBackup = async () => {
|
|
982
|
+
setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
|
|
983
|
+
try {
|
|
984
|
+
const res = await fetch('/dsh-subscriptions/import-backup', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'Content-Type': 'application/json' },
|
|
987
|
+
body: JSON.stringify({ passphrase: backupPass, vault: backupData.trim() }),
|
|
988
|
+
})
|
|
989
|
+
const d = await res.json().catch(() => ({}))
|
|
990
|
+
if (!res.ok || !d.ok) throw new Error((d.error && d.error.message) || ('HTTP ' + res.status))
|
|
991
|
+
setBackupInfo(t('backupImportOk') + ' (' + d.slotCount + ' slots)')
|
|
992
|
+
if (d.accounts) setAccounts(d.accounts)
|
|
993
|
+
await reload()
|
|
994
|
+
} catch (e) {
|
|
995
|
+
setBackupErr(cleanErrorMessage(e.message || e))
|
|
996
|
+
}
|
|
997
|
+
setBackupLoading(false)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const downloadBackupFile = () => {
|
|
1001
|
+
if (!backupData) return
|
|
1002
|
+
try {
|
|
1003
|
+
const blob = new window.Blob([backupData], { type: 'text/plain;charset=utf-8' })
|
|
1004
|
+
const url = URL.createObjectURL(blob)
|
|
1005
|
+
const a = document.createElement('a')
|
|
1006
|
+
a.href = url
|
|
1007
|
+
a.download = 'dsh-subscriptions-backup.enc'
|
|
1008
|
+
document.body.appendChild(a)
|
|
1009
|
+
a.click()
|
|
1010
|
+
document.body.removeChild(a)
|
|
1011
|
+
URL.revokeObjectURL(url)
|
|
1012
|
+
} catch { /* ignore */ }
|
|
1013
|
+
}
|
|
911
1014
|
|
|
912
1015
|
const checkUpdater = () => {
|
|
913
1016
|
setUpdater((u) => Object.assign({}, u, { checking: true, message: '' }))
|
|
@@ -1056,6 +1159,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1056
1159
|
|
|
1057
1160
|
const connect = async (provider, index) => {
|
|
1058
1161
|
setErr('')
|
|
1162
|
+
if (provider === 'copilot') {
|
|
1163
|
+
return deviceStartLogin(provider, index).catch((e) => setErr(cleanErrorMessage(e.message || e)))
|
|
1164
|
+
}
|
|
1059
1165
|
if (provider === 'antigravity' && (!draft || !draft.antigravityClientId || !String(draft.antigravityClientId).trim())) {
|
|
1060
1166
|
setErr(t('missingClientIdAntigravity'))
|
|
1061
1167
|
return
|
|
@@ -1521,6 +1627,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1521
1627
|
React.createElement('div', { className: 'dsub-row' },
|
|
1522
1628
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: genDiag }, t('diagGenerate')),
|
|
1523
1629
|
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: runAnalyzeSession }, analyzingCache ? '…' : t('cacheAnalyze')),
|
|
1630
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('export'); setBackupErr(''); setBackupInfo('') } }, '🔒 ' + t('backupExport')),
|
|
1631
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: () => { setBackupModal('import'); setBackupErr(''); setBackupInfo('') } }, '📥 ' + t('backupImport')),
|
|
1524
1632
|
React.createElement('a', { href: 'https://github.com/GooDAnDReaDY/dsh-subscriptions/issues', target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('diagIssues')),
|
|
1525
1633
|
),
|
|
1526
1634
|
React.createElement('div', { className: 'dsub-sub' }, t('diagHint')),
|
|
@@ -1535,6 +1643,63 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1535
1643
|
}, t('diagCopy')),
|
|
1536
1644
|
React.createElement('pre', { className: 'dsub-diag' }, diag),
|
|
1537
1645
|
) : null,
|
|
1646
|
+
backupModal ? React.createElement('div', { className: 'dsub-panel-box', style: { marginTop: '10px' } },
|
|
1647
|
+
React.createElement('div', { className: 'dsub-panel-title' },
|
|
1648
|
+
React.createElement('span', null, backupModal === 'export' ? ('🔒 ' + t('backupExportTitle')) : ('📥 ' + t('backupImportTitle'))),
|
|
1649
|
+
React.createElement('button', {
|
|
1650
|
+
type: 'button',
|
|
1651
|
+
className: 'dsub-mini',
|
|
1652
|
+
onClick: () => { setBackupModal(null); setBackupPass(''); setBackupData(''); setBackupErr(''); setBackupInfo('') }
|
|
1653
|
+
}, '✕ ' + t('closeModal'))
|
|
1654
|
+
),
|
|
1655
|
+
React.createElement('div', { className: 'dsub-panel-desc' },
|
|
1656
|
+
backupModal === 'export' ? t('backupExportHint') : t('backupImportHint')
|
|
1657
|
+
),
|
|
1658
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' } },
|
|
1659
|
+
backupModal === 'import' ? React.createElement('textarea', {
|
|
1660
|
+
className: 'dsub-diag',
|
|
1661
|
+
style: { width: '100%', height: '80px', fontFamily: 'monospace', fontSize: '11px' },
|
|
1662
|
+
placeholder: t('backupPayloadPlaceholder'),
|
|
1663
|
+
value: backupData,
|
|
1664
|
+
onChange: (e) => setBackupData(e.target.value),
|
|
1665
|
+
}) : null,
|
|
1666
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1667
|
+
React.createElement('input', {
|
|
1668
|
+
type: 'password',
|
|
1669
|
+
className: 'dsub-grow',
|
|
1670
|
+
placeholder: t('backupPassphrase'),
|
|
1671
|
+
value: backupPass,
|
|
1672
|
+
onChange: (e) => setBackupPass(e.target.value),
|
|
1673
|
+
}),
|
|
1674
|
+
backupModal === 'export' ? React.createElement('button', {
|
|
1675
|
+
type: 'button',
|
|
1676
|
+
className: 'dsub-mini',
|
|
1677
|
+
disabled: backupLoading || !backupPass || backupPass.length < 4,
|
|
1678
|
+
onClick: doExportBackup,
|
|
1679
|
+
}, backupLoading ? '…' : t('backupExportAction')) : React.createElement('button', {
|
|
1680
|
+
type: 'button',
|
|
1681
|
+
className: 'dsub-mini',
|
|
1682
|
+
disabled: backupLoading || !backupPass || !backupData,
|
|
1683
|
+
onClick: doImportBackup,
|
|
1684
|
+
}, backupLoading ? '…' : t('backupImportAction'))
|
|
1685
|
+
),
|
|
1686
|
+
backupErr ? React.createElement('div', { className: 'dsub-bad' }, backupErr) : null,
|
|
1687
|
+
backupInfo ? React.createElement('div', { className: 'dsub-ok' }, backupInfo) : null,
|
|
1688
|
+
(backupModal === 'export' && backupData) ? React.createElement('div', { className: 'dsub-row' },
|
|
1689
|
+
React.createElement('button', {
|
|
1690
|
+
type: 'button',
|
|
1691
|
+
className: 'dsub-mini',
|
|
1692
|
+
onClick: () => { try { navigator.clipboard.writeText(backupData) } catch { /* ignore */ } },
|
|
1693
|
+
}, t('diagCopy')),
|
|
1694
|
+
React.createElement('button', {
|
|
1695
|
+
type: 'button',
|
|
1696
|
+
className: 'dsub-mini',
|
|
1697
|
+
onClick: downloadBackupFile,
|
|
1698
|
+
}, t('backupDownload')),
|
|
1699
|
+
React.createElement('pre', { className: 'dsub-diag', style: { maxHeight: '60px', overflow: 'hidden', textOverflow: 'ellipsis' } }, backupData)
|
|
1700
|
+
) : null
|
|
1701
|
+
)
|
|
1702
|
+
) : null,
|
|
1538
1703
|
),
|
|
1539
1704
|
names.map((prov) => {
|
|
1540
1705
|
const rows = slots
|
|
@@ -1564,7 +1729,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1564
1729
|
type: 'button', className: 'dsub-mini',
|
|
1565
1730
|
onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
1566
1731
|
}, on ? t('reconnect') : t('connect')),
|
|
1567
|
-
(row.slot.provider === 'codex' ? React.createElement('button', {
|
|
1732
|
+
((row.slot.provider === 'codex' || row.slot.provider === 'copilot') ? React.createElement('button', {
|
|
1568
1733
|
type: 'button', className: 'dsub-mini',
|
|
1569
1734
|
onClick: () => deviceStartLogin(row.slot.provider, row.slot.index).catch((e) => setErr(cleanErrorMessage(e.message || e))),
|
|
1570
1735
|
}, t('deviceLogin')) : null),
|
|
@@ -1826,7 +1991,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1826
1991
|
var est = Math.floor(rem / (w.usedPercent / account.requests))
|
|
1827
1992
|
if (est > 0) extra = React.createElement('span', null, t('forecast') + ' ' + est)
|
|
1828
1993
|
}
|
|
1829
|
-
|
|
1994
|
+
var resetElem = w.resetAt ? React.createElement(ResetCountdown, { key: 'reset-' + idx, resetAt: w.resetAt }) : null
|
|
1995
|
+
var extraContent = (resetElem && extra)
|
|
1996
|
+
? React.createElement('span', { className: 'dsub-row', style: { gap: '6px' } }, resetElem, extra)
|
|
1997
|
+
: (resetElem || extra)
|
|
1998
|
+
items.push(renderItem(windowLabel(w), w.usedPercent, extraContent))
|
|
1830
1999
|
}
|
|
1831
2000
|
})
|
|
1832
2001
|
}
|
|
@@ -1851,11 +2020,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1851
2020
|
React.createElement('div', { className: 'dsub-usage-grid' }, items)
|
|
1852
2021
|
)
|
|
1853
2022
|
})(),
|
|
1854
|
-
(account.paidTierName || account.ref ? React.createElement('div', { className: 'dsub-slot-meta-row' },
|
|
2023
|
+
((account.paidTierName || account.ref || (telemetry && telemetry.accounts && telemetry.accounts[account.ref])) ? React.createElement('div', { className: 'dsub-slot-meta-row' },
|
|
1855
2024
|
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' },
|
|
1856
2025
|
React.createElement('strong', null, t('plan') + ': '),
|
|
1857
2026
|
account.paidTierName
|
|
1858
2027
|
) : null,
|
|
2028
|
+
(function(){
|
|
2029
|
+
const accTelem = telemetry && telemetry.accounts && telemetry.accounts[account.ref]
|
|
2030
|
+
if (!accTelem) return null
|
|
2031
|
+
const parts = []
|
|
2032
|
+
if (accTelem.avgTtftMs != null) parts.push('TTFT: ' + accTelem.avgTtftMs + 'ms')
|
|
2033
|
+
else if (accTelem.lastTtftMs != null) parts.push('TTFT: ' + accTelem.lastTtftMs + 'ms')
|
|
2034
|
+
if (accTelem.avgTps != null) parts.push(accTelem.avgTps + ' t/s')
|
|
2035
|
+
else if (accTelem.lastTps != null) parts.push(accTelem.lastTps + ' t/s')
|
|
2036
|
+
if (parts.length === 0) return null
|
|
2037
|
+
return React.createElement('span', { className: 'dsub-speed-tag', title: t('speedMetricsHint') },
|
|
2038
|
+
'⚡ ' + parts.join(' · ')
|
|
2039
|
+
)
|
|
2040
|
+
})(),
|
|
1859
2041
|
account.ref ? React.createElement('span', { className: 'dsub-ref-tag' },
|
|
1860
2042
|
React.createElement('span', null, t('storedAs')),
|
|
1861
2043
|
React.createElement('code', null, account.ref)
|
|
@@ -2166,7 +2348,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2166
2348
|
if (state.active) {
|
|
2167
2349
|
const a = state.active
|
|
2168
2350
|
const wins = Array.isArray(a.windows) ? a.windows : []
|
|
2169
|
-
const winStr = wins.map((w) =>
|
|
2351
|
+
const winStr = wins.map((w) => {
|
|
2352
|
+
let s = (w.label || w.id) + ' ' + Math.round(w.usedPercent) + '%'
|
|
2353
|
+
if (w.resetAt && w.resetAt > now) {
|
|
2354
|
+
s += ' (' + formatRelativeReset(w.resetAt, t('lang'), now) + ')'
|
|
2355
|
+
}
|
|
2356
|
+
return s
|
|
2357
|
+
}).join(' ')
|
|
2170
2358
|
const provName = a.provider ? (a.provider.charAt(0).toUpperCase() + a.provider.slice(1)) : ''
|
|
2171
2359
|
pillText = (a.provider === 'codex' && a.fastMode ? '⚡ ' : '') + provName + (winStr ? ' · ' + winStr : (a.usagePercent != null ? ' · ' + Math.round(a.usagePercent) + '%' : ''))
|
|
2172
2360
|
}
|
|
@@ -2209,7 +2397,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2209
2397
|
),
|
|
2210
2398
|
secWin ? React.createElement('div', null,
|
|
2211
2399
|
React.createElement('div', { className: 'dsub-barLabelRow' },
|
|
2212
|
-
React.createElement('span', null, (secWin.label || '7d') + ' window'),
|
|
2400
|
+
React.createElement('span', null, ((secWin.label || '7d') + ' window') + (secWin && secWin.resetAt ? ' (' + formatRelativeReset(secWin.resetAt, t('lang'), now) + ')' : '')),
|
|
2213
2401
|
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, secPct + '%'),
|
|
2214
2402
|
),
|
|
2215
2403
|
React.createElement('div', { className: 'dsub-barTrack' },
|
package/lib/crypto.js
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
1
|
-
import { scryptSync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
1
|
+
import { scryptSync, pbkdf2Sync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
2
2
|
|
|
3
|
-
// AES-256-GCM with
|
|
4
|
-
//
|
|
5
|
-
export function encryptWithPassphrase(plainText, passphrase) {
|
|
3
|
+
// AES-256-GCM with PBKDF2 (100,000 iterations, SHA-256) (DSHE2:) or scrypt (DSHE1:).
|
|
4
|
+
// Backward-compatible across both key derivation algorithms.
|
|
5
|
+
export function encryptWithPassphrase(plainText, passphrase, { algorithm = "scrypt" } = {}) {
|
|
6
6
|
const salt = randomBytes(16)
|
|
7
|
-
const
|
|
7
|
+
const isPbkdf2 = algorithm === "pbkdf2"
|
|
8
|
+
const key = isPbkdf2
|
|
9
|
+
? pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
|
|
10
|
+
: scryptSync(String(passphrase), salt, 32)
|
|
8
11
|
const iv = randomBytes(12)
|
|
9
12
|
const cipher = createCipheriv("aes-256-gcm", key, iv)
|
|
10
13
|
const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
|
|
11
14
|
const tag = cipher.getAuthTag()
|
|
12
15
|
// format: magic + salt + iv + tag + data
|
|
13
|
-
|
|
16
|
+
const prefix = isPbkdf2 ? "DSHE2:" : "DSHE1:"
|
|
17
|
+
return prefix + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export function decryptWithPassphrase(payload, passphrase) {
|
|
17
21
|
const raw = String(payload || "")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
let key
|
|
23
|
+
let salt
|
|
24
|
+
let iv
|
|
25
|
+
let tag
|
|
26
|
+
let data
|
|
27
|
+
if (raw.startsWith("DSHE2:")) {
|
|
28
|
+
const buf = Buffer.from(raw.slice(6), "base64")
|
|
29
|
+
salt = buf.subarray(0, 16)
|
|
30
|
+
iv = buf.subarray(16, 28)
|
|
31
|
+
tag = buf.subarray(28, 44)
|
|
32
|
+
data = buf.subarray(44)
|
|
33
|
+
key = pbkdf2Sync(String(passphrase), salt, 100000, 32, "sha256")
|
|
34
|
+
} else if (raw.startsWith("DSHE1:")) {
|
|
35
|
+
const buf = Buffer.from(raw.slice(6), "base64")
|
|
36
|
+
salt = buf.subarray(0, 16)
|
|
37
|
+
iv = buf.subarray(16, 28)
|
|
38
|
+
tag = buf.subarray(28, 44)
|
|
39
|
+
data = buf.subarray(44)
|
|
40
|
+
key = scryptSync(String(passphrase), salt, 32)
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error("unknown export format")
|
|
43
|
+
}
|
|
25
44
|
const decipher = createDecipheriv("aes-256-gcm", key, iv)
|
|
26
45
|
decipher.setAuthTag(tag)
|
|
27
46
|
return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8")
|
package/lib/gemini-schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeTools } from './tools-normalizer.js'
|
|
1
2
|
const TYPE_MAP = {
|
|
2
3
|
string: 'string',
|
|
3
4
|
number: 'number',
|
|
@@ -33,7 +34,7 @@ export function toGeminiSchema(schema, depth) {
|
|
|
33
34
|
else type = 'string'
|
|
34
35
|
}
|
|
35
36
|
const out = { type }
|
|
36
|
-
if (typed && typed.nullable) out.nullable = true
|
|
37
|
+
if ((typed && typed.nullable) || schema.nullable === true) out.nullable = true
|
|
37
38
|
if (schema.description) out.description = String(schema.description)
|
|
38
39
|
if (type === 'object') {
|
|
39
40
|
const props = schema.properties
|
|
@@ -65,10 +66,11 @@ export function toGeminiSchema(schema, depth) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export function geminiFunctionDeclarations(tools) {
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const norm = normalizeTools(tools)
|
|
70
|
+
if (!norm) return undefined
|
|
71
|
+
return norm.map((tool) => ({
|
|
70
72
|
name: tool.name,
|
|
71
|
-
description: tool.description
|
|
72
|
-
parameters: toGeminiSchema(tool.parameters
|
|
73
|
+
description: tool.description,
|
|
74
|
+
parameters: toGeminiSchema(tool.parameters),
|
|
73
75
|
}))
|
|
74
76
|
}
|
package/lib/history.js
CHANGED
|
@@ -109,6 +109,108 @@ export class HistoryStore {
|
|
|
109
109
|
return this.rows.length
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Summarize session and 24-hour telemetry for UI stat cards. */
|
|
113
|
+
accountTelemetry(ref) {
|
|
114
|
+
if (!ref) return null
|
|
115
|
+
let count = 0
|
|
116
|
+
let ttftSum = 0
|
|
117
|
+
let ttftCount = 0
|
|
118
|
+
let lastTtft = null
|
|
119
|
+
let tpsSum = 0
|
|
120
|
+
let tpsCount = 0
|
|
121
|
+
let lastTps = null
|
|
122
|
+
let totalOutputTokens = 0
|
|
123
|
+
let latencySum = 0
|
|
124
|
+
let lastLatency = 0
|
|
125
|
+
|
|
126
|
+
for (const r of this.rows) {
|
|
127
|
+
if (r.ref !== ref) continue
|
|
128
|
+
count++
|
|
129
|
+
if (typeof r.ms === "number" && r.ms > 0) {
|
|
130
|
+
latencySum += r.ms
|
|
131
|
+
if (!lastLatency) lastLatency = r.ms
|
|
132
|
+
}
|
|
133
|
+
if (typeof r.ttftMs === "number" && r.ttftMs >= 0) {
|
|
134
|
+
ttftSum += r.ttftMs
|
|
135
|
+
ttftCount++
|
|
136
|
+
if (lastTtft === null) lastTtft = r.ttftMs
|
|
137
|
+
}
|
|
138
|
+
if (typeof r.tps === "number" && r.tps > 0) {
|
|
139
|
+
tpsSum += r.tps
|
|
140
|
+
tpsCount++
|
|
141
|
+
if (lastTps === null) lastTps = r.tps
|
|
142
|
+
}
|
|
143
|
+
if (typeof r.outputTokens === "number" && r.outputTokens > 0) {
|
|
144
|
+
totalOutputTokens += r.outputTokens
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (count === 0) return null
|
|
148
|
+
return {
|
|
149
|
+
totalRequests: count,
|
|
150
|
+
avgLatencyMs: count > 0 ? Math.round(latencySum / count) : 0,
|
|
151
|
+
lastLatencyMs: lastLatency,
|
|
152
|
+
avgTtftMs: ttftCount > 0 ? Math.round(ttftSum / ttftCount) : null,
|
|
153
|
+
lastTtftMs: lastTtft,
|
|
154
|
+
avgTps: tpsCount > 0 ? Math.round((tpsSum / tpsCount) * 10) / 10 : null,
|
|
155
|
+
lastTps: lastTps,
|
|
156
|
+
totalOutputTokens,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
accountsTelemetry() {
|
|
161
|
+
const map = {}
|
|
162
|
+
for (const r of this.rows) {
|
|
163
|
+
if (!r.ref) continue
|
|
164
|
+
if (!map[r.ref]) {
|
|
165
|
+
map[r.ref] = {
|
|
166
|
+
count: 0,
|
|
167
|
+
latencySum: 0,
|
|
168
|
+
lastLatency: 0,
|
|
169
|
+
ttftSum: 0,
|
|
170
|
+
ttftCount: 0,
|
|
171
|
+
lastTtft: null,
|
|
172
|
+
tpsSum: 0,
|
|
173
|
+
tpsCount: 0,
|
|
174
|
+
lastTps: null,
|
|
175
|
+
totalOutputTokens: 0,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const st = map[r.ref]
|
|
179
|
+
st.count++
|
|
180
|
+
if (typeof r.ms === "number" && r.ms > 0) {
|
|
181
|
+
st.latencySum += r.ms
|
|
182
|
+
if (!st.lastLatency) st.lastLatency = r.ms
|
|
183
|
+
}
|
|
184
|
+
if (typeof r.ttftMs === "number" && r.ttftMs >= 0) {
|
|
185
|
+
st.ttftSum += r.ttftMs
|
|
186
|
+
st.ttftCount++
|
|
187
|
+
if (st.lastTtft === null) st.lastTtft = r.ttftMs
|
|
188
|
+
}
|
|
189
|
+
if (typeof r.tps === "number" && r.tps > 0) {
|
|
190
|
+
st.tpsSum += r.tps
|
|
191
|
+
st.tpsCount++
|
|
192
|
+
if (st.lastTps === null) st.lastTps = r.tps
|
|
193
|
+
}
|
|
194
|
+
if (typeof r.outputTokens === "number" && r.outputTokens > 0) {
|
|
195
|
+
st.totalOutputTokens += r.outputTokens
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const out = {}
|
|
199
|
+
for (const [ref, st] of Object.entries(map)) {
|
|
200
|
+
out[ref] = {
|
|
201
|
+
totalRequests: st.count,
|
|
202
|
+
avgLatencyMs: st.count > 0 ? Math.round(st.latencySum / st.count) : 0,
|
|
203
|
+
lastLatencyMs: st.lastLatency,
|
|
204
|
+
avgTtftMs: st.ttftCount > 0 ? Math.round(st.ttftSum / st.ttftCount) : null,
|
|
205
|
+
lastTtftMs: st.lastTtft,
|
|
206
|
+
avgTps: st.tpsCount > 0 ? Math.round((st.tpsSum / st.tpsCount) * 10) / 10 : null,
|
|
207
|
+
lastTps: st.lastTps,
|
|
208
|
+
totalOutputTokens: st.totalOutputTokens,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out
|
|
212
|
+
}
|
|
213
|
+
|
|
112
214
|
/** Summarize session and 24-hour telemetry for UI stat cards. */
|
|
113
215
|
telemetrySummary() {
|
|
114
216
|
const total = this.rows.length
|
|
@@ -124,6 +226,7 @@ export class HistoryStore {
|
|
|
124
226
|
lastRequestAt: null,
|
|
125
227
|
lastProvider: null,
|
|
126
228
|
lastModel: null,
|
|
229
|
+
accounts: {},
|
|
127
230
|
}
|
|
128
231
|
}
|
|
129
232
|
let success = 0
|
|
@@ -157,6 +260,7 @@ export class HistoryStore {
|
|
|
157
260
|
lastRequestAt: (first && first.ts) || null,
|
|
158
261
|
lastProvider: (first && first.provider) || null,
|
|
159
262
|
lastModel: (first && first.model) || null,
|
|
263
|
+
accounts: this.accountsTelemetry(),
|
|
160
264
|
}
|
|
161
265
|
}
|
|
162
266
|
}
|