@goodandready/dsh-subscriptions 0.6.83 → 0.6.85
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/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/accounts.js +1 -0
- package/lib/client.js +91 -31
- package/lib/config-schema.js +4 -0
- package/lib/diagnostic-wizard.js +20 -21
- package/lib/http.js +7 -3
- package/lib/routes/accounts.js +7 -1
- package/lib/routes/status.js +4 -9
- package/lib/sse.js +25 -7
- package/lib/subscriptions.js +2 -1
- package/lib/updater.js +11 -3
- package/lib/vault.js +24 -1
- package/lib/vendor-factory.js +6 -3
- package/lib/vendors/antigravity.js +2 -2
- package/lib/vendors/claude.js +6 -5
- package/lib/vendors/codex.js +5 -5
- package/lib/vendors/cody.js +2 -2
- package/lib/vendors/copilot-cli.js +31 -4
- package/lib/vendors/copilot.js +5 -5
- package/lib/vendors/cursor.js +2 -2
- package/lib/vendors/ernie.js +10 -4
- package/lib/vendors/glm.js +2 -2
- package/lib/vendors/grok.js +6 -6
- package/lib/vendors/jetbrains.js +2 -2
- package/lib/vendors/kimi.js +2 -2
- package/lib/vendors/kiro.js +2 -2
- package/lib/vendors/perplexity.js +2 -2
- package/lib/vendors/qwen.js +2 -2
- package/lib/vendors/replit.js +2 -2
- package/lib/vendors/spark.js +2 -2
- package/lib/wire.js +27 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,6 +310,7 @@ dsh-subscriptions:
|
|
|
310
310
|
- `customVendors.authUrl`, `tokenUrl` and `baseUrl` must use `https://`. Plain `http://` is accepted only for loopback hosts (`localhost`, `127.0.0.1`, `::1`).
|
|
311
311
|
- Grok `baseUrl` overrides must point to `api.x.ai`, `grok.com` / `*.grok.com`, or a loopback host; other hosts are rejected with `GROK_UNTRUSTED_HOST` so bearer tokens never leave trusted xAI infrastructure.
|
|
312
312
|
- Antigravity CLI imports must configure `antigravityClientSecret`; the embedded default was removed.
|
|
313
|
+
- `copilotCliPath` must point to a `copilot`, `github-copilot-cli` or `copilot-cli` binary; rename custom wrappers.
|
|
313
314
|
- The error buffer and diagnostics reports redact bearer tokens, API keys, JWTs and `key=value` credentials before storing or returning them.
|
|
314
315
|
|
|
315
316
|
|
package/README.ru.md
CHANGED
|
@@ -283,6 +283,7 @@ dsh-subscriptions:
|
|
|
283
283
|
- `customVendors.authUrl`, `tokenUrl` и `baseUrl` обязаны использовать `https://`. Открытый `http://` допускается только для loopback-хостов (`localhost`, `127.0.0.1`, `::1`).
|
|
284
284
|
- Переопределение Grok `baseUrl` должно указывать на `api.x.ai`, `grok.com` / `*.grok.com` или loopback-хост; остальные хосты отклоняются с `GROK_UNTRUSTED_HOST`, чтобы bearer-токен не покидал доверенную инфраструктуру xAI.
|
|
285
285
|
- Импорт Antigravity CLI требует явного `antigravityClientSecret`; встроенный default удалён.
|
|
286
|
+
- `copilotCliPath` должен указывать на бинарь `copilot`, `github-copilot-cli` или `copilot-cli`; кастомные обёртки переименуйте.
|
|
286
287
|
- Error buffer и диагностические отчёты редактируют bearer-токены, API-ключи, JWT и `key=value`-credentials до сохранения и выдачи.
|
|
287
288
|
|
|
288
289
|
|
package/README.zh.md
CHANGED
|
@@ -189,6 +189,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
|
189
189
|
- `customVendors.authUrl`、`tokenUrl` 和 `baseUrl` 必须使用 `https://`。明文 `http://` 仅允许用于回环主机(`localhost`、`127.0.0.1`、`::1`)。
|
|
190
190
|
- Grok `baseUrl` 覆盖只能指向 `api.x.ai`、`grok.com` / `*.grok.com` 或回环主机;其他主机会以 `GROK_UNTRUSTED_HOST` 拒绝,避免 bearer 令牌离开可信的 xAI 基础设施。
|
|
191
191
|
- Antigravity CLI 导入必须配置 `antigravityClientSecret`;内置默认值已移除。
|
|
192
|
+
- `copilotCliPath` 必须指向 `copilot`、`github-copilot-cli` 或 `copilot-cli` 可执行文件;自定义包装器请重命名。
|
|
192
193
|
- 错误缓冲区和诊断报告会在存储或返回前脱敏 bearer 令牌、API 密钥、JWT 和 `key=value` 凭据。
|
|
193
194
|
|
|
194
195
|
|
package/lib/accounts.js
CHANGED
|
@@ -379,6 +379,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
379
379
|
label: slot.label || info.label,
|
|
380
380
|
pacePerHour: info.pacePerHour || 0,
|
|
381
381
|
maxConcurrency: slot.maxConcurrency !== undefined ? slot.maxConcurrency : (curConfig.accountMaxConcurrency || 1),
|
|
382
|
+
maxQueueSize: slot.maxQueueSize > 0 ? Number(slot.maxQueueSize) : (curConfig.maxQueueSize || 20),
|
|
382
383
|
})
|
|
383
384
|
}
|
|
384
385
|
return out
|
package/lib/client.js
CHANGED
|
@@ -56,9 +56,19 @@ window.__ModuleLoader__.load({
|
|
|
56
56
|
const PKG = '@goodandready/dsh-subscriptions'
|
|
57
57
|
const ROW_ID = 'dsh-subscriptions'
|
|
58
58
|
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
59
|
-
|
|
60
|
-
let INSTRUCTIONS = {};
|
|
61
59
|
|
|
60
|
+
function safeExternalUrl(raw, allowDataImage) {
|
|
61
|
+
const text = String(raw == null ? '' : raw).trim()
|
|
62
|
+
if (!text) return ''
|
|
63
|
+
if (allowDataImage && /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml)[;,]/i.test(text)) return text
|
|
64
|
+
try {
|
|
65
|
+
const base = (typeof window !== 'undefined' && window.location && window.location.origin) || 'http://127.0.0.1'
|
|
66
|
+
const url = new URL(text, base)
|
|
67
|
+
if (url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'dsh-app:') return url.href
|
|
68
|
+
} catch { /* invalid url */ }
|
|
69
|
+
return ''
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const en = {
|
|
63
73
|
"syncModels": "Sync Models",
|
|
64
74
|
"syncModelsTitle": "Discover and synchronize available models from vendor API",
|
|
@@ -157,6 +167,7 @@ window.__ModuleLoader__.load({
|
|
|
157
167
|
"atRestEncrypted": "Encrypted at Rest (AES-256-GCM)",
|
|
158
168
|
"atRestEncryptedTitle": "Credentials are protected at rest with master key encryption.",
|
|
159
169
|
"maxConcurrencySlotTitle": "Max parallel streams for this account slot (default: 1)",
|
|
170
|
+
"monthlyCostSlotTitle": "Monthly subscription cost in USD for this account (default: 20)",
|
|
160
171
|
"concurrencyQueueHint": "Active and queued parallel requests for this account",
|
|
161
172
|
"activeStreams": "active",
|
|
162
173
|
"queuedStreams": "queued",
|
|
@@ -489,6 +500,7 @@ window.__ModuleLoader__.load({
|
|
|
489
500
|
"atRestEncrypted": "静态存储加密 (AES-256-GCM)",
|
|
490
501
|
"atRestEncryptedTitle": "凭据已通过主密钥进行静态 AES-256-GCM 加密保护。",
|
|
491
502
|
"maxConcurrencySlotTitle": "当前账户槽位最大并发请求数(默认:1)",
|
|
503
|
+
"monthlyCostSlotTitle": "该账户每月订阅费用(美元,默认:20)",
|
|
492
504
|
"concurrencyQueueHint": "当前账户槽位进行中与排队中的并发请求",
|
|
493
505
|
"activeStreams": "活跃",
|
|
494
506
|
"queuedStreams": "排队中",
|
|
@@ -861,6 +873,7 @@ window.__ModuleLoader__.load({
|
|
|
861
873
|
const [modelsCatalog, setModelsCatalog] = React.useState([])
|
|
862
874
|
const [paste, setPaste] = React.useState({})
|
|
863
875
|
const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
|
|
876
|
+
const [loginLinks, setLoginLinks] = React.useState({}) // #595 provider:index -> fallback auth URL when a popup is blocked
|
|
864
877
|
const [proxyRes, setProxyRes] = React.useState({}) // #88 key -> {ok,latencyMs,error}
|
|
865
878
|
const [diag, setDiag] = React.useState('')
|
|
866
879
|
const [sessionAnalysis, setSessionAnalysis] = React.useState(null)
|
|
@@ -1253,10 +1266,10 @@ window.__ModuleLoader__.load({
|
|
|
1253
1266
|
const pendingKeys = Object.entries(device).filter(([, d]) => d && d.status === 'pending')
|
|
1254
1267
|
if (!pendingKeys.length) return
|
|
1255
1268
|
const timers = pendingKeys.map(([key, d]) => setInterval(() => {
|
|
1256
|
-
devicePollOnce(key.split(':')[0], Number(key.split(':')[1]))
|
|
1269
|
+
devicePollOnce(key.split(':')[0], Number(key.split(':')[1])).catch(() => {})
|
|
1257
1270
|
}, d.intervalMs || 5000))
|
|
1258
1271
|
return () => timers.forEach(clearInterval)
|
|
1259
|
-
})
|
|
1272
|
+
}, [device])
|
|
1260
1273
|
|
|
1261
1274
|
// #81: settings snapshot status - never render phantom inputs before the
|
|
1262
1275
|
// config snapshot arrives, and offer a retry when the store is unavailable.
|
|
@@ -1274,7 +1287,13 @@ window.__ModuleLoader__.load({
|
|
|
1274
1287
|
}
|
|
1275
1288
|
|
|
1276
1289
|
const slots = Array.isArray(draft.slots) ? draft.slots : []
|
|
1277
|
-
const setSlots = (next) => setDraft((d) =>
|
|
1290
|
+
const setSlots = (next) => setDraft((d) => {
|
|
1291
|
+
const base = Array.isArray(d && d.slots) ? d.slots : []
|
|
1292
|
+
return Object.assign({}, d, { slots: typeof next === 'function' ? next(base) : next })
|
|
1293
|
+
})
|
|
1294
|
+
const updateSlot = (provider, index, patch) => setSlots((prev) => prev.map((s) => (
|
|
1295
|
+
s && s.provider === provider && Number(s.index) === Number(index) ? Object.assign({}, s, patch) : s
|
|
1296
|
+
)))
|
|
1278
1297
|
const accountOf = (provider, index) => accounts.find((a) => a.provider === provider && a.index === index) || {}
|
|
1279
1298
|
|
|
1280
1299
|
const save = async () => {
|
|
@@ -1306,12 +1325,31 @@ window.__ModuleLoader__.load({
|
|
|
1306
1325
|
setErr(t('missingClientIdAntigravity'))
|
|
1307
1326
|
return
|
|
1308
1327
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1328
|
+
// #595: open the popup synchronously, before the first await, so the
|
|
1329
|
+
// click gesture is still valid in Chrome and Firefox.
|
|
1330
|
+
let popup = null
|
|
1331
|
+
try { popup = window.open('', '_blank') } catch { popup = null }
|
|
1332
|
+
try {
|
|
1333
|
+
const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
|
|
1334
|
+
const data = await res.json().catch(() => ({}))
|
|
1335
|
+
if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1336
|
+
const key = provider + ':' + index
|
|
1337
|
+
if (data.headless) setHeadlessInfo((m) => Object.assign({}, m, { [key]: data.headless }))
|
|
1338
|
+
const authTarget = safeExternalUrl(data.url)
|
|
1339
|
+
if (authTarget) {
|
|
1340
|
+
if (popup && !popup.closed) {
|
|
1341
|
+
popup.location.href = authTarget
|
|
1342
|
+
try { popup.opener = null } catch { /* cross-origin */ }
|
|
1343
|
+
} else {
|
|
1344
|
+
setLoginLinks((m) => Object.assign({}, m, { [key]: authTarget }))
|
|
1345
|
+
}
|
|
1346
|
+
} else if (popup && !popup.closed) {
|
|
1347
|
+
popup.close()
|
|
1348
|
+
}
|
|
1349
|
+
} catch (e) {
|
|
1350
|
+
if (popup && !popup.closed) popup.close()
|
|
1351
|
+
throw e
|
|
1352
|
+
}
|
|
1315
1353
|
}
|
|
1316
1354
|
|
|
1317
1355
|
// #90: device-code login (headless). Server mints user_code; we show it,
|
|
@@ -1509,7 +1547,7 @@ window.__ModuleLoader__.load({
|
|
|
1509
1547
|
const used = slots.filter((s) => s.provider === provider).map((s) => s.index)
|
|
1510
1548
|
let index = 1
|
|
1511
1549
|
while (used.indexOf(index) >= 0) index++
|
|
1512
|
-
setSlots(
|
|
1550
|
+
setSlots((prev) => prev.concat([{ provider: provider, index: index, label: '' }]))
|
|
1513
1551
|
}
|
|
1514
1552
|
|
|
1515
1553
|
const names = providers.length ? providers : [
|
|
@@ -2338,8 +2376,9 @@ window.__ModuleLoader__.load({
|
|
|
2338
2376
|
const account = accountOf(row.slot.provider, row.slot.index)
|
|
2339
2377
|
const key = row.slot.provider + ':' + row.slot.index
|
|
2340
2378
|
const on = !!account.configured
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2379
|
+
const avatarSrc = safeExternalUrl(account.avatarUrl, true)
|
|
2380
|
+
const avatarEl = (avatarSrc ? h('img', {
|
|
2381
|
+
src: avatarSrc,
|
|
2343
2382
|
alt: account.email || account.label || 'Avatar',
|
|
2344
2383
|
className: 'dsub-avatar',
|
|
2345
2384
|
onError: (e) => { e.target.style.display = 'none' },
|
|
@@ -2355,9 +2394,7 @@ window.__ModuleLoader__.load({
|
|
|
2355
2394
|
value: row.slot.label || '',
|
|
2356
2395
|
placeholder: account.email || account.label || (t('accountLabel') + ' ' + row.slot.index),
|
|
2357
2396
|
onChange: (e) => {
|
|
2358
|
-
|
|
2359
|
-
next[row.i] = Object.assign({}, next[row.i], { label: e.target.value })
|
|
2360
|
-
setSlots(next)
|
|
2397
|
+
updateSlot(row.slot.provider, row.slot.index, { label: e.target.value })
|
|
2361
2398
|
},
|
|
2362
2399
|
}),
|
|
2363
2400
|
h('span', { className: 'dsub-badge' + (account.isRevoked ? ' dsub-badge-bad' : (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : ''))) }, badgeFor(account, t)),
|
|
@@ -2431,7 +2468,7 @@ window.__ModuleLoader__.load({
|
|
|
2431
2468
|
onClick: () => {
|
|
2432
2469
|
const slot = row.slot
|
|
2433
2470
|
Promise.resolve(on ? logout(slot.provider, slot.index) : null)
|
|
2434
|
-
.then(() => setSlots(
|
|
2471
|
+
.then(() => setSlots((prev) => prev.filter((s) => !(s && s.provider === slot.provider && Number(s.index) === Number(slot.index)))))
|
|
2435
2472
|
.catch((e) => setErr(cleanErrorMessage(e.message || e)))
|
|
2436
2473
|
},
|
|
2437
2474
|
}, '\u00d7'),
|
|
@@ -2478,10 +2515,10 @@ window.__ModuleLoader__.load({
|
|
|
2478
2515
|
h('div', { style: { fontFamily: 'monospace', fontSize: '11px', margin: '4px 0', wordBreak: 'break-all' } },
|
|
2479
2516
|
headlessInfo[key].sshTunnelCommand
|
|
2480
2517
|
),
|
|
2481
|
-
headlessInfo[key].svgDataUrl ? h('div', { style: { marginTop: 6, textAlign: 'center' } },
|
|
2518
|
+
safeExternalUrl(headlessInfo[key].svgDataUrl, true) ? h('div', { style: { marginTop: 6, textAlign: 'center' } },
|
|
2482
2519
|
h('div', { className: 'dsub-sub', style: { marginBottom: 4 } }, t('headlessScanQr')),
|
|
2483
2520
|
h('img', {
|
|
2484
|
-
src: headlessInfo[key].svgDataUrl,
|
|
2521
|
+
src: safeExternalUrl(headlessInfo[key].svgDataUrl, true),
|
|
2485
2522
|
alt: 'QR Code',
|
|
2486
2523
|
style: { width: '130px', height: '130px', borderRadius: '4px', background: '#fff', padding: '4px' }
|
|
2487
2524
|
})
|
|
@@ -2537,7 +2574,7 @@ window.__ModuleLoader__.load({
|
|
|
2537
2574
|
})() : null),
|
|
2538
2575
|
(openHelp[key] ? (function renderInlineHelp(){
|
|
2539
2576
|
const prov = row.slot.provider;
|
|
2540
|
-
const info =
|
|
2577
|
+
const info = {
|
|
2541
2578
|
title: prov,
|
|
2542
2579
|
stepsZh: ['请在上方输入框中粘贴令牌或 API Key 并点击“保存令牌”。'],
|
|
2543
2580
|
stepsEn: ['Paste your token or API key into the field above and click "Save token".'],
|
|
@@ -2575,9 +2612,7 @@ window.__ModuleLoader__.load({
|
|
|
2575
2612
|
value: row.slot.proxyUrl || '',
|
|
2576
2613
|
placeholder: t('proxyPlaceholder'),
|
|
2577
2614
|
onChange: (e) => {
|
|
2578
|
-
|
|
2579
|
-
next[row.i] = Object.assign({}, next[row.i], { proxyUrl: e.target.value })
|
|
2580
|
-
setSlots(next)
|
|
2615
|
+
updateSlot(row.slot.provider, row.slot.index, { proxyUrl: e.target.value })
|
|
2581
2616
|
},
|
|
2582
2617
|
}),
|
|
2583
2618
|
h('button', {
|
|
@@ -2598,10 +2633,26 @@ window.__ModuleLoader__.load({
|
|
|
2598
2633
|
value: row.slot.maxConcurrency !== undefined ? row.slot.maxConcurrency : 1,
|
|
2599
2634
|
title: t('maxConcurrencySlotTitle'),
|
|
2600
2635
|
onChange: (e) => {
|
|
2601
|
-
const next = slots.slice()
|
|
2602
2636
|
const val = parseInt(e.target.value, 10)
|
|
2603
|
-
|
|
2604
|
-
|
|
2637
|
+
updateSlot(row.slot.provider, row.slot.index, { maxConcurrency: isNaN(val) ? 1 : Math.max(1, val) })
|
|
2638
|
+
},
|
|
2639
|
+
})
|
|
2640
|
+
),
|
|
2641
|
+
h('span', {
|
|
2642
|
+
style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'inline-flex', alignItems: 'center', gap: '3px' },
|
|
2643
|
+
title: t('monthlyCostSlotTitle')
|
|
2644
|
+
},
|
|
2645
|
+
'💰',
|
|
2646
|
+
h('input', {
|
|
2647
|
+
type: 'number',
|
|
2648
|
+
min: 0,
|
|
2649
|
+
step: 0.01,
|
|
2650
|
+
style: { width: '56px', padding: '1px 4px', fontSize: '11px', textAlign: 'center' },
|
|
2651
|
+
value: row.slot.monthlyCost !== undefined ? row.slot.monthlyCost : 20,
|
|
2652
|
+
title: t('monthlyCostSlotTitle'),
|
|
2653
|
+
onChange: (e) => {
|
|
2654
|
+
const val = parseFloat(e.target.value)
|
|
2655
|
+
updateSlot(row.slot.provider, row.slot.index, { monthlyCost: isNaN(val) ? 20 : Math.max(0, val) })
|
|
2605
2656
|
},
|
|
2606
2657
|
})
|
|
2607
2658
|
),
|
|
@@ -2627,7 +2678,7 @@ window.__ModuleLoader__.load({
|
|
|
2627
2678
|
type: 'button', className: 'dsub-mini',
|
|
2628
2679
|
onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch { /* clipboard write error */ } },
|
|
2629
2680
|
}, t('deviceCopy')),
|
|
2630
|
-
h('a', { href: d.authUrl, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
|
|
2681
|
+
h('a', { href: safeExternalUrl(d.authUrl) || undefined, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
|
|
2631
2682
|
),
|
|
2632
2683
|
h('div', { className: 'dsub-sub' }, t('deviceHint')),
|
|
2633
2684
|
) : null,
|
|
@@ -2721,9 +2772,12 @@ window.__ModuleLoader__.load({
|
|
|
2721
2772
|
}, '🔄 ' + t('reconnectNow'))
|
|
2722
2773
|
) : null,
|
|
2723
2774
|
(function(){ var r=checkRes[key]; if(!r) return null; var txt=r.ok ? ('ok ' + (r.email||'')) : ('fail ' + (r.error && r.error.message || '')); var cls=r.ok ? 'dsub-ok' : 'dsub-bad'; var q=r.quota; if(q && q.remaining!=null) txt += ' quota:'+q.remaining+(q.limit!=null?'/'+q.limit:''); return h('div', {className: cls}, txt) })(),
|
|
2724
|
-
|
|
2775
|
+
loginLinks[key] ? h('div', { className: 'dsub-verify' },
|
|
2776
|
+
h('a', { href: loginLinks[key], target: '_blank', rel: 'noopener noreferrer' }, t('verifyLink'))
|
|
2777
|
+
) : null,
|
|
2778
|
+
safeExternalUrl(account.validationUrl) ? h('div', { className: 'dsub-verify' },
|
|
2725
2779
|
t('verifyPrefix'),
|
|
2726
|
-
h('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, t('verifyLink')),
|
|
2780
|
+
h('a', { href: safeExternalUrl(account.validationUrl) || undefined, target: '_blank', rel: 'noopener noreferrer' }, t('verifyLink')),
|
|
2727
2781
|
t('verifySuffix'),
|
|
2728
2782
|
) : null,
|
|
2729
2783
|
(function usageLimitsBlock(){
|
|
@@ -3626,7 +3680,13 @@ window.__ModuleLoader__.load({
|
|
|
3626
3680
|
if (!prov || !providerMatch.test(prov)) return
|
|
3627
3681
|
fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(prov) + '&index=1', { cache: 'no-store' })
|
|
3628
3682
|
.then((r) => r.json()).then((data) => {
|
|
3629
|
-
|
|
3683
|
+
const target = data && safeExternalUrl(data.url)
|
|
3684
|
+
if (target) {
|
|
3685
|
+
const popup = window.open(target, '_blank', 'noopener')
|
|
3686
|
+
if (!popup) {
|
|
3687
|
+
try { navigator.clipboard.writeText(target) } catch { /* clipboard blocked */ }
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3630
3690
|
}).catch(() => {})
|
|
3631
3691
|
}
|
|
3632
3692
|
const line = (line) => line.trim()
|
package/lib/config-schema.js
CHANGED
|
@@ -22,6 +22,10 @@ export const Slot = z.object({
|
|
|
22
22
|
.description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
|
|
23
23
|
maxConcurrency: z.number().default(1)
|
|
24
24
|
.description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
|
|
25
|
+
monthlyCost: z.number().default(20)
|
|
26
|
+
.description('#590 Optional monthly subscription cost in USD for analytics. Defaults to 20.'),
|
|
27
|
+
maxQueueSize: z.number().default(0)
|
|
28
|
+
.description('#592 Per-account queue depth before overflow. 0 = use the global maxQueueSize.'),
|
|
25
29
|
})
|
|
26
30
|
|
|
27
31
|
export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
|
package/lib/diagnostic-wizard.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { maskEmail, maskSecrets, cleanErrorMessage } from './mask.js'
|
|
5
5
|
import { getVendor } from './vendors/index.js'
|
|
6
|
+
import { vendorFetch } from './http.js'
|
|
7
|
+
import { proxyFetch } from './proxy.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Mask sensitive credentials inside diagnostic reports.
|
|
@@ -50,6 +52,8 @@ export async function runDiagnosticWizard(options = {}) {
|
|
|
50
52
|
const includeNetwork = Boolean(options.includeNetwork)
|
|
51
53
|
const fetchImpl = options.fetchImpl || globalThis.fetch || fetch
|
|
52
54
|
const timeoutMs = options.timeoutMs || 8000
|
|
55
|
+
const vendorCallFetch = vendorFetch(fetchImpl, timeoutMs)
|
|
56
|
+
const proxyImplFactory = typeof options.proxyFetchImpl === 'function' ? options.proxyFetchImpl : proxyFetch
|
|
53
57
|
const now = Date.now()
|
|
54
58
|
|
|
55
59
|
let raw = options.rawBlob || null
|
|
@@ -140,7 +144,7 @@ export async function runDiagnosticWizard(options = {}) {
|
|
|
140
144
|
// -------------------------------------------------------------
|
|
141
145
|
// STEP 2: Network & Proxy Verification
|
|
142
146
|
// -------------------------------------------------------------
|
|
143
|
-
const proxyUrl = account.proxyUrl ||
|
|
147
|
+
const proxyUrl = account.proxyUrl || null
|
|
144
148
|
let proxyStatus = 'pass'
|
|
145
149
|
let proxyCode = 'DIRECT_CONNECTION'
|
|
146
150
|
let proxyMsg = 'Direct network connection configured'
|
|
@@ -171,16 +175,11 @@ export async function runDiagnosticWizard(options = {}) {
|
|
|
171
175
|
try {
|
|
172
176
|
const controller = new AbortController()
|
|
173
177
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const impl = proxyImplFactory(proxyUrl)
|
|
179
|
+
if (!impl) throw new Error('invalid proxy URL')
|
|
180
|
+
await impl('https://api.openai.com/v1/models', {
|
|
176
181
|
method: 'HEAD',
|
|
177
182
|
signal: controller.signal,
|
|
178
|
-
}).catch((e) => {
|
|
179
|
-
// If proxy accepts HTTP or rejects HEAD with 405/404, TCP connection succeeded
|
|
180
|
-
if (e && e.name !== 'AbortError' && e.code !== 'ECONNREFUSED' && e.code !== 'ENOTFOUND') {
|
|
181
|
-
return { ok: true }
|
|
182
|
-
}
|
|
183
|
-
throw e
|
|
184
183
|
})
|
|
185
184
|
clearTimeout(timer)
|
|
186
185
|
proxyLatencyMs = Date.now() - t0
|
|
@@ -238,18 +237,18 @@ export async function runDiagnosticWizard(options = {}) {
|
|
|
238
237
|
// Attempt safe singleflight token refresh if vendor supports it
|
|
239
238
|
let vendor = null
|
|
240
239
|
try { vendor = getVendor(provider) } catch { /* unknown provider */ }
|
|
241
|
-
if (vendor && typeof vendor.refresh === 'function' && blob.refresh_token) {
|
|
240
|
+
if (vendor && typeof vendor.refresh === 'function' && (blob.refreshToken || blob.refresh_token)) {
|
|
242
241
|
try {
|
|
243
242
|
const controller = new AbortController()
|
|
244
243
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
245
|
-
const refreshed = await vendor.refresh(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
const refreshed = await vendor.refresh(
|
|
245
|
+
options.live ? options.live() : {},
|
|
246
|
+
blob,
|
|
247
|
+
(url, init) => vendorCallFetch(url, { ...(init || {}), signal: controller.signal }),
|
|
248
|
+
)
|
|
250
249
|
clearTimeout(timer)
|
|
251
250
|
tokenLatencyMs = Date.now() - t0
|
|
252
|
-
if (refreshed && (refreshed.access_token || refreshed.token)) {
|
|
251
|
+
if (refreshed && (refreshed.accessToken || refreshed.access_token || refreshed.token)) {
|
|
253
252
|
tokenStatus = 'pass'
|
|
254
253
|
tokenCode = 'TOKEN_REFRESH_OK'
|
|
255
254
|
tokenMsg = `Token refreshed successfully (${tokenLatencyMs}ms)`
|
|
@@ -313,11 +312,11 @@ export async function runDiagnosticWizard(options = {}) {
|
|
|
313
312
|
try {
|
|
314
313
|
const controller = new AbortController()
|
|
315
314
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
316
|
-
const models = await vendor.listModels(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
const models = await vendor.listModels(
|
|
316
|
+
blob,
|
|
317
|
+
options.live ? options.live() : {},
|
|
318
|
+
(url, init) => vendorCallFetch(url, { ...(init || {}), signal: controller.signal }),
|
|
319
|
+
)
|
|
321
320
|
clearTimeout(timer)
|
|
322
321
|
catalogLatencyMs = Date.now() - t0
|
|
323
322
|
modelCount = Array.isArray(models) ? models.length : 0
|
package/lib/http.js
CHANGED
|
@@ -72,9 +72,9 @@ export function isLoopback(value) {
|
|
|
72
72
|
return false
|
|
73
73
|
}
|
|
74
74
|
const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '')
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
|
|
76
|
+
if (address.startsWith('::ffff:')) return isLoopbackIPv4(address.slice('::ffff:'.length))
|
|
77
|
+
return isLoopbackIPv4(address)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
|
|
@@ -441,3 +441,7 @@ export function escapeHtml(text) {
|
|
|
441
441
|
.replace(/</g, '<')
|
|
442
442
|
.replace(/>/g, '>')
|
|
443
443
|
}
|
|
444
|
+
|
|
445
|
+
export function vendorFetch(fetchImpl, timeoutMs = 15000) {
|
|
446
|
+
return (url, init, options) => fetchWithTimeout(fetchImpl || fetch, url, init, { timeoutMs, ...(options || {}) })
|
|
447
|
+
}
|
package/lib/routes/accounts.js
CHANGED
|
@@ -603,7 +603,13 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
603
603
|
return
|
|
604
604
|
}
|
|
605
605
|
try {
|
|
606
|
-
const
|
|
606
|
+
const provider = String(payload.provider || '')
|
|
607
|
+
const index = Number(payload.index)
|
|
608
|
+
if (!isProvider(provider) || !Number.isInteger(index) || index < 1 || index > 99) {
|
|
609
|
+
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'invalid provider or index' } })
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
const ref = oauthRef(provider, index)
|
|
607
613
|
await store.clearRef(ref)
|
|
608
614
|
await syncAdapter()
|
|
609
615
|
refreshModels().catch(() => {})
|
package/lib/routes/status.js
CHANGED
|
@@ -17,7 +17,7 @@ import { readFileSync } from 'node:fs'
|
|
|
17
17
|
import { getRecentAlerts, clearAlerts } from '../alerts.js'
|
|
18
18
|
import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
19
19
|
import { Config, plainConfig, publicConfig, restoreMaskedSecrets } from '../config-schema.js'
|
|
20
|
-
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson,
|
|
20
|
+
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson, isTimeoutError, vendorFetch } from '../http.js'
|
|
21
21
|
import { quotaSnapshot } from '../ratelimit.js'
|
|
22
22
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
23
23
|
import { getVendor } from '../vendors/index.js'
|
|
@@ -384,8 +384,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
384
384
|
const vendor = getVendor(provider)
|
|
385
385
|
let capturedQuota = null
|
|
386
386
|
const probeFetch = async (url, init = {}) => {
|
|
387
|
-
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
388
|
-
const res2 = await
|
|
387
|
+
const doFetch = vendorFetch((typeof fetchForRef === "function" && fetchForRef(ref)) || fetch, 15_000)
|
|
388
|
+
const res2 = await vendorFetch(doFetch, 15_000)(url, init)
|
|
389
389
|
try {
|
|
390
390
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
391
391
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
@@ -431,11 +431,6 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
431
431
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
432
432
|
return
|
|
433
433
|
}
|
|
434
|
-
const host = (req.headers.host || '').split(':')[0]
|
|
435
|
-
if (host !== 'localhost' && host !== '127.0.0.1') {
|
|
436
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'localhost only' } })
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
434
|
const limit = Math.max(1, Math.min(Number(queryOf(req).get('limit') || '10'), 100))
|
|
440
435
|
writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
|
|
441
436
|
}),
|
|
@@ -522,7 +517,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
522
517
|
let capturedQuota = null
|
|
523
518
|
const probeFetch = async (url, init = {}) => {
|
|
524
519
|
const doFetch = (typeof fetchForRef === "function" && fetchForRef(ref)) || fetch
|
|
525
|
-
const res2 = await
|
|
520
|
+
const res2 = await vendorFetch(doFetch, 15_000)(url, init)
|
|
526
521
|
try {
|
|
527
522
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
528
523
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
package/lib/sse.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @param {any} body
|
|
3
|
-
* @param {{ idleTimeoutMs?: number, signal?: AbortSignal }} [options]
|
|
3
|
+
* @param {{ idleTimeoutMs?: number, totalTimeoutMs?: number, maxBufferBytes?: number, signal?: AbortSignal }} [options]
|
|
4
4
|
* @returns {AsyncGenerator<string, void, unknown>}
|
|
5
5
|
*/
|
|
6
|
-
export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {}) {
|
|
6
|
+
export async function* iterateSse(body, { idleTimeoutMs = 60000, totalTimeoutMs = 600000, maxBufferBytes = 8 * 1024 * 1024, signal } = {}) {
|
|
7
7
|
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
8
8
|
const decoder = new TextDecoder()
|
|
9
9
|
let buffer = ''
|
|
10
10
|
let terminated = false
|
|
11
|
+
const deadline = totalTimeoutMs > 0 ? Date.now() + totalTimeoutMs : 0
|
|
11
12
|
|
|
12
13
|
async function* fromText(chunk) {
|
|
13
14
|
if (terminated) return
|
|
15
|
+
if (chunk.length > maxBufferBytes) {
|
|
16
|
+
throw new Error('SSE buffer limit exceeded (' + maxBufferBytes + ' chars)')
|
|
17
|
+
}
|
|
14
18
|
buffer += chunk
|
|
19
|
+
if (buffer.length > maxBufferBytes) {
|
|
20
|
+
throw new Error('SSE buffer limit exceeded (' + maxBufferBytes + ' chars)')
|
|
21
|
+
}
|
|
15
22
|
let sep
|
|
16
23
|
while ((sep = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
17
24
|
const raw = buffer.slice(0, sep)
|
|
@@ -32,7 +39,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
async function readWithTimeout(r) {
|
|
42
|
+
async function readWithTimeout(r, timeoutMs) {
|
|
36
43
|
if (signal && signal.aborted) {
|
|
37
44
|
if (typeof r.cancel === 'function') await r.cancel().catch(() => {})
|
|
38
45
|
const err = new Error('stream aborted by client')
|
|
@@ -41,12 +48,15 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
41
48
|
}
|
|
42
49
|
let timer
|
|
43
50
|
const timeoutPromise = new Promise((_, reject) => {
|
|
44
|
-
if (!
|
|
51
|
+
if (!timeoutMs || timeoutMs <= 0) return
|
|
45
52
|
timer = setTimeout(() => {
|
|
46
|
-
const
|
|
53
|
+
const message = timeoutMs === idleTimeoutMs
|
|
54
|
+
? `stream idle timeout: no data received for ${Math.round(timeoutMs / 1000)}s`
|
|
55
|
+
: 'SSE stream deadline exceeded'
|
|
56
|
+
const err = /** @type {Error & { code?: string }} */ (new Error(message))
|
|
47
57
|
err.code = 'TIMEOUT'
|
|
48
58
|
reject(err)
|
|
49
|
-
},
|
|
59
|
+
}, timeoutMs)
|
|
50
60
|
})
|
|
51
61
|
|
|
52
62
|
let abortPromise
|
|
@@ -77,7 +87,13 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
77
87
|
if (reader) {
|
|
78
88
|
try {
|
|
79
89
|
while (!terminated) {
|
|
80
|
-
|
|
90
|
+
let wait = idleTimeoutMs
|
|
91
|
+
if (deadline) {
|
|
92
|
+
const remaining = deadline - Date.now()
|
|
93
|
+
if (remaining <= 0) throw new Error('SSE stream deadline exceeded')
|
|
94
|
+
wait = idleTimeoutMs > 0 ? Math.min(idleTimeoutMs, remaining) : remaining
|
|
95
|
+
}
|
|
96
|
+
const { done, value } = await readWithTimeout(reader, wait)
|
|
81
97
|
if (done) break
|
|
82
98
|
yield* fromText(decoder.decode(value, { stream: true }))
|
|
83
99
|
if (terminated) break
|
|
@@ -92,6 +108,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
92
108
|
}
|
|
93
109
|
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
94
110
|
for await (const chunk of body) {
|
|
111
|
+
if (deadline && Date.now() >= deadline) throw new Error('SSE stream deadline exceeded')
|
|
95
112
|
if (signal && signal.aborted) {
|
|
96
113
|
const err = new Error('stream aborted by client')
|
|
97
114
|
err.name = 'AbortError'
|
|
@@ -108,6 +125,7 @@ export async function* iterateSse(body, { idleTimeoutMs = 60000, signal } = {})
|
|
|
108
125
|
return
|
|
109
126
|
}
|
|
110
127
|
const text = typeof body === 'string' ? body : await new Response(body).text()
|
|
128
|
+
if (text.length > maxBufferBytes) throw new Error('SSE buffer limit exceeded (' + maxBufferBytes + ' chars)')
|
|
111
129
|
yield* fromText(text)
|
|
112
130
|
}
|
|
113
131
|
|
package/lib/subscriptions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readErrorBody } from './wire.js'
|
|
1
2
|
import { isProvider } from "./refs.js"
|
|
2
3
|
import { pickAccountWithTrace, markCooldown, isSwitchableError, modelFamily } from "./rotate.js"
|
|
3
4
|
import { quotaSnapshot } from "./ratelimit.js"
|
|
@@ -262,7 +263,7 @@ export function createSubscriptionsService(deps) {
|
|
|
262
263
|
} catch { /* ignore subscriptions error */ }
|
|
263
264
|
}
|
|
264
265
|
if (!res.ok) {
|
|
265
|
-
const txt = await res
|
|
266
|
+
const txt = await readErrorBody(res)
|
|
266
267
|
const err = new Error("vendor http " + res.status + (txt ? ": " + txt.slice(0, 200) : ""))
|
|
267
268
|
err.status = res.status
|
|
268
269
|
if (res.status === 429) err.code = "RATE_LIMIT"
|
package/lib/updater.js
CHANGED
|
@@ -16,11 +16,19 @@ function header(request, name) {
|
|
|
16
16
|
return Array.isArray(value) ? value[0] : value
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function isLoopbackIPv4(address) {
|
|
20
|
+
if (!address || !address.startsWith('127.')) return false
|
|
21
|
+
const parts = address.split('.')
|
|
22
|
+
if (parts.length !== 4) return false
|
|
23
|
+
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export function isLoopback(value) {
|
|
20
27
|
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
if (!address) return false
|
|
29
|
+
if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
|
|
30
|
+
const ipv4 = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address
|
|
31
|
+
return isLoopbackIPv4(ipv4)
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function isTrustedUpdateRequest(request, targetCtx) {
|
package/lib/vault.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
// #353 & #360: Encrypted Vault Export/Import for Subscriptions
|
|
2
2
|
// Secure migration and backup of slots and credentials using AES-256-GCM.
|
|
3
3
|
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
4
5
|
import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
|
|
5
6
|
import { isProvider, oauthRef } from './refs.js'
|
|
6
7
|
|
|
7
8
|
export const VAULT_FORMAT = 'dsh-subscriptions-vault-v1'
|
|
8
9
|
|
|
10
|
+
let cachedVersion = ''
|
|
11
|
+
function pluginVersion() {
|
|
12
|
+
if (!cachedVersion) {
|
|
13
|
+
try {
|
|
14
|
+
cachedVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '0.0.0'
|
|
15
|
+
} catch {
|
|
16
|
+
cachedVersion = '0.0.0'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return cachedVersion
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertSupportedVaultVersion(version) {
|
|
23
|
+
const text = String(version == null ? '' : version).trim()
|
|
24
|
+
if (!text) return
|
|
25
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(text)
|
|
26
|
+
if (!match) throw new Error('invalid vault version')
|
|
27
|
+
if (Number(match[1]) > 0) throw new Error('vault version ' + text + ' is newer than this plugin supports')
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
/**
|
|
10
31
|
* Encrypts slots configuration and credential blobs into a portable vault string.
|
|
11
32
|
*/
|
|
@@ -17,7 +38,7 @@ export function exportVault({ slots = [], blobs = {}, exportedBy = 'dsh-subscrip
|
|
|
17
38
|
|
|
18
39
|
const payload = {
|
|
19
40
|
format: VAULT_FORMAT,
|
|
20
|
-
version:
|
|
41
|
+
version: pluginVersion(),
|
|
21
42
|
exportedAt: new Date().toISOString(),
|
|
22
43
|
exportedBy,
|
|
23
44
|
slots: Array.isArray(slots) ? slots : [],
|
|
@@ -66,6 +87,8 @@ export function importVault(encryptedPayload, passphrase) {
|
|
|
66
87
|
throw new Error('invalid vault schema: unsupported structure')
|
|
67
88
|
}
|
|
68
89
|
|
|
90
|
+
assertSupportedVaultVersion(data.version)
|
|
91
|
+
|
|
69
92
|
const refs = new Set()
|
|
70
93
|
for (const slot of data.slots) {
|
|
71
94
|
if (!slot || !isProvider(slot.provider) || !Number.isSafeInteger(slot.index) || slot.index < 1) {
|
package/lib/vendor-factory.js
CHANGED
|
@@ -10,7 +10,7 @@ import { fetchWithTimeout } from "./http.js"
|
|
|
10
10
|
|
|
11
11
|
import { buildAuthorizeUrl } from "./oauth.js"
|
|
12
12
|
import { codexResponsesBody, modelCatalog } from "./messages.js"
|
|
13
|
-
import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from "./wire.js"
|
|
13
|
+
import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError , readErrorBody } from "./wire.js"
|
|
14
14
|
import { emailFromToken } from "./jwt.js"
|
|
15
15
|
|
|
16
16
|
function defaultModels(providerId, models) {
|
|
@@ -92,6 +92,7 @@ export function createVendorFromProfile(profile) {
|
|
|
92
92
|
const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
|
|
93
93
|
headers: identityHeaders(blob, { Accept: "application/json" }),
|
|
94
94
|
}, { timeoutMs: 15000 })
|
|
95
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
95
96
|
const json = await readJson(res)
|
|
96
97
|
const rows = []
|
|
97
98
|
for (const entry of json.data || json.models || []) {
|
|
@@ -101,7 +102,9 @@ export function createVendorFromProfile(profile) {
|
|
|
101
102
|
}
|
|
102
103
|
if (!rows.length) throw new Error("empty catalog")
|
|
103
104
|
return modelCatalog(id, rows)
|
|
104
|
-
} catch {
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const status = Number(err && (err.status || err.statusCode) || 0)
|
|
107
|
+
if (status === 401 || status === 403 || (err && err.code === 'AUTH')) throw err
|
|
105
108
|
return catalog
|
|
106
109
|
}
|
|
107
110
|
},
|
|
@@ -125,7 +128,7 @@ export function createVendorFromProfile(profile) {
|
|
|
125
128
|
body: JSON.stringify(body),
|
|
126
129
|
signal,
|
|
127
130
|
}, { timeoutMs: 60000, stream: true })
|
|
128
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
131
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
129
132
|
yield* codexResponsesStream(res.body)
|
|
130
133
|
},
|
|
131
134
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
3
|
import { googleContents } from '../messages.js'
|
|
4
|
-
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
|
|
4
|
+
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth , readErrorBody } from '../wire.js'
|
|
5
5
|
import { fetchWithTimeout } from '../http.js'
|
|
6
6
|
import { emailFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
7
7
|
import {
|
|
@@ -239,7 +239,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, signal, s
|
|
|
239
239
|
body: JSON.stringify(envelope),
|
|
240
240
|
signal,
|
|
241
241
|
}, { timeoutMs: RESPONSE_TIMEOUT_MS })
|
|
242
|
-
if (!res.ok) throwHttpError(res.status, await res
|
|
242
|
+
if (!res.ok) throwHttpError(res.status, await readErrorBody(res))
|
|
243
243
|
if (saveBlob && sessionId && sessionId !== ready.sessionId) {
|
|
244
244
|
await saveBlob({ ...ready, sessionId })
|
|
245
245
|
}
|
package/lib/vendors/claude.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
import { fetchWithTimeout } from '../http.js'
|
|
2
3
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
4
|
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
4
|
-
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
5
|
+
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson , readErrorBody } from '../wire.js'
|
|
5
6
|
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
6
7
|
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
7
8
|
|
|
@@ -253,7 +254,7 @@ function applyClaudeThinking(payload, options) {
|
|
|
253
254
|
export async function check(blob, _cfg, fetchImpl) {
|
|
254
255
|
const impl = fetchImpl || fetch
|
|
255
256
|
const res = await impl(PROFILE, { headers: oauthHeaders(blob.accessToken) })
|
|
256
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
257
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
257
258
|
const json = await readJson(res)
|
|
258
259
|
return { ok: true, raw: json }
|
|
259
260
|
}
|
|
@@ -273,7 +274,7 @@ export async function usage(blob, _cfg, fetchImpl) {
|
|
|
273
274
|
const billingAccountState = new Map()
|
|
274
275
|
const MAX_BILLING_ACCOUNTS = 256
|
|
275
276
|
|
|
276
|
-
function resolveAccountKey(cfg, context) {
|
|
277
|
+
export function resolveAccountKey(cfg, context) {
|
|
277
278
|
const explicit = (context && context.accountId) || (cfg && cfg.accountId)
|
|
278
279
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim()
|
|
279
280
|
const blob = (context && context.blob) || (cfg && cfg.blob)
|
|
@@ -281,7 +282,7 @@ function resolveAccountKey(cfg, context) {
|
|
|
281
282
|
const id = blob.accountId || blob.id || blob.email
|
|
282
283
|
if (typeof id === 'string' && id.trim()) return id.trim()
|
|
283
284
|
if (typeof blob.accessToken === 'string' && blob.accessToken.length >= 8) {
|
|
284
|
-
return
|
|
285
|
+
return 'token:' + createHash('sha256').update(String(blob.accessToken)).digest('hex').slice(0, 16)
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
return 'default'
|
|
@@ -357,6 +358,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
357
358
|
body: JSON.stringify(payload),
|
|
358
359
|
signal,
|
|
359
360
|
})
|
|
360
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
361
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
361
362
|
yield* anthropicStream(res.body)
|
|
362
363
|
}
|
package/lib/vendors/codex.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID, createHash } from 'node:crypto'
|
|
|
3
3
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
4
4
|
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
5
5
|
import { chatgptAccountId, emailFromToken, planFromToken, avatarFromToken, scopesFromToken, organizationsFromToken } from '../jwt.js'
|
|
6
|
-
import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
|
|
6
|
+
import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError , readErrorBody } from '../wire.js'
|
|
7
7
|
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
8
8
|
|
|
9
9
|
export const id = 'codex'
|
|
@@ -129,7 +129,7 @@ export async function deviceStart(cfg, fetchImpl) {
|
|
|
129
129
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
130
130
|
body: JSON.stringify({ client_id: cfg.clientId }),
|
|
131
131
|
})
|
|
132
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
132
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res), 'DEVICE')
|
|
133
133
|
const json = await readJson(res)
|
|
134
134
|
if (!json || typeof json.user_code !== 'string' || typeof json.device_auth_id !== 'string') {
|
|
135
135
|
throw httpError(502, 'device auth response is incomplete', 'DEVICE')
|
|
@@ -151,7 +151,7 @@ export async function devicePoll(cfg, session, fetchImpl) {
|
|
|
151
151
|
})
|
|
152
152
|
// 403/404 = not confirmed yet (matches reference implementations)
|
|
153
153
|
if (res.status === 403 || res.status === 404) return { status: 'pending' }
|
|
154
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
154
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res), 'DEVICE')
|
|
155
155
|
const json = await readJson(res)
|
|
156
156
|
if (!json || typeof json.authorization_code !== 'string' || typeof json.code_verifier !== 'string') {
|
|
157
157
|
throw httpError(502, 'device auth token response is incomplete', 'DEVICE')
|
|
@@ -218,7 +218,7 @@ export async function check(blob, cfg, fetchImpl) {
|
|
|
218
218
|
const res = await fetchWithTimeout(impl, base + "/models?client_version=" + encodeURIComponent(version), {
|
|
219
219
|
headers: identityHeaders(blob, cfg, { Accept: "application/json" }),
|
|
220
220
|
}, { timeoutMs: 15000 })
|
|
221
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
221
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
222
222
|
const json = await readJson(res)
|
|
223
223
|
// quota headers may contain limits
|
|
224
224
|
return { ok: true, raw: json }
|
|
@@ -272,7 +272,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
272
272
|
body: JSON.stringify(body),
|
|
273
273
|
signal,
|
|
274
274
|
})
|
|
275
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
275
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
276
276
|
yield* codexResponsesStream(res.body)
|
|
277
277
|
}
|
|
278
278
|
export function deriveSessionId(options) {
|
package/lib/vendors/cody.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
|
|
5
5
|
export const id = 'cody'
|
|
6
6
|
const CODY_API_BASE = 'https://sourcegraph.com/.api'
|
|
@@ -75,6 +75,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
75
75
|
signal
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
78
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
79
79
|
yield* openaiChatStream(res.body)
|
|
80
80
|
}
|
|
@@ -11,6 +11,12 @@ import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
|
11
11
|
const MAX_STDERR_CHARS = 2048
|
|
12
12
|
const GITHUB_TOKEN_PATTERN = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g
|
|
13
13
|
|
|
14
|
+
const COPILOT_CLI_BASENAME = /^(?:copilot|github-copilot-cli|copilot-cli)(?:\.(?:cmd|bat|exe))?$/i
|
|
15
|
+
|
|
16
|
+
export function isAllowedCopilotBinary(binaryPath) {
|
|
17
|
+
return COPILOT_CLI_BASENAME.test(String(binaryPath || '').split(/[\\/]/).pop())
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function redactCopilotStderr(text) {
|
|
15
21
|
return String(text || '').replace(GITHUB_TOKEN_PATTERN, '[REDACTED]')
|
|
16
22
|
}
|
|
@@ -134,9 +140,14 @@ export async function readCopilotCredentials(home, env = process.env) {
|
|
|
134
140
|
export function findCopilotCli(customPath, env = process.env, platform = process.platform) {
|
|
135
141
|
if (customPath && typeof customPath === 'string' && customPath.trim()) {
|
|
136
142
|
const p = customPath.trim()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
let exists = false
|
|
144
|
+
try { exists = existsSync(p) } catch { /* best-effort */ }
|
|
145
|
+
if (exists) {
|
|
146
|
+
if (!isAllowedCopilotBinary(p)) {
|
|
147
|
+
throw new Error('copilotCliPath must point to a copilot binary (copilot, github-copilot-cli or copilot-cli)')
|
|
148
|
+
}
|
|
149
|
+
return p
|
|
150
|
+
}
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
const pathVar = env.PATH || ''
|
|
@@ -265,6 +276,9 @@ export function resolveSpawnCommand(binaryPath, args, platform = process.platfor
|
|
|
265
276
|
* @returns {AsyncGenerator<any, void, unknown>}
|
|
266
277
|
*/
|
|
267
278
|
export async function* streamCopilotCli({ binaryPath, options, blob, config: _config, signal, spawnImpl }) {
|
|
279
|
+
if (!isAllowedCopilotBinary(binaryPath)) {
|
|
280
|
+
throw new Error('copilotCliPath must point to a copilot binary (copilot, github-copilot-cli or copilot-cli)')
|
|
281
|
+
}
|
|
268
282
|
if (signal && signal.aborted) {
|
|
269
283
|
throw new LlmError('Request aborted by client', 'ABORTED')
|
|
270
284
|
}
|
|
@@ -283,9 +297,17 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
283
297
|
throw new LlmError(`Failed to spawn Copilot CLI: ${err && err.message ? err.message : err}`, 'INTERNAL_ERROR')
|
|
284
298
|
}
|
|
285
299
|
|
|
300
|
+
let abortHandler = null
|
|
301
|
+
let killTimer = null
|
|
286
302
|
if (signal) {
|
|
287
|
-
|
|
303
|
+
abortHandler = () => {
|
|
288
304
|
try { child.kill('SIGTERM') } catch { /* best-effort */ }
|
|
305
|
+
killTimer = setTimeout(() => {
|
|
306
|
+
if (!processEnded && !child.killed) {
|
|
307
|
+
try { child.kill('SIGKILL') } catch { /* best-effort */ }
|
|
308
|
+
}
|
|
309
|
+
}, 2000)
|
|
310
|
+
if (killTimer && typeof killTimer.unref === 'function') killTimer.unref()
|
|
289
311
|
}
|
|
290
312
|
signal.addEventListener('abort', abortHandler, { once: true })
|
|
291
313
|
}
|
|
@@ -359,6 +381,7 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
359
381
|
}
|
|
360
382
|
})
|
|
361
383
|
|
|
384
|
+
try {
|
|
362
385
|
while (true) {
|
|
363
386
|
if (chunkQueue.length) {
|
|
364
387
|
const line = chunkQueue.shift()
|
|
@@ -423,6 +446,10 @@ export async function* streamCopilotCli({ binaryPath, options, blob, config: _co
|
|
|
423
446
|
if (processError && !chunkQueue.length) throw processError
|
|
424
447
|
}
|
|
425
448
|
}
|
|
449
|
+
} finally {
|
|
450
|
+
if (signal && abortHandler) signal.removeEventListener('abort', abortHandler)
|
|
451
|
+
if (killTimer) clearTimeout(killTimer)
|
|
452
|
+
}
|
|
426
453
|
|
|
427
454
|
yield {
|
|
428
455
|
type: 'finish',
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LlmError } from "@deepseek-ai/dsh-llm"
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
|
|
3
3
|
import { fetchWithTimeout } from "../http.js"
|
|
4
|
-
import { openaiChatStream, readJson, httpError } from "../wire.js"
|
|
4
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from "../wire.js"
|
|
5
5
|
import { findCopilotCli, streamCopilotCli, isCopilotCliAvailable, COPILOT_CLI_BINARIES } from "./copilot-cli.js"
|
|
6
6
|
|
|
7
7
|
export const id = "copilot"
|
|
@@ -148,7 +148,7 @@ export async function requestDeviceCode(fetchImpl) {
|
|
|
148
148
|
scope: "read:user"
|
|
149
149
|
})
|
|
150
150
|
})
|
|
151
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
151
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
152
152
|
return readJson(res)
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -166,7 +166,7 @@ export async function pollDeviceToken(deviceCode, fetchImpl) {
|
|
|
166
166
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
169
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
170
170
|
return readJson(res)
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -259,7 +259,7 @@ async function exchangeCopilotToken(githubToken, fetchImpl) {
|
|
|
259
259
|
"User-Agent": "GitHubCopilotChat/0.24.0"
|
|
260
260
|
}
|
|
261
261
|
})
|
|
262
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
262
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
263
263
|
return readJson(res)
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -354,6 +354,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
354
354
|
signal
|
|
355
355
|
})
|
|
356
356
|
|
|
357
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
357
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
358
358
|
yield* openaiChatStream(res.body)
|
|
359
359
|
}
|
package/lib/vendors/cursor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, readJson, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
import { asUsageSnapshot } from '../usage.js'
|
|
5
5
|
|
|
6
6
|
export const id = 'cursor'
|
|
@@ -201,6 +201,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
201
201
|
signal,
|
|
202
202
|
})
|
|
203
203
|
|
|
204
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
204
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
205
205
|
yield* openaiChatStream(res.body)
|
|
206
206
|
}
|
package/lib/vendors/ernie.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, readJson, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
|
|
5
5
|
export const id = 'ernie'
|
|
6
6
|
export const ERNIE_OAUTH_URL = 'https://aip.baidubce.com/oauth/2.0/token'
|
|
7
7
|
const ERNIE_BASE_URL = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat'
|
|
8
8
|
|
|
9
|
+
export function ernieChatUrl(token, endpoint, baseUrl = ERNIE_BASE_URL) {
|
|
10
|
+
// #636: the v1 Qianfan endpoint requires access_token in the query. Always
|
|
11
|
+
// encode it so & and # cannot truncate or inject query parameters.
|
|
12
|
+
return baseUrl + '/' + endpoint + '?access_token=' + encodeURIComponent(token)
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
const ERNIE_MODELS = [
|
|
10
16
|
{
|
|
11
17
|
id: 'ernie-speed-128k',
|
|
@@ -53,7 +59,7 @@ async function refreshErnieToken(apiKey, secretKey, fetchImpl) {
|
|
|
53
59
|
client_secret: secretKey,
|
|
54
60
|
}),
|
|
55
61
|
})
|
|
56
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
62
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
57
63
|
return readJson(res)
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -74,7 +80,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
74
80
|
if (!token) throw new LlmError('Baidu ERNIE requires access token or API Key + Secret Key', 'AUTH')
|
|
75
81
|
|
|
76
82
|
const endpoint = options.model === 'ernie-4.0-turbo-8k' ? 'completions_pro' : 'ernie_speed'
|
|
77
|
-
const url =
|
|
83
|
+
const url = ernieChatUrl(token, endpoint, (config && config.apiBase) || ERNIE_BASE_URL)
|
|
78
84
|
|
|
79
85
|
const messages = openaiMessages(options).map((m) => ({
|
|
80
86
|
role: m.role === 'system' ? 'user' : m.role,
|
|
@@ -91,6 +97,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
91
97
|
signal
|
|
92
98
|
})
|
|
93
99
|
|
|
94
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
100
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
95
101
|
yield* openaiChatStream(res.body)
|
|
96
102
|
}
|
package/lib/vendors/glm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, readJson, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
import { asUsageSnapshot } from '../usage.js'
|
|
5
5
|
|
|
6
6
|
export const id = 'glm'
|
|
@@ -109,6 +109,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
109
109
|
signal,
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
112
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
113
113
|
yield* openaiChatStream(res.body)
|
|
114
114
|
}
|
package/lib/vendors/grok.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
3
3
|
import { modelCatalog, codexResponsesBody } from '../messages.js'
|
|
4
|
-
import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
4
|
+
import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson , readErrorBody } from '../wire.js'
|
|
5
5
|
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
6
6
|
import { asUsageSnapshot, usageWindows, grokBillingPercent } from '../usage.js'
|
|
7
7
|
|
|
@@ -250,7 +250,7 @@ export async function listModels(blob, cfg, fetchImpl) {
|
|
|
250
250
|
export async function check(blob, cfg, fetchImpl) {
|
|
251
251
|
const impl = fetchImpl || fetch
|
|
252
252
|
const res = await impl(MODELS, { headers: { Authorization: "Bearer " + blob.accessToken, Accept: "application/json" } })
|
|
253
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
253
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
254
254
|
const json = await readJson(res)
|
|
255
255
|
return { ok: true, raw: json }
|
|
256
256
|
}
|
|
@@ -308,7 +308,7 @@ export async function deviceStart(cfg, fetchImpl) {
|
|
|
308
308
|
},
|
|
309
309
|
body: params.toString(),
|
|
310
310
|
})
|
|
311
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
311
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res), 'DEVICE')
|
|
312
312
|
const json = await readJson(res)
|
|
313
313
|
if (!json || typeof json.user_code !== 'string' || typeof json.device_code !== 'string') {
|
|
314
314
|
throw httpError(502, 'xAI device authorization response is incomplete', 'DEVICE')
|
|
@@ -338,7 +338,7 @@ export async function devicePoll(cfg, session, fetchImpl) {
|
|
|
338
338
|
body: params.toString(),
|
|
339
339
|
})
|
|
340
340
|
if (res.status === 400 || res.status === 403) {
|
|
341
|
-
const rawText = await res
|
|
341
|
+
const rawText = await readErrorBody(res)
|
|
342
342
|
let errJson = {}
|
|
343
343
|
try {
|
|
344
344
|
errJson = JSON.parse(rawText)
|
|
@@ -356,7 +356,7 @@ export async function devicePoll(cfg, session, fetchImpl) {
|
|
|
356
356
|
}
|
|
357
357
|
throw httpError(res.status, rawText, 'DEVICE')
|
|
358
358
|
}
|
|
359
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
359
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res), 'DEVICE')
|
|
360
360
|
const json = await readJson(res)
|
|
361
361
|
if (!json || typeof json.access_token !== 'string') {
|
|
362
362
|
throw httpError(502, 'xAI device token response is incomplete', 'DEVICE')
|
|
@@ -391,7 +391,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
391
391
|
body: JSON.stringify(body),
|
|
392
392
|
signal,
|
|
393
393
|
})
|
|
394
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
394
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
395
395
|
yield* codexResponsesStream(res.body)
|
|
396
396
|
}
|
|
397
397
|
|
package/lib/vendors/jetbrains.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
5
5
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
6
6
|
import { openaiMessages, modelCatalog } from '../messages.js'
|
|
7
|
-
import { jsonTokenRequest, tokenBlobFromOAuth, openaiChatStream, httpError, readJson } from '../wire.js'
|
|
7
|
+
import { jsonTokenRequest, tokenBlobFromOAuth, openaiChatStream, httpError, readJson , readErrorBody } from '../wire.js'
|
|
8
8
|
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
9
9
|
|
|
10
10
|
export const id = 'jetbrains'
|
|
@@ -193,6 +193,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
193
193
|
signal,
|
|
194
194
|
})
|
|
195
195
|
|
|
196
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
196
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
197
197
|
yield* openaiChatStream(res.body)
|
|
198
198
|
}
|
package/lib/vendors/kimi.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, readJson, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
import { asUsageSnapshot } from '../usage.js'
|
|
5
5
|
|
|
6
6
|
export const id = 'kimi'
|
|
@@ -116,6 +116,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
116
116
|
signal,
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
119
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
120
120
|
yield* openaiChatStream(res.body)
|
|
121
121
|
}
|
package/lib/vendors/kiro.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, readJson, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, readJson, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
import { asUsageSnapshot } from '../usage.js'
|
|
5
5
|
|
|
6
6
|
export const id = 'kiro'
|
|
@@ -112,6 +112,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
112
112
|
signal,
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
115
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
116
116
|
yield* openaiChatStream(res.body)
|
|
117
117
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
5
5
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
6
6
|
import { openaiMessages, modelCatalog } from '../messages.js'
|
|
7
|
-
import { jsonTokenRequest, tokenBlobFromOAuth, openaiChatStream, httpError, readJson } from '../wire.js'
|
|
7
|
+
import { jsonTokenRequest, tokenBlobFromOAuth, openaiChatStream, httpError, readJson , readErrorBody } from '../wire.js'
|
|
8
8
|
import { emailFromToken, planFromToken, avatarFromToken, scopesFromToken } from '../jwt.js'
|
|
9
9
|
|
|
10
10
|
export const id = 'perplexity'
|
|
@@ -205,6 +205,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
205
205
|
signal,
|
|
206
206
|
})
|
|
207
207
|
|
|
208
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
208
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
209
209
|
yield* openaiChatStream(res.body)
|
|
210
210
|
}
|
package/lib/vendors/qwen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages, openaiTools } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
|
|
5
5
|
export const id = 'qwen'
|
|
6
6
|
const QWEN_API_BASE = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
|
@@ -81,6 +81,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
81
81
|
signal
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
84
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
85
85
|
yield* openaiChatStream(res.body)
|
|
86
86
|
}
|
package/lib/vendors/replit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
|
|
5
5
|
export const id = 'replit'
|
|
6
6
|
const REPLIT_API_BASE = 'https://replit.com/api/v1/ai'
|
|
@@ -64,6 +64,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
64
64
|
signal
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
67
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
68
68
|
yield* openaiChatStream(res.body)
|
|
69
69
|
}
|
package/lib/vendors/spark.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
2
2
|
import { openaiMessages } from '../messages.js'
|
|
3
|
-
import { openaiChatStream, httpError } from '../wire.js'
|
|
3
|
+
import { openaiChatStream, httpError , readErrorBody } from '../wire.js'
|
|
4
4
|
|
|
5
5
|
export const id = 'spark'
|
|
6
6
|
const SPARK_API_BASE = 'https://spark-api-open.xf-yun.com/v1'
|
|
@@ -68,6 +68,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
68
68
|
signal
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
if (!res.ok) throw httpError(res.status, await res
|
|
71
|
+
if (!res.ok) throw httpError(res.status, await readErrorBody(res))
|
|
72
72
|
yield* openaiChatStream(res.body)
|
|
73
73
|
}
|
package/lib/wire.js
CHANGED
|
@@ -432,4 +432,30 @@ export async function jsonTokenRequest(url, body, fetchImpl, headers, { timeoutM
|
|
|
432
432
|
body: JSON.stringify(body),
|
|
433
433
|
}, { timeoutMs })
|
|
434
434
|
return readJson(res)
|
|
435
|
-
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function readErrorBody(res, maxChars = 4096) {
|
|
438
|
+
if (!res || typeof res !== 'object') return ''
|
|
439
|
+
try {
|
|
440
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
441
|
+
const text = await res.text()
|
|
442
|
+
return String(text || '').slice(0, maxChars)
|
|
443
|
+
}
|
|
444
|
+
const reader = res.body.getReader()
|
|
445
|
+
const decoder = new TextDecoder()
|
|
446
|
+
let out = ''
|
|
447
|
+
try {
|
|
448
|
+
while (out.length < maxChars) {
|
|
449
|
+
const { done, value } = await reader.read()
|
|
450
|
+
if (done) break
|
|
451
|
+
out += decoder.decode(value, { stream: true })
|
|
452
|
+
}
|
|
453
|
+
out += decoder.decode()
|
|
454
|
+
} finally {
|
|
455
|
+
if (typeof reader.cancel === 'function') await reader.cancel().catch(() => {})
|
|
456
|
+
}
|
|
457
|
+
return out.slice(0, maxChars)
|
|
458
|
+
} catch {
|
|
459
|
+
return ''
|
|
460
|
+
}
|
|
461
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.85",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|