@goodandready/dsh-subscriptions 0.5.22 → 0.5.25
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 +50 -2
- package/lib/adapter.js +2 -0
- package/lib/client.js +185 -74
- package/lib/code-assist.js +3 -0
- package/lib/google-validation.js +10 -0
- package/lib/http.js +22 -0
- package/lib/index.js +46 -1
- package/lib/rotate.js +2 -1
- package/lib/sse.js +20 -2
- package/lib/stream-rotate.js +14 -4
- package/lib/vendors/antigravity.js +1 -0
- package/lib/vendors/cursor.js +80 -8
- package/lib/wire.js +10 -2
- package/package.json +2 -2
package/lib/accounts.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
2
2
|
import { oauthRef, isProvider } from './refs.js'
|
|
3
3
|
import { parseBlob, serializeBlob } from './blob.js'
|
|
4
|
+
import { recordSwitch, recordExhaust, recordBroken } from './health.js'
|
|
5
|
+
import { executeWithRetry } from './backoff.js'
|
|
4
6
|
import { getVendor } from './vendors/index.js'
|
|
5
7
|
|
|
6
8
|
const SKEW_MS = 60 * 1000
|
|
@@ -54,6 +56,8 @@ export function vendorConfig(provider, cfg) {
|
|
|
54
56
|
export function createAccountStore({ credentials, getConfig, fetchImpl, fetchForRef, onLimitNotice }) {
|
|
55
57
|
const cooldowns = new Map()
|
|
56
58
|
const quotas = new Map()
|
|
59
|
+
const quarantines = new Map()
|
|
60
|
+
const writeQueues = new Map()
|
|
57
61
|
const refreshLocks = new Map()
|
|
58
62
|
const refreshFailures = new Map()
|
|
59
63
|
const usage = new Map()
|
|
@@ -86,13 +90,20 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
async function saveBlob(ref, blob) {
|
|
89
|
-
|
|
93
|
+
const prev = writeQueues.get(ref) || Promise.resolve()
|
|
94
|
+
const next = prev.catch(() => {}).then(async () => {
|
|
95
|
+
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
96
|
+
})
|
|
97
|
+
writeQueues.set(ref, next)
|
|
98
|
+
await next
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
async function clearRef(ref) {
|
|
93
102
|
await credentials.unset(credentialRef(ref))
|
|
94
103
|
cooldowns.delete(ref)
|
|
95
104
|
quotas.delete(ref)
|
|
105
|
+
quarantines.delete(ref)
|
|
106
|
+
writeQueues.delete(ref)
|
|
96
107
|
for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
|
|
97
108
|
for (const k of [...notifyThresholds.keys()]) { if (k.startsWith(ref + ':')) notifyThresholds.delete(k) }
|
|
98
109
|
refreshFailures.delete(ref)
|
|
@@ -128,6 +139,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
128
139
|
return {
|
|
129
140
|
...base,
|
|
130
141
|
cooldownUntil: cooldowns.get(ref) ? cooldowns.get(ref).until : 0,
|
|
142
|
+
quarantineUntil: quarantines.get(ref) ? quarantines.get(ref).until : 0,
|
|
143
|
+
quarantineReason: quarantines.get(ref) ? quarantines.get(ref).reason : null,
|
|
131
144
|
cooldownFamilies: cooldowns.get(ref) && cooldowns.get(ref).families ? cooldowns.get(ref).families : null,
|
|
132
145
|
quota: quotas.get(ref) || null,
|
|
133
146
|
usage: windows.get(ref) || base.usage || null,
|
|
@@ -144,14 +157,20 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
144
157
|
async function listAccounts(provider) {
|
|
145
158
|
const slots = normalizeSlots(getConfig().slots).filter((s) => s.provider === provider)
|
|
146
159
|
const out = []
|
|
160
|
+
const now = Date.now()
|
|
147
161
|
for (const slot of slots) {
|
|
148
162
|
const info = await describeRef(slot.ref)
|
|
163
|
+
const q = quarantines.get(slot.ref)
|
|
164
|
+
const inQ = q && Number(q.until) > now
|
|
149
165
|
out.push({
|
|
150
166
|
ref: slot.ref,
|
|
151
167
|
hasToken: !!info.configured,
|
|
152
168
|
usagePercent: info.usagePercent,
|
|
153
169
|
cooldownUntil: info.cooldownUntil,
|
|
170
|
+
cooldownFamilies: info.cooldownFamilies || null,
|
|
154
171
|
quota: info.quota || null,
|
|
172
|
+
quarantineUntil: inQ ? q.until : 0,
|
|
173
|
+
quarantineReason: inQ ? q.reason : null,
|
|
155
174
|
label: slot.label || info.label,
|
|
156
175
|
})
|
|
157
176
|
}
|
|
@@ -174,7 +193,19 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
174
193
|
const promise = (async () => {
|
|
175
194
|
try {
|
|
176
195
|
const cfg = vendorConfig(provider, getConfig())
|
|
177
|
-
const next = await
|
|
196
|
+
const next = await executeWithRetry(
|
|
197
|
+
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref)),
|
|
198
|
+
{
|
|
199
|
+
maxRetries: 2,
|
|
200
|
+
initialDelayMs: 300,
|
|
201
|
+
maxDelayMs: 2000,
|
|
202
|
+
isRetryable: (err) => {
|
|
203
|
+
const status = Number(err && (err.status || err.statusCode) || 0)
|
|
204
|
+
if (status === 400 || status === 401 || status === 403) return false
|
|
205
|
+
return true
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
)
|
|
178
209
|
const merged = {
|
|
179
210
|
...blob,
|
|
180
211
|
...next,
|
|
@@ -297,8 +328,25 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
297
328
|
recordSuccess(ref) {
|
|
298
329
|
// успешный запрос сбрасывает кулдаун и возвращает аккаунт в строй
|
|
299
330
|
cooldowns.delete(ref)
|
|
331
|
+
quarantines.delete(ref)
|
|
300
332
|
if (health.has(ref)) health.set(ref, null)
|
|
301
333
|
},
|
|
334
|
+
rememberQuarantine(ref, reason, until) {
|
|
335
|
+
if (!ref) return
|
|
336
|
+
quarantines.set(ref, { reason: reason || 'RATE_LIMIT', until: Number(until) || (Date.now() + 3600000) })
|
|
337
|
+
},
|
|
338
|
+
getQuarantine(ref) {
|
|
339
|
+
const q = quarantines.get(ref)
|
|
340
|
+
if (!q) return null
|
|
341
|
+
if (Number(q.until) <= Date.now()) {
|
|
342
|
+
quarantines.delete(ref)
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
return q
|
|
346
|
+
},
|
|
347
|
+
releaseQuarantine(ref) {
|
|
348
|
+
quarantines.delete(ref)
|
|
349
|
+
},
|
|
302
350
|
rememberRequest(ref) { requestCounts.set(ref, (requestCounts.get(ref) || 0) + 1) },
|
|
303
351
|
getRequestCount(ref) { return requestCounts.get(ref) || 0 },
|
|
304
352
|
}
|
package/lib/adapter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getVendor } from './vendors/index.js'
|
|
|
4
4
|
import { modelCatalog } from './messages.js'
|
|
5
5
|
import { streamWithRotation } from './stream-rotate.js'
|
|
6
6
|
import { pickFetch } from './proxy.js'
|
|
7
|
+
import { quotaSnapshot, parseBody } from './ratelimit.js'
|
|
7
8
|
|
|
8
9
|
// #94: heuristic for test/preview/beta/legacy model ids.
|
|
9
10
|
function isDeprecatedId(id) {
|
|
@@ -109,6 +110,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
109
110
|
if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
|
|
110
111
|
},
|
|
111
112
|
streamOnce: async function* (account, opts) {
|
|
113
|
+
const t0 = Date.now()
|
|
112
114
|
const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
113
115
|
const vendor = getVendor(provider)
|
|
114
116
|
// ponytail: capture x-ratelimit headers without touching vendors
|
package/lib/client.js
CHANGED
|
@@ -78,6 +78,24 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
'.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
|
|
79
79
|
'.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
80
80
|
'.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
|
|
81
|
+
'.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
|
|
82
|
+
'.dsub-panel-warn{border-color:rgba(245,158,11,0.4);background:rgba(245,158,11,0.05)}' +
|
|
83
|
+
'.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
84
|
+
'.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
85
|
+
'.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
|
|
86
|
+
'.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
|
|
87
|
+
'.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}' +
|
|
88
|
+
'.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
|
|
89
|
+
'.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
|
|
90
|
+
'.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
|
|
91
|
+
'.dsub-tag-ok{border-color:rgba(16,185,129,0.3);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
92
|
+
'.dsub-tag-warn{border-color:rgba(245,158,11,0.4);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
93
|
+
'.dsub-tag-bad{border-color:rgba(239,68,68,0.4);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
94
|
+
'.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
|
|
95
|
+
'.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
96
|
+
'.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
|
|
97
|
+
'.dsub-btn-danger{border-color:rgba(239,68,68,0.4)!important;color:var(--dsw-alias-state-error-primary)!important}' +
|
|
98
|
+
'.dsub-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important}' +
|
|
81
99
|
'.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
|
|
82
100
|
'.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
|
|
83
101
|
'.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
|
|
@@ -261,7 +279,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
261
279
|
'privacyMask': 'Mask emails and account identifiers',
|
|
262
280
|
'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
|
|
263
281
|
'resetCredits': 'Reset credits',
|
|
264
|
-
'resetCreditsHint': 'ChatGPT reset cards:
|
|
282
|
+
'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
|
|
283
|
+
'resetCreditsAction': 'Reset ChatGPT Limit',
|
|
284
|
+
'usageLimitsTitle': 'Usage Limits',
|
|
285
|
+
'slotDetailsTitle': 'Slot Details',
|
|
265
286
|
'resetAvailable': 'Reset attempts available',
|
|
266
287
|
'resetExpires': 'earliest expires',
|
|
267
288
|
'resetAck': 'I understand one attempt will be consumed',
|
|
@@ -335,6 +356,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
335
356
|
'slashStatus': 'Subscription status',
|
|
336
357
|
'none': 'none',
|
|
337
358
|
'forecast': '≈',
|
|
359
|
+
'resetCredits': 'Сброс лимитов ChatGPT',
|
|
360
|
+
'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
|
|
361
|
+
'resetCreditsAction': 'Сбросить лимит ChatGPT',
|
|
362
|
+
'resetAvailable': 'Доступно сбросов',
|
|
363
|
+
'resetExpires': 'срок действия',
|
|
364
|
+
'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
|
|
365
|
+
'resetWait': 'подтверждение через',
|
|
366
|
+
'resetReady': 'готово к сбросу',
|
|
367
|
+
'resetGo': 'Сбросить лимит сейчас',
|
|
368
|
+
'resetBusy': 'Сброс…',
|
|
369
|
+
'resetDone': 'Лимит успешно сброшен',
|
|
370
|
+
'resetNothing': 'Лимит не требует сброса',
|
|
371
|
+
'resetNoCredit': 'Нет доступных попыток сброса',
|
|
372
|
+
'resetRedeemed': 'Попытка уже использована',
|
|
373
|
+
'windowPrimary': 'Окно 5 часов',
|
|
374
|
+
'windowSecondary': 'Окно 7 дней',
|
|
375
|
+
'usageLimitsTitle': 'Лимиты использования',
|
|
376
|
+
'slotDetailsTitle': 'Параметры слота',
|
|
338
377
|
'resetLabel': 'reset',
|
|
339
378
|
'check': 'Check',
|
|
340
379
|
'checking': 'Checking…',
|
|
@@ -457,6 +496,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
457
496
|
'slashStatus': 'Статус подписок',
|
|
458
497
|
'none': 'нет',
|
|
459
498
|
'forecast': '≈',
|
|
499
|
+
'resetCredits': 'Сброс лимитов ChatGPT',
|
|
500
|
+
'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
|
|
501
|
+
'resetCreditsAction': 'Сбросить лимит ChatGPT',
|
|
502
|
+
'resetAvailable': 'Доступно сбросов',
|
|
503
|
+
'resetExpires': 'срок действия',
|
|
504
|
+
'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
|
|
505
|
+
'resetWait': 'подтверждение через',
|
|
506
|
+
'resetReady': 'готово к сбросу',
|
|
507
|
+
'resetGo': 'Сбросить лимит сейчас',
|
|
508
|
+
'resetBusy': 'Сброс…',
|
|
509
|
+
'resetDone': 'Лимит успешно сброшен',
|
|
510
|
+
'resetNothing': 'Лимит не требует сброса',
|
|
511
|
+
'resetNoCredit': 'Нет доступных попыток сброса',
|
|
512
|
+
'resetRedeemed': 'Попытка уже использована',
|
|
513
|
+
'windowPrimary': 'Окно 5 часов',
|
|
514
|
+
'windowSecondary': 'Окно 7 дней',
|
|
515
|
+
'usageLimitsTitle': 'Лимиты использования',
|
|
516
|
+
'slotDetailsTitle': 'Параметры слота',
|
|
460
517
|
}
|
|
461
518
|
|
|
462
519
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
@@ -1082,39 +1139,54 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1082
1139
|
return code
|
|
1083
1140
|
}
|
|
1084
1141
|
if (st.phase !== 'confirm') {
|
|
1085
|
-
return React.createElement('div', { className: 'dsub-
|
|
1086
|
-
React.createElement('
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1142
|
+
return React.createElement('div', { className: 'dsub-panel-box' },
|
|
1143
|
+
React.createElement('div', { className: 'dsub-panel-title' },
|
|
1144
|
+
React.createElement('span', null, '⚡ ' + t('resetCredits')),
|
|
1145
|
+
React.createElement('button', {
|
|
1146
|
+
type: 'button',
|
|
1147
|
+
className: 'dsub-mini',
|
|
1148
|
+
disabled: st.phase === 'loading',
|
|
1149
|
+
onClick: () => resetPrepare(row.slot.provider, row.slot.index, key).catch((e) => setErr(String(e && e.message || e))),
|
|
1150
|
+
}, st.phase === 'loading' ? '…' : t('resetCreditsAction'))
|
|
1151
|
+
),
|
|
1152
|
+
React.createElement('div', { className: 'dsub-panel-desc' }, t('resetCreditsHint'))
|
|
1091
1153
|
)
|
|
1092
1154
|
}
|
|
1093
1155
|
var ready = now >= st.readyAt
|
|
1094
1156
|
var secs = Math.max(0, Math.ceil((st.readyAt - now) / 1000))
|
|
1095
|
-
return React.createElement('div', { className: 'dsub-
|
|
1096
|
-
React.createElement('div',
|
|
1097
|
-
|
|
1098
|
-
|
|
1157
|
+
return React.createElement('div', { className: 'dsub-panel-box dsub-panel-warn' },
|
|
1158
|
+
React.createElement('div', { className: 'dsub-panel-title' },
|
|
1159
|
+
React.createElement('span', null, '⚠️ ' + t('resetCredits')),
|
|
1160
|
+
React.createElement('span', { className: 'dsub-tag-ok' }, t('resetAvailable') + ': ' + st.availableCount)
|
|
1099
1161
|
),
|
|
1100
|
-
React.createElement('
|
|
1162
|
+
st.creditExpiresAt ? React.createElement('div', { className: 'dsub-panel-desc' },
|
|
1163
|
+
t('resetExpires') + ': ' + new Date(st.creditExpiresAt).toLocaleString()
|
|
1164
|
+
) : null,
|
|
1165
|
+
React.createElement('label', { className: 'dsub-row', style: { cursor: 'pointer', margin: '4px 0' } },
|
|
1101
1166
|
React.createElement('input', {
|
|
1102
1167
|
type: 'checkbox',
|
|
1103
1168
|
checked: !!st.ack,
|
|
1104
1169
|
onChange: (e) => setReset((m) => Object.assign({}, m, { [key]: Object.assign({}, st, { ack: e.target.checked }) })),
|
|
1105
1170
|
}),
|
|
1106
|
-
React.createElement('span',
|
|
1171
|
+
React.createElement('span', { style: { fontSize: '12px', fontWeight: 500 } }, t('resetAck')),
|
|
1107
1172
|
),
|
|
1108
|
-
React.createElement('div', { className: 'dsub-row' },
|
|
1173
|
+
React.createElement('div', { className: 'dsub-row', style: { gap: '8px' } },
|
|
1109
1174
|
React.createElement('button', {
|
|
1110
|
-
type: 'button',
|
|
1175
|
+
type: 'button',
|
|
1176
|
+
className: 'dsub-mini dsub-btn-danger',
|
|
1111
1177
|
disabled: !st.ack || !ready || !!st.busy,
|
|
1112
1178
|
onClick: () => resetConsume(key).catch((e) => setErr(String(e && e.message || e))),
|
|
1113
1179
|
}, st.busy ? t('resetBusy') : (ready ? t('resetGo') : (t('resetWait') + ' ' + secs + 's'))),
|
|
1114
1180
|
!ready && !st.busy ? React.createElement('span', { className: 'dsub-sub' }, t('resetWait') + ' ' + secs + 's') : null,
|
|
1115
|
-
st.ack && ready ? React.createElement('span', { className: 'dsub-ok' }, t('resetReady')) : null,
|
|
1181
|
+
st.ack && ready ? React.createElement('span', { className: 'dsub-ok', style: { fontSize: '12px', fontWeight: 600 } }, '✓ ' + t('resetReady')) : null,
|
|
1182
|
+
React.createElement('button', {
|
|
1183
|
+
type: 'button',
|
|
1184
|
+
className: 'dsub-mini',
|
|
1185
|
+
style: { marginLeft: 'auto' },
|
|
1186
|
+
onClick: () => setReset((m) => Object.assign({}, m, { [key]: { phase: 'idle' } })),
|
|
1187
|
+
}, t('cancel'))
|
|
1116
1188
|
),
|
|
1117
|
-
st.result ? React.createElement('div', { className: 'dsub-ok' }, resultText(st.result)) : null,
|
|
1189
|
+
st.result ? React.createElement('div', { className: 'dsub-ok', style: { fontWeight: 600 } }, resultText(st.result)) : null,
|
|
1118
1190
|
st.error ? React.createElement('div', { className: 'dsub-bad' }, st.error) : null,
|
|
1119
1191
|
)
|
|
1120
1192
|
})() : null),
|
|
@@ -1126,56 +1198,89 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1126
1198
|
React.createElement('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, t('verifyLink')),
|
|
1127
1199
|
t('verifySuffix'),
|
|
1128
1200
|
) : null,
|
|
1129
|
-
(function(){
|
|
1130
|
-
var
|
|
1131
|
-
var
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1201
|
+
(function usageLimitsBlock(){
|
|
1202
|
+
var wins = account.usage
|
|
1203
|
+
var q = account.quota
|
|
1204
|
+
var hasWins = Array.isArray(wins) && wins.length > 0
|
|
1205
|
+
var hasQuota = q && q.usedPercent != null
|
|
1206
|
+
var hasUsagePct = !hasWins && !hasQuota && account.usagePercent != null
|
|
1207
|
+
|
|
1208
|
+
if (!hasWins && !hasQuota && !hasUsagePct) return null
|
|
1209
|
+
|
|
1210
|
+
function windowLabel(w){
|
|
1211
|
+
var id = String((w && w.id) || '')
|
|
1212
|
+
var given = w && (w.ru || w.en)
|
|
1213
|
+
if (given && given !== id) return given
|
|
1214
|
+
if (id === 'primary_window') return t('windowPrimary')
|
|
1215
|
+
if (id === 'secondary_window') return t('windowSecondary')
|
|
1216
|
+
if (!id) return t('quota')
|
|
1217
|
+
return id.replace(/_/g, ' ')
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function renderItem(label, pct, detail){
|
|
1221
|
+
var p = Math.min(100, Math.max(0, Math.round(pct)))
|
|
1222
|
+
var tagCls = p >= 90 ? 'dsub-tag-bad' : (p >= 70 ? 'dsub-tag-warn' : 'dsub-tag-ok')
|
|
1223
|
+
var fillBg = p >= 90 ? 'var(--dsw-alias-state-error-primary)' : (p >= 70 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)')
|
|
1224
|
+
return React.createElement('div', { key: label, className: 'dsub-usage-card' },
|
|
1225
|
+
React.createElement('div', { className: 'dsub-usage-head' },
|
|
1226
|
+
React.createElement('span', null, label),
|
|
1227
|
+
React.createElement('span', { className: tagCls }, p + '%')
|
|
1138
1228
|
),
|
|
1139
|
-
React.createElement('
|
|
1229
|
+
React.createElement('div', { className: 'dsub-usage-track' },
|
|
1230
|
+
React.createElement('div', {
|
|
1231
|
+
className: 'dsub-usage-fill',
|
|
1232
|
+
style: { width: p + '%', background: fillBg }
|
|
1233
|
+
})
|
|
1234
|
+
),
|
|
1235
|
+
detail ? React.createElement('div', { className: 'dsub-usage-meta' }, detail) : null
|
|
1140
1236
|
)
|
|
1141
1237
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
wins.forEach(function(w){
|
|
1156
|
-
if(w&&w.usedPercent!=null)parts.push(bar(windowLabel(w),w.usedPercent))
|
|
1238
|
+
|
|
1239
|
+
var items = []
|
|
1240
|
+
if (hasWins) {
|
|
1241
|
+
wins.forEach(function(w, idx){
|
|
1242
|
+
if (w && w.usedPercent != null) {
|
|
1243
|
+
var extra = null
|
|
1244
|
+
if (idx === 0 && account.requests && w.usedPercent < 100) {
|
|
1245
|
+
var rem = 100 - w.usedPercent
|
|
1246
|
+
var est = Math.floor(rem / (w.usedPercent / account.requests))
|
|
1247
|
+
if (est > 0) extra = React.createElement('span', null, t('forecast') + ' ' + est)
|
|
1248
|
+
}
|
|
1249
|
+
items.push(renderItem(windowLabel(w), w.usedPercent, extra))
|
|
1250
|
+
}
|
|
1157
1251
|
})
|
|
1158
1252
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
if(est>0)parts.push(React.createElement('span',{className:'dsub-sub',key:'fc'},t('forecast')+' '+est+' ('+windowLabel(w0)+')'))
|
|
1171
|
-
}
|
|
1253
|
+
if (hasQuota) {
|
|
1254
|
+
items.push(renderItem(t('quota'), q.usedPercent, q.resetAt ? React.createElement(ResetCountdown, { key: 'reset', resetAt: q.resetAt }) : null))
|
|
1255
|
+
} else if (hasUsagePct) {
|
|
1256
|
+
items.push(renderItem(t('quota'), account.usagePercent, null))
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
var at = (q && q.measuredAt) || (account.usageAt || 0)
|
|
1260
|
+
var agoStr = null
|
|
1261
|
+
if (at) {
|
|
1262
|
+
var m = Math.round((Date.now() - at) / 60000)
|
|
1263
|
+
if (m >= 1) agoStr = m + 'm ' + (t('lang') === 'ru' || t('refresh') === 'Обновить' ? 'назад' : 'ago')
|
|
1172
1264
|
}
|
|
1173
|
-
if(!parts.length)return null
|
|
1174
|
-
return React.createElement('div',{className:'dsub-block'},parts)
|
|
1175
|
-
})(),
|
|
1176
1265
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1266
|
+
return React.createElement('div', { className: 'dsub-panel-box' },
|
|
1267
|
+
React.createElement('div', { className: 'dsub-panel-title' },
|
|
1268
|
+
React.createElement('span', null, '📊 ' + t('usageLimitsTitle')),
|
|
1269
|
+
agoStr ? React.createElement('span', { className: 'dsub-sub' }, agoStr) : null
|
|
1270
|
+
),
|
|
1271
|
+
React.createElement('div', { className: 'dsub-usage-grid' }, items)
|
|
1272
|
+
)
|
|
1273
|
+
})(),
|
|
1274
|
+
(account.paidTierName || account.ref ? React.createElement('div', { className: 'dsub-slot-meta-row' },
|
|
1275
|
+
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' },
|
|
1276
|
+
React.createElement('strong', null, t('plan') + ': '),
|
|
1277
|
+
account.paidTierName
|
|
1278
|
+
) : null,
|
|
1279
|
+
account.ref ? React.createElement('span', { className: 'dsub-ref-tag' },
|
|
1280
|
+
React.createElement('span', null, t('storedAs')),
|
|
1281
|
+
React.createElement('code', null, account.ref)
|
|
1282
|
+
) : null
|
|
1283
|
+
) : null),
|
|
1179
1284
|
)
|
|
1180
1285
|
}),
|
|
1181
1286
|
React.createElement('button', {
|
|
@@ -1260,20 +1365,26 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1260
1365
|
))
|
|
1261
1366
|
} catch { moved = false }
|
|
1262
1367
|
if (moved) return
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1368
|
+
// #244: Fallback for harness cores before settings.plugin.item slot was
|
|
1369
|
+
// introduced (< 0.1.2-rc.1). Once all production profiles run a core
|
|
1370
|
+
// that has settings.plugin.item, this block can be safely removed.
|
|
1371
|
+
// Tracked in Gitea #244 — do not remove without verifying minimum core.
|
|
1372
|
+
try {
|
|
1373
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1374
|
+
{
|
|
1375
|
+
name: 'settings.section',
|
|
1376
|
+
id: '@goodandready/dsh-subscriptions',
|
|
1377
|
+
order: 28,
|
|
1378
|
+
locale: NS,
|
|
1379
|
+
label: () => t('title'),
|
|
1380
|
+
inject: () => ({ ctx: ctx }),
|
|
1381
|
+
},
|
|
1382
|
+
SubsSection,
|
|
1383
|
+
))
|
|
1384
|
+
} catch (e) {
|
|
1385
|
+
// Neither slot system available — settings panel will not render.
|
|
1386
|
+
// This is expected on headless / non-web profiles.
|
|
1387
|
+
}
|
|
1277
1388
|
}
|
|
1278
1389
|
|
|
1279
1390
|
// Горячий переключатель провайдера в строке композера: клик циклично
|
package/lib/code-assist.js
CHANGED
|
@@ -128,11 +128,13 @@ export async function discoverProject(fetchImpl, token, metadata, extraHeaders)
|
|
|
128
128
|
}
|
|
129
129
|
const projectId = projectFrom(op) || account.projectId
|
|
130
130
|
const after = accountFromLoad(op && op.response ? op.response : load)
|
|
131
|
+
const notice = noticeFromLoadCodeAssist(op && op.response ? op.response : load)
|
|
131
132
|
return {
|
|
132
133
|
projectId,
|
|
133
134
|
tierId: after.paidTierId || tierId,
|
|
134
135
|
paidTierId: after.paidTierId,
|
|
135
136
|
paidTierName: after.paidTierName,
|
|
137
|
+
accountNotice: notice ? notice.message : '',
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -231,6 +233,7 @@ export async function inspectGoogleAccount(fetchImpl, token, { metadata, extraHe
|
|
|
231
233
|
return {
|
|
232
234
|
validation: null,
|
|
233
235
|
notice,
|
|
236
|
+
accountNotice: notice ? notice.message : '',
|
|
234
237
|
projectId: resolvedProject,
|
|
235
238
|
paidTierId: account.paidTierId,
|
|
236
239
|
paidTierName: account.paidTierName,
|
package/lib/google-validation.js
CHANGED
|
@@ -115,3 +115,13 @@ export function googleRateLimitMessage(status, bodyText) {
|
|
|
115
115
|
}
|
|
116
116
|
return ''
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
export function googleLicenseMessage(status, bodyText) {
|
|
120
|
+
if (status !== 403) return ''
|
|
121
|
+
const api = parseGoogleApiError(bodyText)
|
|
122
|
+
const msg = String(api?.message || bodyText || '')
|
|
123
|
+
if (/do not have a valid license|#3501|PERMISSION_DENIED/i.test(msg)) {
|
|
124
|
+
return 'У аккаунта Google нет активной лицензии Gemini Code Assist / Antigravity (#3501), либо не привязан проект Google Cloud. Проверьте подписку на https://one.google.com или подключите другой Google аккаунт.'
|
|
125
|
+
}
|
|
126
|
+
return ''
|
|
127
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -42,3 +42,25 @@ export function queryOf(req) {
|
|
|
42
42
|
return new URLSearchParams()
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch with connect/overall timeout using AbortSignal.any.
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs = 30000 } = {}) {
|
|
50
|
+
const impl = fetchImpl || fetch
|
|
51
|
+
if (!timeoutMs || timeoutMs <= 0) return impl(url, init)
|
|
52
|
+
const controller = new AbortController()
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
|
|
55
|
+
}, timeoutMs)
|
|
56
|
+
|
|
57
|
+
const signal = init.signal
|
|
58
|
+
? AbortSignal.any([init.signal, controller.signal])
|
|
59
|
+
: controller.signal
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await impl(url, { ...init, signal })
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
}
|
|
66
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -22,13 +22,14 @@ import { createResetCreditService } from './reset-credits.js'
|
|
|
22
22
|
import { maskEmail, maskLabel, maskText } from './mask.js'
|
|
23
23
|
import { proxyFetch, pickFetch } from './proxy.js'
|
|
24
24
|
import { HistoryStore } from './history.js'
|
|
25
|
+
import { ProactiveTokenRefreshDaemon } from './proactive-refresh.js'
|
|
25
26
|
import {
|
|
26
27
|
inspectGoogleAccount,
|
|
27
28
|
antigravityMetadata,
|
|
28
29
|
antigravityIdentityHeaders,
|
|
29
30
|
} from './code-assist.js'
|
|
30
31
|
|
|
31
|
-
export const name = 'dsh-subscriptions'
|
|
32
|
+
export const name = '@goodandready/dsh-subscriptions'
|
|
32
33
|
export const inject = ['llm', 'credentials', 'webServer', 'settings']
|
|
33
34
|
|
|
34
35
|
const NS = 'dsh-subscriptions'
|
|
@@ -109,6 +110,11 @@ export const Config = z.object({
|
|
|
109
110
|
|
|
110
111
|
function publicConfig(cfg) {
|
|
111
112
|
const clone = structuredClone(cfg)
|
|
113
|
+
// #242: redact all *ClientSecret fields before exposing config via GET /config.
|
|
114
|
+
// Secrets must never leave the server; the UI does not need them.
|
|
115
|
+
for (const key of Object.keys(clone)) {
|
|
116
|
+
if (key.endsWith('ClientSecret')) clone[key] = clone[key] ? '••••••' : ''
|
|
117
|
+
}
|
|
112
118
|
return clone
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -183,6 +189,43 @@ export function apply(ctx, config) {
|
|
|
183
189
|
const history = new HistoryStore()
|
|
184
190
|
const recordHistory = (entry) => history.add(entry)
|
|
185
191
|
|
|
192
|
+
// Proactive token refresh daemon: runs periodically and refreshes tokens nearing expiration
|
|
193
|
+
const refreshDaemon = new ProactiveTokenRefreshDaemon({
|
|
194
|
+
refreshLeadMs: 15 * 60 * 1000,
|
|
195
|
+
checkIntervalMs: 60 * 1000,
|
|
196
|
+
})
|
|
197
|
+
refreshDaemon.start(
|
|
198
|
+
async () => {
|
|
199
|
+
const slots = normalizeSlots(live().slots)
|
|
200
|
+
const accounts = []
|
|
201
|
+
for (const slot of slots) {
|
|
202
|
+
try {
|
|
203
|
+
const raw = await store.resolveRaw(slot.ref)
|
|
204
|
+
if (!raw) continue
|
|
205
|
+
const blob = parseBlob(raw)
|
|
206
|
+
if (blob && blob.refreshToken && blob.expiresAt) {
|
|
207
|
+
accounts.push({
|
|
208
|
+
ref: slot.ref,
|
|
209
|
+
provider: slot.provider,
|
|
210
|
+
refreshToken: blob.refreshToken,
|
|
211
|
+
expiresAt: blob.expiresAt,
|
|
212
|
+
blob,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
return accounts
|
|
218
|
+
},
|
|
219
|
+
async (acc) => {
|
|
220
|
+
try {
|
|
221
|
+
await store.ensureFresh(acc.provider, acc.blob, acc.ref)
|
|
222
|
+
} catch (e) {
|
|
223
|
+
try { ctx.log && ctx.log.warn && ctx.log.warn('[dsh-subscriptions] proactive refresh failed for ' + acc.ref + ': ' + String(e && e.message || e)) } catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
ctx.effect(() => () => refreshDaemon.stop(), 'dsh-subscriptions: proactive token refresh daemon')
|
|
228
|
+
|
|
186
229
|
// #85: host-only reset credit service for Codex accounts.
|
|
187
230
|
const resetCredits = createResetCreditService({ loadBlob: (ref) => store.loadBlob(ref) })
|
|
188
231
|
function refForSlot(provider, index) {
|
|
@@ -236,6 +279,7 @@ export function apply(ctx, config) {
|
|
|
236
279
|
cooldownMs: () => live().cooldownMs,
|
|
237
280
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
238
281
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
282
|
+
rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
|
|
239
283
|
recordSuccess: (ref) => store.recordSuccess(ref),
|
|
240
284
|
getHealth: (ref) => store.getHealth(ref),
|
|
241
285
|
recordSwitch: (ref) => store.recordSwitch(ref),
|
|
@@ -301,6 +345,7 @@ export function apply(ctx, config) {
|
|
|
301
345
|
cooldownMs: () => live().cooldownMs,
|
|
302
346
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
303
347
|
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
348
|
+
rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
|
|
304
349
|
rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
|
|
305
350
|
getQuota: (ref) => store.getQuota(ref),
|
|
306
351
|
refreshUsage: (provider) => store.refreshUsage(provider),
|
package/lib/rotate.js
CHANGED
|
@@ -5,6 +5,7 @@ const SWITCH_CODES = new Set([
|
|
|
5
5
|
'RATE_LIMIT',
|
|
6
6
|
'QUOTA',
|
|
7
7
|
'QUOTA_EXCEEDED',
|
|
8
|
+
'LICENSE_REQUIRED',
|
|
8
9
|
])
|
|
9
10
|
|
|
10
11
|
export function isSwitchableError(err) {
|
|
@@ -12,7 +13,7 @@ export function isSwitchableError(err) {
|
|
|
12
13
|
const code = err.code || (err.failure && err.failure.code)
|
|
13
14
|
if (SWITCH_CODES.has(code)) return true
|
|
14
15
|
const status = err.status || err.statusCode
|
|
15
|
-
return status === 429 || status === 500 || status === 502 || status === 503
|
|
16
|
+
return status === 401 || status === 403 || status === 429 || status === 500 || status === 502 || status === 503
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function modelFamily(provider, model) {
|
package/lib/sse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export async function* iterateSse(body) {
|
|
1
|
+
export async function* iterateSse(body, { idleTimeoutMs = 60000 } = {}) {
|
|
2
2
|
const reader = body && typeof body.getReader === 'function' ? body.getReader() : null
|
|
3
3
|
const decoder = new TextDecoder()
|
|
4
4
|
let buffer = ''
|
|
@@ -18,9 +18,27 @@ export async function* iterateSse(body) {
|
|
|
18
18
|
yield data
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
async function readWithTimeout(r) {
|
|
23
|
+
if (!idleTimeoutMs || idleTimeoutMs <= 0) return r.read()
|
|
24
|
+
let timer
|
|
25
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
const err = new Error(`stream idle timeout: no data received for ${Math.round(idleTimeoutMs / 1000)}s`)
|
|
28
|
+
err.code = 'TIMEOUT'
|
|
29
|
+
reject(err)
|
|
30
|
+
}, idleTimeoutMs)
|
|
31
|
+
})
|
|
32
|
+
try {
|
|
33
|
+
return await Promise.race([r.read(), timeoutPromise])
|
|
34
|
+
} finally {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
if (reader) {
|
|
22
40
|
while (true) {
|
|
23
|
-
const { done, value } = await reader
|
|
41
|
+
const { done, value } = await readWithTimeout(reader)
|
|
24
42
|
if (done) break
|
|
25
43
|
yield* fromText(decoder.decode(value, { stream: true }))
|
|
26
44
|
}
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pickAccount, markCooldown, isSwitchableError, modelFamily } from './rotate.js'
|
|
2
|
-
import { putInQuarantine, REASON_RATE_LIMIT } from './quarantine.js'
|
|
2
|
+
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED } from './quarantine.js'
|
|
3
3
|
|
|
4
4
|
export async function* streamWithRotation({
|
|
5
5
|
accounts,
|
|
@@ -38,9 +38,8 @@ export async function* streamWithRotation({
|
|
|
38
38
|
|
|
39
39
|
tried.add(account.ref || account.id)
|
|
40
40
|
|
|
41
|
+
let firstChunkDelivered = false
|
|
41
42
|
try {
|
|
42
|
-
// In-flight seamless failover (#166): stream generator execution
|
|
43
|
-
let firstChunkDelivered = false
|
|
44
43
|
for await (const chunk of streamOnce(account, options)) {
|
|
45
44
|
firstChunkDelivered = true
|
|
46
45
|
yield chunk
|
|
@@ -48,6 +47,9 @@ export async function* streamWithRotation({
|
|
|
48
47
|
return
|
|
49
48
|
} catch (err) {
|
|
50
49
|
lastError = err
|
|
50
|
+
// If chunks were already yielded to the caller, never rotate mid-stream
|
|
51
|
+
// as that would repeat or scramble generated tokens.
|
|
52
|
+
if (firstChunkDelivered) throw err
|
|
51
53
|
if (!isSwitchableError(err)) throw err
|
|
52
54
|
|
|
53
55
|
// Move slot to cooldown and quarantine (#172)
|
|
@@ -55,7 +57,15 @@ export async function* streamWithRotation({
|
|
|
55
57
|
account.cooldownUntil = cooled.cooldownUntil
|
|
56
58
|
if (cooled.cooldownFamilies) account.cooldownFamilies = cooled.cooldownFamilies
|
|
57
59
|
|
|
58
|
-
const
|
|
60
|
+
const status = Number(err && (err.status || err.statusCode) || 0)
|
|
61
|
+
const code = String(err && err.code || '')
|
|
62
|
+
const reason = (status === 401 || code === 'AUTH' || code === 'TOKEN_REVOKED')
|
|
63
|
+
? REASON_REVOKED
|
|
64
|
+
: (status === 403 || code === 'HARD_LIMIT' || code === 'LICENSE_REQUIRED')
|
|
65
|
+
? REASON_HARD_LIMIT
|
|
66
|
+
: REASON_RATE_LIMIT
|
|
67
|
+
|
|
68
|
+
const quarantined = putInQuarantine(account, reason, nowMs())
|
|
59
69
|
account.quarantineUntil = quarantined.quarantineUntil
|
|
60
70
|
account.quarantineReason = quarantined.quarantineReason
|
|
61
71
|
|
|
@@ -84,6 +84,7 @@ async function withProject(blob, fetchImpl, saveBlob) {
|
|
|
84
84
|
projectId: found.projectId || blob.projectId || '',
|
|
85
85
|
paidTierId: found.paidTierId || blob.paidTierId || '',
|
|
86
86
|
paidTierName: found.paidTierName || blob.paidTierName || '',
|
|
87
|
+
accountNotice: found.accountNotice || blob.accountNotice || '',
|
|
87
88
|
sessionId: blob.sessionId || randomUUID(),
|
|
88
89
|
}
|
|
89
90
|
if (saveBlob) await saveBlob(next)
|
package/lib/vendors/cursor.js
CHANGED
|
@@ -11,6 +11,13 @@ export const CURSOR_AGENT_URL = 'https://agentn.us.api5.cursor.sh/agent.v1.Agent
|
|
|
11
11
|
export const CURSOR_CLIENT_VERSION = 'cli-2026.05.01-eea359f'
|
|
12
12
|
|
|
13
13
|
export const CURSOR_MODELS = [
|
|
14
|
+
{
|
|
15
|
+
id: 'composer-2.5',
|
|
16
|
+
name: 'Composer 2.5 Fast',
|
|
17
|
+
contextWindow: 200000,
|
|
18
|
+
maxTokens: 64000,
|
|
19
|
+
inputModalities: ['text', 'image'],
|
|
20
|
+
},
|
|
14
21
|
{
|
|
15
22
|
id: 'composer-2',
|
|
16
23
|
name: 'Composer 2',
|
|
@@ -27,24 +34,89 @@ export const CURSOR_MODELS = [
|
|
|
27
34
|
inputModalities: ['text', 'image'],
|
|
28
35
|
},
|
|
29
36
|
{
|
|
30
|
-
id: 'claude-sonnet
|
|
31
|
-
name: 'Claude Sonnet
|
|
37
|
+
id: 'claude-3.7-sonnet',
|
|
38
|
+
name: 'Claude 3.7 Sonnet (Cursor)',
|
|
32
39
|
contextWindow: 200000,
|
|
33
40
|
maxTokens: 64000,
|
|
34
41
|
inputModalities: ['text', 'image'],
|
|
35
42
|
},
|
|
36
43
|
{
|
|
37
|
-
id: '
|
|
38
|
-
name: '
|
|
44
|
+
id: 'claude-3.7-sonnet-thinking',
|
|
45
|
+
name: 'Claude 3.7 Sonnet Thinking (Cursor)',
|
|
39
46
|
contextWindow: 200000,
|
|
40
|
-
maxTokens:
|
|
41
|
-
inputModalities: ['text'],
|
|
47
|
+
maxTokens: 64000,
|
|
48
|
+
inputModalities: ['text', 'image'],
|
|
49
|
+
reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'medium', name: 'Medium' }, { id: 'high', name: 'High' }, { id: 'max', name: 'Max' }] },
|
|
42
50
|
},
|
|
43
51
|
{
|
|
44
|
-
id: '
|
|
45
|
-
name: '
|
|
52
|
+
id: 'claude-3.5-sonnet',
|
|
53
|
+
name: 'Claude 3.5 Sonnet (Cursor)',
|
|
46
54
|
contextWindow: 200000,
|
|
47
55
|
maxTokens: 64000,
|
|
56
|
+
inputModalities: ['text', 'image'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'claude-3.5-haiku',
|
|
60
|
+
name: 'Claude 3.5 Haiku (Cursor)',
|
|
61
|
+
contextWindow: 200000,
|
|
62
|
+
maxTokens: 8192,
|
|
63
|
+
inputModalities: ['text'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'gpt-4o',
|
|
67
|
+
name: 'GPT-4o (Cursor)',
|
|
68
|
+
contextWindow: 128000,
|
|
69
|
+
maxTokens: 16384,
|
|
70
|
+
inputModalities: ['text', 'image'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'gpt-4o-mini',
|
|
74
|
+
name: 'GPT-4o Mini (Cursor)',
|
|
75
|
+
contextWindow: 128000,
|
|
76
|
+
maxTokens: 16384,
|
|
77
|
+
inputModalities: ['text'],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'o3-mini',
|
|
81
|
+
name: 'o3-mini (Cursor)',
|
|
82
|
+
contextWindow: 200000,
|
|
83
|
+
maxTokens: 100000,
|
|
84
|
+
inputModalities: ['text'],
|
|
85
|
+
reasoning: { efforts: [{ id: 'low', name: 'Low' }, { id: 'medium', name: 'Medium' }, { id: 'high', name: 'High' }] },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'o1',
|
|
89
|
+
name: 'o1 (Cursor)',
|
|
90
|
+
contextWindow: 200000,
|
|
91
|
+
maxTokens: 100000,
|
|
92
|
+
inputModalities: ['text', 'image'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'deepseek-r1',
|
|
96
|
+
name: 'DeepSeek R1 (Cursor)',
|
|
97
|
+
contextWindow: 64000,
|
|
98
|
+
maxTokens: 8192,
|
|
99
|
+
inputModalities: ['text'],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'deepseek-v3',
|
|
103
|
+
name: 'DeepSeek V3 (Cursor)',
|
|
104
|
+
contextWindow: 64000,
|
|
105
|
+
maxTokens: 8192,
|
|
106
|
+
inputModalities: ['text'],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'gemini-2.0-flash',
|
|
110
|
+
name: 'Gemini 2.0 Flash (Cursor)',
|
|
111
|
+
contextWindow: 1000000,
|
|
112
|
+
maxTokens: 8192,
|
|
113
|
+
inputModalities: ['text', 'image'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'cursor-small',
|
|
117
|
+
name: 'Cursor Small',
|
|
118
|
+
contextWindow: 32000,
|
|
119
|
+
maxTokens: 4096,
|
|
48
120
|
inputModalities: ['text'],
|
|
49
121
|
},
|
|
50
122
|
]
|
package/lib/wire.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { iterateSse, jsonSse } from './sse.js'
|
|
2
|
-
import { validationFromHttpError, validationRequiredError, googleRateLimitMessage } from './google-validation.js'
|
|
2
|
+
import { validationFromHttpError, validationRequiredError, googleRateLimitMessage, googleLicenseMessage } from './google-validation.js'
|
|
3
3
|
|
|
4
4
|
export function httpError(status, bodyText, code) {
|
|
5
5
|
const snippet = String(bodyText || '').slice(0, 400)
|
|
@@ -21,6 +21,12 @@ export function httpError(status, bodyText, code) {
|
|
|
21
21
|
export function throwHttpError(status, bodyText) {
|
|
22
22
|
const validation = validationFromHttpError(status, bodyText)
|
|
23
23
|
if (validation) throw validationRequiredError(validation)
|
|
24
|
+
const license = googleLicenseMessage(status, bodyText)
|
|
25
|
+
if (license) {
|
|
26
|
+
const err = httpError(status, bodyText, 'LICENSE_REQUIRED')
|
|
27
|
+
err.message = `${license} (${err.message})`
|
|
28
|
+
throw err
|
|
29
|
+
}
|
|
24
30
|
const hint = googleRateLimitMessage(status, bodyText)
|
|
25
31
|
if (hint) {
|
|
26
32
|
const err = httpError(status, bodyText, status === 429 ? 'RATE_LIMIT' : undefined)
|
|
@@ -181,12 +187,14 @@ export async function* googleStream(body) {
|
|
|
181
187
|
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
182
188
|
}
|
|
183
189
|
|
|
184
|
-
export async function formTokenRequest(url, params, fetchImpl, headers) {
|
|
190
|
+
export async function formTokenRequest(url, params, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
|
|
185
191
|
const impl = fetchImpl || fetch
|
|
192
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
186
193
|
const res = await impl(url, {
|
|
187
194
|
method: 'POST',
|
|
188
195
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', ...(headers || {}) },
|
|
189
196
|
body: new URLSearchParams(params),
|
|
197
|
+
signal: timeoutSignal,
|
|
190
198
|
})
|
|
191
199
|
return readJson(res)
|
|
192
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.25",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "node --test test/*.test.mjs"
|
|
38
|
+
"test": "eslint lib/ && node --test test/*.test.mjs"
|
|
39
39
|
},
|
|
40
40
|
"dsh": {
|
|
41
41
|
"bundle": {
|