@goodandready/dsh-subscriptions 0.6.3 → 0.6.5
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/accounts.js +7 -1
- package/lib/blob.js +0 -8
- package/lib/client.js +48 -12
- package/lib/http.js +5 -2
- package/lib/import-auth.js +20 -6
- package/lib/index.js +6 -1
- package/lib/quarantine.js +0 -7
- package/lib/ratelimit-parser.js +0 -24
- package/lib/ratelimit.js +0 -8
- package/lib/refs.js +5 -0
- package/lib/relative-time.js +6 -0
- package/lib/routes/accounts.js +0 -1
- package/lib/routes/oauth.js +0 -1
- package/lib/routes/status.js +0 -1
- package/lib/routes.js +1 -9
- package/lib/session-pin.js +11 -0
- package/lib/vendors/antigravity.js +3 -1
- package/lib/vendors/claude-cli.js +6 -2
- package/lib/vendors/cody.js +12 -1
- package/lib/vendors/grok.js +2 -1
- package/package.json +6 -3
package/lib/accounts.js
CHANGED
|
@@ -12,6 +12,8 @@ export function normalizeSlots(slots) {
|
|
|
12
12
|
const out = []
|
|
13
13
|
const seen = new Set()
|
|
14
14
|
for (const slot of Array.isArray(slots) ? slots : []) {
|
|
15
|
+
// User-editable config: a null or non-object entry must be skipped, not crash the plugin.
|
|
16
|
+
if (!slot || typeof slot !== 'object') continue
|
|
15
17
|
if (!isProvider(slot.provider)) continue
|
|
16
18
|
const index = Number(slot.index)
|
|
17
19
|
if (!Number.isInteger(index) || index < 1) continue
|
|
@@ -105,10 +107,14 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
105
107
|
quarantines.delete(ref)
|
|
106
108
|
writeQueues.delete(ref)
|
|
107
109
|
for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
|
|
108
|
-
for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
|
|
109
110
|
refreshFailures.delete(ref)
|
|
110
111
|
usage.delete(ref)
|
|
111
112
|
usageFetched.delete(ref)
|
|
113
|
+
// Counters and health must not outlive the account, otherwise a
|
|
114
|
+
// re-added slot inherits stale rotation statistics.
|
|
115
|
+
requestCounts.delete(ref)
|
|
116
|
+
health.delete(ref)
|
|
117
|
+
windows.delete(ref)
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
async function describeRef(ref) {
|
package/lib/blob.js
CHANGED
package/lib/client.js
CHANGED
|
@@ -54,6 +54,10 @@ window.__ModuleLoader__.load({
|
|
|
54
54
|
'.dsub-chevOpen{transform:rotate(180deg)}' +
|
|
55
55
|
'.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
|
|
56
56
|
'.dsub-block{margin-top:10px}' +
|
|
57
|
+
'.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}' +
|
|
58
|
+
'.dsub-group[open]{padding-bottom:6px}' +
|
|
59
|
+
'.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}' +
|
|
60
|
+
'.dsub-groupHead::-webkit-details-marker{display:none}' +
|
|
57
61
|
'.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
|
|
58
62
|
'.dsub-grow{flex:1 1 180px;min-width:140px}' +
|
|
59
63
|
'.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
|
|
@@ -310,6 +314,12 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
310
314
|
'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
|
|
311
315
|
'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
|
|
312
316
|
'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
|
|
317
|
+
'closeModal': 'Close',
|
|
318
|
+
'groupAuth': 'Sign-in and redirect',
|
|
319
|
+
'groupPrivacy': 'Privacy and model list',
|
|
320
|
+
'groupRotation': 'Rotation and limits',
|
|
321
|
+
'groupProvider': 'Provider behaviour',
|
|
322
|
+
'groupFallback': 'Local fallback',
|
|
313
323
|
'privacyMask': 'Mask emails and account identifiers',
|
|
314
324
|
'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
|
|
315
325
|
'autoLoopback': 'Auto Loopback',
|
|
@@ -539,6 +549,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
539
549
|
function badgeFor(account, t) {
|
|
540
550
|
if (!account || !account.configured) return t('notConnected')
|
|
541
551
|
if (account.validationUrl) return t('verifyAccount')
|
|
552
|
+
// #303: a quarantined account says why it is parked.
|
|
553
|
+
if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
|
|
554
|
+
return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
|
|
555
|
+
}
|
|
542
556
|
if (account.cooldownUntil && account.cooldownUntil > Date.now()) return t('coolingDown')
|
|
543
557
|
if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
|
|
544
558
|
return t('connected')
|
|
@@ -862,6 +876,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
862
876
|
React.createElement('div', { className: 'dsub-h' }, t('title')),
|
|
863
877
|
React.createElement('div', { className: 'dsub-sub' },
|
|
864
878
|
t('intro')),
|
|
879
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
880
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupAuth')),
|
|
865
881
|
React.createElement('label', { className: 'dsub-row' },
|
|
866
882
|
React.createElement('input', {
|
|
867
883
|
type: 'checkbox',
|
|
@@ -875,23 +891,26 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
875
891
|
React.createElement('label', { className: 'dsub-row' },
|
|
876
892
|
React.createElement('input', {
|
|
877
893
|
type: 'checkbox',
|
|
878
|
-
checked: !!draft.
|
|
879
|
-
onChange: (e) => setDraft((d) => Object.assign({}, d, {
|
|
894
|
+
checked: !!draft.autoLoopback,
|
|
895
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
|
|
880
896
|
}),
|
|
881
|
-
React.createElement('span', null, t('
|
|
897
|
+
React.createElement('span', null, t('autoLoopback')),
|
|
882
898
|
),
|
|
883
899
|
React.createElement('div', { className: 'dsub-sub' },
|
|
884
|
-
t('
|
|
900
|
+
t('autoLoopbackHint')),
|
|
901
|
+
),
|
|
902
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
903
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupPrivacy')),
|
|
885
904
|
React.createElement('label', { className: 'dsub-row' },
|
|
886
905
|
React.createElement('input', {
|
|
887
906
|
type: 'checkbox',
|
|
888
|
-
checked: !!draft.
|
|
889
|
-
onChange: (e) => setDraft((d) => Object.assign({}, d, {
|
|
907
|
+
checked: !!draft.privacyMask,
|
|
908
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
|
|
890
909
|
}),
|
|
891
|
-
React.createElement('span', null, t('
|
|
910
|
+
React.createElement('span', null, t('privacyMask')),
|
|
892
911
|
),
|
|
893
912
|
React.createElement('div', { className: 'dsub-sub' },
|
|
894
|
-
t('
|
|
913
|
+
t('privacyMaskHint')),
|
|
895
914
|
React.createElement('label', { className: 'dsub-row' },
|
|
896
915
|
React.createElement('input', {
|
|
897
916
|
type: 'checkbox',
|
|
@@ -902,6 +921,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
902
921
|
),
|
|
903
922
|
React.createElement('div', { className: 'dsub-sub' },
|
|
904
923
|
t('hideDeprecatedHint')),
|
|
924
|
+
),
|
|
925
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
926
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupRotation')),
|
|
905
927
|
React.createElement('div', { className: 'dsub-row' },
|
|
906
928
|
React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
907
929
|
React.createElement('select', {
|
|
@@ -931,6 +953,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
931
953
|
),
|
|
932
954
|
React.createElement('div', { className: 'dsub-sub' },
|
|
933
955
|
t('expiryNotifyDaysHint')),
|
|
956
|
+
),
|
|
957
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
958
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupProvider')),
|
|
934
959
|
React.createElement('label', { className: 'dsub-row' },
|
|
935
960
|
React.createElement('input', {
|
|
936
961
|
type: 'checkbox',
|
|
@@ -956,6 +981,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
956
981
|
),
|
|
957
982
|
React.createElement('div', { className: 'dsub-sub' },
|
|
958
983
|
t('codexVerbosityHint')),
|
|
984
|
+
),
|
|
985
|
+
React.createElement('details', { className: 'dsub-group', open: true },
|
|
986
|
+
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
|
|
959
987
|
React.createElement('label', { className: 'dsub-row' },
|
|
960
988
|
React.createElement('input', {
|
|
961
989
|
type: 'checkbox',
|
|
@@ -982,6 +1010,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
982
1010
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
|
|
983
1011
|
}),
|
|
984
1012
|
) : null,
|
|
1013
|
+
),
|
|
985
1014
|
React.createElement('details', { className: 'dsub-details' },
|
|
986
1015
|
React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
|
|
987
1016
|
React.createElement('div', { className: 'dsub-block' },
|
|
@@ -1691,15 +1720,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1691
1720
|
const rows = state.accounts.map((a, i) => {
|
|
1692
1721
|
const isLogged = state.logged.includes(a.provider)
|
|
1693
1722
|
const cooled = a.cooldownUntil && a.cooldownUntil > now
|
|
1723
|
+
const quarantined = Boolean(a.quarantineReason) && Number(a.quarantineUntil || 0) > now
|
|
1694
1724
|
const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
|
|
1695
1725
|
const b = brandBadge(a.provider)
|
|
1696
1726
|
const isActiveThis = state.active && state.active.provider === a.provider && state.active.index === a.index
|
|
1697
1727
|
|
|
1698
1728
|
let statusClass = 'dsub-statusOff'
|
|
1699
1729
|
let statusText = t('subsNotLogged')
|
|
1700
|
-
if (
|
|
1730
|
+
if (quarantined) {
|
|
1731
|
+
statusClass = 'dsub-statusBad'
|
|
1732
|
+
statusText = a.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(a.quarantineReason)
|
|
1733
|
+
} else if (cooled) {
|
|
1701
1734
|
statusClass = 'dsub-statusWarn'
|
|
1702
|
-
|
|
1735
|
+
// Only some model families may be cooled down - say which ones.
|
|
1736
|
+
statusText = t('subsCooldown') + (Array.isArray(a.cooldownFamilies) && a.cooldownFamilies.length ? ' · ' + a.cooldownFamilies.join('/') : '')
|
|
1703
1737
|
} else if (isLogged) {
|
|
1704
1738
|
if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
|
|
1705
1739
|
else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
|
|
@@ -1715,7 +1749,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1715
1749
|
isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
|
|
1716
1750
|
a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
|
|
1717
1751
|
),
|
|
1718
|
-
React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
|
|
1752
|
+
React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass, role: 'status', 'aria-live': 'polite' }, statusText),
|
|
1719
1753
|
),
|
|
1720
1754
|
(isLogged && pct != null) ? React.createElement('div', { className: 'dsub-barTrack', style: { marginTop: 4, height: 4 } },
|
|
1721
1755
|
React.createElement('div', {
|
|
@@ -1746,7 +1780,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1746
1780
|
className: 'dsub-modalWrap',
|
|
1747
1781
|
onClick: (e) => { if (e.target === e.currentTarget) setState((s) => Object.assign({}, s, { open: false })) },
|
|
1748
1782
|
},
|
|
1749
|
-
React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
|
|
1783
|
+
React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': t('subsModalTitle') },
|
|
1750
1784
|
React.createElement('div', { className: 'dsub-modalHead' },
|
|
1751
1785
|
React.createElement('div', { className: 'dsub-modalTitleWrap' },
|
|
1752
1786
|
React.createElement('div', { className: 'dsub-modalIcon' }, '⚡'),
|
|
@@ -1765,6 +1799,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1765
1799
|
React.createElement('button', {
|
|
1766
1800
|
type: 'button',
|
|
1767
1801
|
className: 'dsub-mini',
|
|
1802
|
+
autoFocus: true,
|
|
1803
|
+
'aria-label': t('closeModal'),
|
|
1768
1804
|
onClick: () => setState((s) => Object.assign({}, s, { open: false })),
|
|
1769
1805
|
}, '✕'),
|
|
1770
1806
|
),
|
package/lib/http.js
CHANGED
|
@@ -12,7 +12,7 @@ export function writeHtml(res, code, html) {
|
|
|
12
12
|
} catch { /* socket closed */ }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function readBody(req, maxBytes) {
|
|
15
|
+
export function readBody(req, maxBytes = 64 * 1024) {
|
|
16
16
|
return new Promise((resolve, reject) => {
|
|
17
17
|
const chunks = []
|
|
18
18
|
let size = 0
|
|
@@ -30,7 +30,10 @@ export function readBody(req, maxBytes) {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* @param {import('node:http').IncomingMessage} request
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
* Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
|
|
34
37
|
export function isTrustedSettingsRequest(request) {
|
|
35
38
|
return request.headers['sec-fetch-site'] !== 'cross-site'
|
|
36
39
|
}
|
package/lib/import-auth.js
CHANGED
|
@@ -4,8 +4,12 @@ import { join } from 'node:path'
|
|
|
4
4
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
5
5
|
import { discoverCodingAgentConfigs } from './importers/coding-agents.js'
|
|
6
6
|
|
|
7
|
+
// Test seam: a fixture root overrides the real home for the duration of a
|
|
8
|
+
// public call. Undefined (the default) keeps production behaviour.
|
|
9
|
+
let activeHome = null
|
|
10
|
+
|
|
7
11
|
function homeFile(...parts) {
|
|
8
|
-
return join(homedir(), ...parts)
|
|
12
|
+
return join(activeHome || homedir(), ...parts)
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
async function readJson(path) {
|
|
@@ -17,7 +21,17 @@ async function readJson(path) {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
export async function discoverLocalCliSessions() {
|
|
24
|
+
export async function discoverLocalCliSessions(options = {}) {
|
|
25
|
+
const prevHome = activeHome
|
|
26
|
+
activeHome = options && options.home ? String(options.home) : null
|
|
27
|
+
try {
|
|
28
|
+
return await discoverLocalCliSessionsInner()
|
|
29
|
+
} finally {
|
|
30
|
+
activeHome = prevHome
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function discoverLocalCliSessionsInner() {
|
|
21
35
|
const detected = {}
|
|
22
36
|
|
|
23
37
|
// 1. Codex CLI
|
|
@@ -144,7 +158,7 @@ export async function discoverLocalCliSessions() {
|
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
// 8. Claude CLI Direct Import
|
|
147
|
-
const claudeCreds = await readClaudeCredentials()
|
|
161
|
+
const claudeCreds = await readClaudeCredentials(activeHome)
|
|
148
162
|
if (claudeCreds) {
|
|
149
163
|
detected['claude-cli'] = {
|
|
150
164
|
provider: 'claude',
|
|
@@ -186,13 +200,13 @@ export async function discoverLocalCliSessions() {
|
|
|
186
200
|
return detected
|
|
187
201
|
}
|
|
188
202
|
|
|
189
|
-
export async function loadLocalCliBlob(provider) {
|
|
190
|
-
const discovered = await discoverLocalCliSessions()
|
|
203
|
+
export async function loadLocalCliBlob(provider, options = {}) {
|
|
204
|
+
const discovered = await discoverLocalCliSessions(options)
|
|
191
205
|
const info = discovered[provider]
|
|
192
206
|
if (!info) throw new Error(`no local CLI session found for ${provider}`)
|
|
193
207
|
|
|
194
208
|
if (provider === 'claude-cli' || (provider === 'claude' && info.provider === 'claude')) {
|
|
195
|
-
const creds = await readClaudeCredentials()
|
|
209
|
+
const creds = await readClaudeCredentials(options && options.home)
|
|
196
210
|
if (!creds) throw new Error('Claude CLI credentials not found')
|
|
197
211
|
return {
|
|
198
212
|
accessToken: creds.accessToken,
|
package/lib/index.js
CHANGED
|
@@ -141,7 +141,7 @@ export function apply(ctx, config) {
|
|
|
141
141
|
if (!cfg.ollamaFallback || !models.length) throw err
|
|
142
142
|
const model = cfg.ollamaFallbackModel || models[0].id
|
|
143
143
|
try { ctx.emit && ctx.emit('subscriptions.ollama-fallback', { provider, model, reason: err && err.code || 'EXHAUSTED' }) } catch {}
|
|
144
|
-
try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}),
|
|
144
|
+
try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}), falling back to ollama/${model}`) } catch {}
|
|
145
145
|
try {
|
|
146
146
|
recordHistory({
|
|
147
147
|
provider: 'ollama',
|
|
@@ -440,6 +440,11 @@ export function apply(ctx, config) {
|
|
|
440
440
|
configured: info.configured,
|
|
441
441
|
writable: info.writable,
|
|
442
442
|
cooldownUntil: info.cooldownUntil,
|
|
443
|
+
// #303: expose the cooldown scope and quarantine so the card can
|
|
444
|
+
// explain why only some models are blocked.
|
|
445
|
+
cooldownFamilies: info.cooldownFamilies || null,
|
|
446
|
+
quarantineUntil: info.quarantineUntil || 0,
|
|
447
|
+
quarantineReason: info.quarantineReason || null,
|
|
443
448
|
usagePercent: info.usagePercent,
|
|
444
449
|
quota: info.quota || null,
|
|
445
450
|
usage: info.usage || null,
|
package/lib/quarantine.js
CHANGED
|
@@ -29,10 +29,3 @@ export function isQuarantined(account, nowMs = Date.now()) {
|
|
|
29
29
|
return Number(account.quarantineUntil) > Number(nowMs)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function releaseFromQuarantine(account) {
|
|
33
|
-
const next = { ...account }
|
|
34
|
-
delete next.quarantineReason
|
|
35
|
-
delete next.quarantineUntil
|
|
36
|
-
if (next.status === 'quarantine') next.status = 'active'
|
|
37
|
-
return next
|
|
38
|
-
}
|
package/lib/ratelimit-parser.js
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export function parseRateLimitHeaders(headers) {
|
|
2
|
-
if (!headers || typeof headers !== 'object') return null
|
|
3
|
-
|
|
4
|
-
const get = (key) => {
|
|
5
|
-
const k = Object.keys(headers).find((h) => h.toLowerCase() === key.toLowerCase())
|
|
6
|
-
return k ? headers[k] : null
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const remainingTokens = get('x-ratelimit-remaining-tokens')
|
|
10
|
-
const resetTokens = get('x-ratelimit-reset-tokens')
|
|
11
|
-
const remainingRequests = get('x-ratelimit-remaining-requests')
|
|
12
|
-
const resetRequests = get('x-ratelimit-reset-requests')
|
|
13
|
-
|
|
14
|
-
if (!remainingTokens && !resetTokens && !remainingRequests && !resetRequests) {
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
remainingTokens: remainingTokens != null ? Number(remainingTokens) : null,
|
|
20
|
-
resetTokensTime: resetTokens || null,
|
|
21
|
-
remainingRequests: remainingRequests != null ? Number(remainingRequests) : null,
|
|
22
|
-
resetRequestsTime: resetRequests || null
|
|
23
|
-
}
|
|
24
|
-
}
|
package/lib/ratelimit.js
CHANGED
|
@@ -208,11 +208,3 @@ export function quotaSnapshot(provider, headers, bodyJson, nowMs = Date.now()) {
|
|
|
208
208
|
return { remaining, limit, resetAt: resetAt ?? null, usedPercent, measuredAt: nowMs }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
export function formatQuota(q) {
|
|
212
|
-
if (!q) return ""
|
|
213
|
-
const parts = []
|
|
214
|
-
if (q.remaining!=null && q.limit!=null) parts.push(q.remaining+"/"+q.limit)
|
|
215
|
-
else if (q.remaining!=null) parts.push("left "+q.remaining)
|
|
216
|
-
if (q.resetAt) parts.push("reset "+new Date(q.resetAt).toLocaleString())
|
|
217
|
-
return parts.join(" · ")
|
|
218
|
-
}
|
package/lib/refs.js
CHANGED
|
@@ -61,6 +61,11 @@ export function displayName(provider) {
|
|
|
61
61
|
return DISPLAY[provider] || displayExtra[provider] || provider
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} provider
|
|
66
|
+
* @param {number} index
|
|
67
|
+
* @returns {string} provider-scoped credential ref, e.g. CODEX_OAUTH_1
|
|
68
|
+
*/
|
|
64
69
|
export function oauthRef(provider, index) {
|
|
65
70
|
const n = Number(index)
|
|
66
71
|
if (!Number.isInteger(n) || n < 1) throw new Error('account index must be an integer >= 1')
|
package/lib/relative-time.js
CHANGED
|
@@ -26,6 +26,12 @@ function fill(template, n) {
|
|
|
26
26
|
return String(template).replace('{n}', String(n))
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {number} resetAt epoch ms of the quota window reset
|
|
31
|
+
* @param {string} [lang]
|
|
32
|
+
* @param {number} [now]
|
|
33
|
+
* @returns {string} human-readable remaining time, down to the minute
|
|
34
|
+
*/
|
|
29
35
|
export function formatRelativeReset(resetAt, lang = 'ru', now = Date.now()) {
|
|
30
36
|
if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
|
|
31
37
|
const delta = resetAt - now
|
package/lib/routes/accounts.js
CHANGED
package/lib/routes/oauth.js
CHANGED
package/lib/routes/status.js
CHANGED
package/lib/routes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { publicConfig } from './config-schema.js'
|
|
2
2
|
import { PROVIDERS, displayName } from './refs.js'
|
|
3
|
-
import { writeHtml, writeJson } from './http.js'
|
|
3
|
+
import { escapeHtml, writeHtml, writeJson } from './http.js'
|
|
4
4
|
import { registerStatusRoutes } from './routes/status.js'
|
|
5
5
|
import { registerOauthRoutes } from './routes/oauth.js'
|
|
6
6
|
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
@@ -11,7 +11,6 @@ export function registerRoutes(ctx, state) {
|
|
|
11
11
|
accountsView,
|
|
12
12
|
history,
|
|
13
13
|
live,
|
|
14
|
-
subscriptions,
|
|
15
14
|
} = state
|
|
16
15
|
|
|
17
16
|
async function configResponse() {
|
|
@@ -101,10 +100,3 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'
|
|
|
101
100
|
registerAccountsRoutes(ctx, state)
|
|
102
101
|
registerProxyRoutes(ctx, state)
|
|
103
102
|
}
|
|
104
|
-
|
|
105
|
-
export function escapeHtml(text) {
|
|
106
|
-
return String(text)
|
|
107
|
-
.replace(/&/g, '&')
|
|
108
|
-
.replace(/</g, '<')
|
|
109
|
-
.replace(/>/g, '>')
|
|
110
|
-
}
|
package/lib/session-pin.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
const sessionPins = new Map()
|
|
2
2
|
const DEFAULT_TTL_MS = 30 * 60 * 1000 // 30 mins
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Bind a session id to an account ref for ttlMs (default 30 minutes).
|
|
6
|
+
* @param {string|undefined} sessionId
|
|
7
|
+
* @param {string|undefined} accountRef
|
|
8
|
+
* @param {number} [ttlMs]
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
4
11
|
export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
|
|
5
12
|
if (!sessionId || !accountRef) return
|
|
6
13
|
sessionPins.set(sessionId, {
|
|
@@ -9,6 +16,10 @@ export function pinSession(sessionId, accountRef, ttlMs = DEFAULT_TTL_MS) {
|
|
|
9
16
|
})
|
|
10
17
|
}
|
|
11
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @param {string|undefined} sessionId
|
|
21
|
+
* @returns {string|null} the bound account ref, or null when absent/expired
|
|
22
|
+
*/
|
|
12
23
|
export function getPinnedAccountRef(sessionId) {
|
|
13
24
|
if (!sessionId) return null
|
|
14
25
|
const entry = sessionPins.get(sessionId)
|
|
@@ -150,7 +150,9 @@ export async function usage(blob, _cfg, fetchImpl) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, saveBlob }) {
|
|
153
|
-
|
|
153
|
+
// Abort must cover the project pre-flight as well as the stream.
|
|
154
|
+
const preflight = (u, i) => fetchImpl(u, { ...(i || {}), signal })
|
|
155
|
+
const ready = await withProject(blob, preflight, saveBlob)
|
|
154
156
|
const { envelope, sessionId } = streamEnvelope({
|
|
155
157
|
projectId: ready.projectId,
|
|
156
158
|
model: options.model,
|
|
@@ -2,8 +2,12 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Read Claude Code CLI credentials. `home` is a test seam: when omitted the
|
|
7
|
+
* real user home is used, so production behaviour is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export async function readClaudeCredentials(home) {
|
|
10
|
+
const p = join(home || homedir(), '.claude', 'credentials.json')
|
|
7
11
|
try {
|
|
8
12
|
const text = await readFile(p, 'utf8')
|
|
9
13
|
const json = JSON.parse(text)
|
package/lib/vendors/cody.js
CHANGED
|
@@ -16,6 +16,11 @@ export const CODY_MODELS = [
|
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
+
/** Catalog id -> Sourcegraph model name. */
|
|
20
|
+
export const CODY_MODEL_ALIASES = {
|
|
21
|
+
'cody-claude-3.5-sonnet': 'claude-3-5-sonnet',
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export function providerInfo() {
|
|
20
25
|
return { id, name: 'Sourcegraph Cody' }
|
|
21
26
|
}
|
|
@@ -50,8 +55,14 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
50
55
|
if (!token) throw new LlmError('Sourcegraph Cody not authenticated', 'AUTH')
|
|
51
56
|
|
|
52
57
|
const url = `${(config && config.apiBase) || CODY_API_BASE}/chat/completions`
|
|
58
|
+
// Catalog ids carry a cody- prefix and dotted versions; the Sourcegraph
|
|
59
|
+
// API expects the bare dashed name (cody-claude-3.5-sonnet -> claude-3-5-sonnet).
|
|
60
|
+
const requested = String(options.model || '').trim()
|
|
61
|
+
const mapped = CODY_MODEL_ALIASES[requested]
|
|
62
|
+
|| requested.replace(/^cody-/, '').replace(/\./g, '-')
|
|
63
|
+
|| 'claude-3-5-sonnet'
|
|
53
64
|
const body = {
|
|
54
|
-
model:
|
|
65
|
+
model: mapped,
|
|
55
66
|
messages: openaiMessages(options),
|
|
56
67
|
stream: true
|
|
57
68
|
}
|
package/lib/vendors/grok.js
CHANGED
|
@@ -236,7 +236,8 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
236
236
|
if (!body.instructions) delete body.instructions
|
|
237
237
|
// grok manages reasoning itself (cli catalog aware); drop the codex-level field
|
|
238
238
|
delete body.reasoning
|
|
239
|
-
|
|
239
|
+
// Thread the caller AbortSignal through the catalog pre-flight too.
|
|
240
|
+
const catalog = await cliCatalogCached(blob, config, (u, i) => fetchImpl(u, { ...(i || {}), signal })).catch(() => new Map())
|
|
240
241
|
const reasoning = grokReasoningBody(options.model, options.reasoningEffort, catalog)
|
|
241
242
|
if (reasoning) body.reasoning = reasoning
|
|
242
243
|
body = grokBodyForModel(options.model, body)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "eslint lib/ && node --test test/*.test.mjs"
|
|
38
|
+
"test": "eslint lib/ && node --test test/*.test.mjs",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
39
40
|
},
|
|
40
41
|
"dsh": {
|
|
41
42
|
"bundle": {
|
|
@@ -62,6 +63,8 @@
|
|
|
62
63
|
"undici": "^8.10.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.8"
|
|
66
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
67
|
+
"@types/node": "^22.20.2",
|
|
68
|
+
"typescript": "^7.0.2"
|
|
66
69
|
}
|
|
67
70
|
}
|