@goodandready/dsh-subscriptions 0.7.33 → 0.7.34
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 +8 -1
- package/lib/adapter-manager.js +38 -0
- package/lib/adapter.js +18 -1
- package/lib/client.js +4 -4
- package/lib/config-schema.js +10 -0
- package/lib/http.js +12 -0
- package/lib/rotate.js +9 -1
- package/lib/stream-rotate.js +3 -0
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -130,6 +130,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
130
130
|
const notifyThresholds = new Map()
|
|
131
131
|
const health = new Map()
|
|
132
132
|
const requestCounts = new Map()
|
|
133
|
+
const lastUsedAt = new Map()
|
|
133
134
|
const windows = new Map()
|
|
134
135
|
const usageFetched = new Map()
|
|
135
136
|
const pings = new Map()
|
|
@@ -278,6 +279,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
278
279
|
// Counters and health must not outlive the account, otherwise a
|
|
279
280
|
// re-added slot inherits stale rotation statistics.
|
|
280
281
|
requestCounts.delete(ref)
|
|
282
|
+
lastUsedAt.delete(ref)
|
|
281
283
|
health.delete(ref)
|
|
282
284
|
windows.delete(ref)
|
|
283
285
|
}
|
|
@@ -427,6 +429,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
427
429
|
isDefault: !!slot.isDefault,
|
|
428
430
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
429
431
|
credentialError: info.credentialError || null,
|
|
432
|
+
lastUsedAt: lastUsedAt.get(slot.ref) || 0,
|
|
430
433
|
duplicateOf,
|
|
431
434
|
})
|
|
432
435
|
}
|
|
@@ -679,6 +682,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
679
682
|
recordExhaust(ref) { health.set(ref, recordExhaust(health.get(ref))) },
|
|
680
683
|
recordBroken(ref) { health.set(ref, recordBroken(health.get(ref))) },
|
|
681
684
|
recordSuccess(ref) {
|
|
685
|
+
if (ref) lastUsedAt.set(ref, Date.now())
|
|
682
686
|
// successful request clears the cooldown and puts the account back in rotation
|
|
683
687
|
cooldowns.delete(ref)
|
|
684
688
|
quarantines.delete(ref)
|
|
@@ -739,7 +743,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
739
743
|
quarantines.delete(ref)
|
|
740
744
|
saveQuarantinesToFile(quarantines)
|
|
741
745
|
},
|
|
742
|
-
rememberRequest(ref) {
|
|
746
|
+
rememberRequest(ref) {
|
|
747
|
+
requestCounts.set(ref, (requestCounts.get(ref) || 0) + 1)
|
|
748
|
+
if (ref) lastUsedAt.set(ref, Date.now())
|
|
749
|
+
},
|
|
743
750
|
getRequestCount(ref) { return requestCounts.get(ref) || 0 },
|
|
744
751
|
rememberPing(ref, { latencyMs, latencyGrade }) { pings.set(ref, { latencyMs, latencyGrade, at: Date.now() }) },
|
|
745
752
|
getPing(ref) { return pings.get(ref) || null },
|
package/lib/adapter-manager.js
CHANGED
|
@@ -48,6 +48,9 @@ export function createAdapterManager({
|
|
|
48
48
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
49
49
|
cooldownMs: () => live().cooldownMs,
|
|
50
50
|
autoPacing: () => live().autoPacing,
|
|
51
|
+
promptCacheTtlMs: () => Number(live().promptCacheTtlMinutes || 0) * 60 * 1000,
|
|
52
|
+
proactiveGuard: () => Boolean(live().proactiveGuard),
|
|
53
|
+
proactiveMinRemaining: () => Number(live().proactiveMinRemaining || 1),
|
|
51
54
|
drainBeforeReset: () => live().drainBeforeReset,
|
|
52
55
|
drainSafetyPercent: () => live().drainSafetyPercent,
|
|
53
56
|
switchAtRemaining: () => live().switchAtRemaining,
|
|
@@ -273,6 +276,26 @@ export function createAdapterManager({
|
|
|
273
276
|
}
|
|
274
277
|
}
|
|
275
278
|
|
|
279
|
+
async function selfHealCoolingAccounts() {
|
|
280
|
+
const providers = await store.loggedInProviders()
|
|
281
|
+
for (const provider of providers) {
|
|
282
|
+
const accounts = await store.listAccounts(provider).catch(() => [])
|
|
283
|
+
for (const account of accounts) {
|
|
284
|
+
if (!account || !account.cooldownUntil || Number(account.cooldownUntil) <= Date.now()) continue
|
|
285
|
+
try {
|
|
286
|
+
const blob = await store.loadBlob(account.ref)
|
|
287
|
+
if (!blob) continue
|
|
288
|
+
const vendor = getVendor(provider)
|
|
289
|
+
const cfg = vendorConfig(provider, live())
|
|
290
|
+
if (typeof vendor.check === 'function') await vendor.check(blob, cfg, fetch)
|
|
291
|
+
else if (typeof vendor.listModels === 'function') await vendor.listModels(blob, cfg, fetch)
|
|
292
|
+
else continue
|
|
293
|
+
store.clearCooldown(account.ref)
|
|
294
|
+
} catch { /* keep cooling on probe failure */ }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
276
299
|
function registerAdapterLifecycle() {
|
|
277
300
|
return ctx.effect(() => {
|
|
278
301
|
syncAdapter().catch((err) => {
|
|
@@ -333,6 +356,9 @@ export function createAdapterManager({
|
|
|
333
356
|
}
|
|
334
357
|
eager()
|
|
335
358
|
|
|
359
|
+
// #820: plugin network calls use a per-address connect floor.
|
|
360
|
+
process.env.DSH_CONNECT_ATTEMPT_TIMEOUT_MS = String(Number(live().connectAttemptTimeoutMs) || 1500)
|
|
361
|
+
|
|
336
362
|
// #799: refresh at startup and hourly. Per-ref snapshots younger than
|
|
337
363
|
// five minutes are skipped, and failures keep last-known-good.
|
|
338
364
|
refreshModels().catch((err) => {
|
|
@@ -345,10 +371,22 @@ export function createAdapterManager({
|
|
|
345
371
|
}, 60 * 60 * 1000)
|
|
346
372
|
if (modelTimer && typeof modelTimer.unref === 'function') modelTimer.unref()
|
|
347
373
|
|
|
374
|
+
let selfHealTimer
|
|
375
|
+
const selfHealMin = Number(live().selfHealIntervalMin) || 0
|
|
376
|
+
if (selfHealMin > 0) {
|
|
377
|
+
selfHealTimer = setInterval(() => {
|
|
378
|
+
selfHealCoolingAccounts().catch((err) => {
|
|
379
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] self-heal error: ' + (err && err.message || err))
|
|
380
|
+
})
|
|
381
|
+
}, selfHealMin * 60 * 1000)
|
|
382
|
+
if (typeof selfHealTimer.unref === 'function') selfHealTimer.unref()
|
|
383
|
+
}
|
|
384
|
+
|
|
348
385
|
return () => {
|
|
349
386
|
adapterDisposed = true
|
|
350
387
|
if (retryTimer) clearTimeout(retryTimer)
|
|
351
388
|
clearInterval(modelTimer)
|
|
389
|
+
if (selfHealTimer) clearInterval(selfHealTimer)
|
|
352
390
|
if (handle) {
|
|
353
391
|
disposeAdapterHandle(handle)
|
|
354
392
|
handle = undefined
|
package/lib/adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveEffectiveReasoning, getModelReasoningCapabilities } from './reasoning-effort.js'
|
|
2
2
|
import { inferModelCapabilities, isDeprecatedId, mergeAccountCatalogs, resolveCatalogSource } from './models-catalog.js'
|
|
3
|
+
import { replayStateKey, unwrapReplayState, wrapReplayState } from './routing-reliability.js'
|
|
3
4
|
import { fetchWithTimeout } from "./http.js"
|
|
4
5
|
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
5
6
|
import { displayName } from './refs.js'
|
|
@@ -206,6 +207,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
206
207
|
...(!poolEnabled ? { onlyRef: defaultRef || '__no_default_account__' } : {}),
|
|
207
208
|
...(defaultRef ? { defaultRef } : {}),
|
|
208
209
|
_decisionTrace: decisionTrace,
|
|
210
|
+
promptCacheTtlMs: typeof deps.promptCacheTtlMs === 'function' ? deps.promptCacheTtlMs() : 0,
|
|
211
|
+
proactiveGuard: typeof deps.proactiveGuard === 'function' ? deps.proactiveGuard() : false,
|
|
212
|
+
proactiveMinRemaining: typeof deps.proactiveMinRemaining === 'function' ? deps.proactiveMinRemaining() : 1,
|
|
209
213
|
autoPacing: effectiveAutoPacing,
|
|
210
214
|
drainBeforeReset: effectiveDrainBeforeReset,
|
|
211
215
|
drainSafetyPercent: effectiveDrainSafetyPercent,
|
|
@@ -390,10 +394,22 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
390
394
|
|
|
391
395
|
blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
392
396
|
if (typeof deps.rememberRequest === 'function') deps.rememberRequest(account.ref)
|
|
397
|
+
const replayKey = replayStateKey(provider, account.ref, opts && opts.model)
|
|
398
|
+
const safeOptions = {
|
|
399
|
+
...opts,
|
|
400
|
+
messages: (Array.isArray(opts.messages) ? opts.messages : []).map((message) => {
|
|
401
|
+
if (!message || !message.replayState) return message
|
|
402
|
+
const replay = unwrapReplayState(message.replayState, replayKey)
|
|
403
|
+
if (replay.matched) return { ...message, replayState: replay.state }
|
|
404
|
+
const copy = { ...message }
|
|
405
|
+
delete copy.replayState
|
|
406
|
+
return copy
|
|
407
|
+
}),
|
|
408
|
+
}
|
|
393
409
|
for await (const chunk of vendor.streamOnce({
|
|
394
410
|
blob,
|
|
395
411
|
accountId: account.id || account.ref,
|
|
396
|
-
options:
|
|
412
|
+
options: safeOptions,
|
|
397
413
|
fetchImpl: quotaFetch,
|
|
398
414
|
headers: attributionHeaders(),
|
|
399
415
|
config: deps.vendorConfig(provider),
|
|
@@ -407,6 +423,7 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
407
423
|
if (chunk.type === 'finish') {
|
|
408
424
|
sawFinish = true
|
|
409
425
|
finishReason = chunk.reason
|
|
426
|
+
if (chunk.replayState) chunk.replayState = wrapReplayState(chunk.replayState, replayKey)
|
|
410
427
|
} else if (chunk.type === 'usage' && chunk.usage) {
|
|
411
428
|
const u = toTokenUsage(chunk.usage)
|
|
412
429
|
if (u && typeof u.outputTokens === 'number' && u.outputTokens > 0) {
|
package/lib/client.js
CHANGED
|
@@ -80,7 +80,7 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
|
|
81
81
|
const en = {
|
|
82
82
|
"syncModels": "Sync Models",
|
|
83
|
-
"syncModelsTitle": "Discover and synchronize available models from
|
|
83
|
+
"syncModelsTitle": "Discover and synchronize available models from provider API",
|
|
84
84
|
"modelsSynced": "Models synchronized",
|
|
85
85
|
"recommendation": "Recommendation",
|
|
86
86
|
"notConnected": "Not connected",
|
|
@@ -89,9 +89,9 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
"usageFull": "Usage 100%",
|
|
90
90
|
"connected": "Connected",
|
|
91
91
|
"title": "Subscriptions",
|
|
92
|
-
"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
|
|
92
|
+
"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 provider page, paste the redirected URL or the code here.",
|
|
93
93
|
"useOrigin": "Use this Web UI origin as OAuth redirect_uri",
|
|
94
|
-
"useOriginHint": "Leave off unless you registered your own OAuth client for this origin.
|
|
94
|
+
"useOriginHint": "Leave off unless you registered your own OAuth client for this origin. Provider CLI clients usually require their published redirect URI plus the paste step.",
|
|
95
95
|
"closeModal": "Close",
|
|
96
96
|
"groupAuth": "Sign-in and redirect",
|
|
97
97
|
"groupPrivacy": "Privacy and model list",
|
|
@@ -152,7 +152,7 @@ window.__ModuleLoader__.load({
|
|
|
152
152
|
"smokeFail": "Smoke test failed",
|
|
153
153
|
"testConnection": "Test",
|
|
154
154
|
"testConnectionSuccess": "Connection verified",
|
|
155
|
-
"cascadingFallback": "Cross-
|
|
155
|
+
"cascadingFallback": "Cross-provider fallback",
|
|
156
156
|
"cascadingFallbackHint": "Automatically route queries to alternative providers when all accounts of a primary provider are exhausted.",
|
|
157
157
|
"cascadingFallbackOrder": "Fallback Provider Order",
|
|
158
158
|
"cascadingFallbackOrderHint": "Sequence of alternative providers to try when primary accounts are exhausted.",
|
package/lib/config-schema.js
CHANGED
|
@@ -181,6 +181,16 @@ export const Config = z.object({
|
|
|
181
181
|
.description('#510 In-app toast alerts for quarantine and quota threshold events.'),
|
|
182
182
|
autoPacing: z.boolean().volatile().default(true)
|
|
183
183
|
.description('#352 Predictive burn-rate load balancing between accounts.'),
|
|
184
|
+
promptCacheTtlMinutes: z.number().volatile().default(5)
|
|
185
|
+
.description('#810 Prompt-cache affinity TTL for session scoring. 0 disables cache affinity.'),
|
|
186
|
+
proactiveGuard: z.boolean().volatile().default(false)
|
|
187
|
+
.description('#792 Block an account before a request when its remaining quota is at the configured floor.'),
|
|
188
|
+
proactiveMinRemaining: z.number().volatile().default(1)
|
|
189
|
+
.description('#792 Remaining-quota floor used by the proactive guard.'),
|
|
190
|
+
selfHealIntervalMin: z.number().volatile().default(0)
|
|
191
|
+
.description('#792 Probe cooling accounts and release their cooldown on success. 0 disables.'),
|
|
192
|
+
connectAttemptTimeoutMs: z.number().volatile().default(1500)
|
|
193
|
+
.description('#820 Per-address connect timeout floor for plugin network calls; larger values are preserved.'),
|
|
184
194
|
drainBeforeReset: z.boolean().volatile().default(false)
|
|
185
195
|
.description('#485 Prioritize accounts with closest reset window to burn expiring quota first.'),
|
|
186
196
|
drainSafetyPercent: z.number().volatile().default(5)
|
package/lib/http.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto'
|
|
2
|
+
import { Agent } from 'undici'
|
|
3
|
+
import { effectiveConnectAttemptTimeout } from './routing-reliability.js'
|
|
2
4
|
|
|
3
5
|
export function writeJson(res, code, body) {
|
|
4
6
|
try {
|
|
@@ -341,6 +343,15 @@ export function isTimeoutError(err) {
|
|
|
341
343
|
|| String(err.message || '').toLowerCase().includes('operation was aborted')
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
const connectDispatchers = new Map()
|
|
347
|
+
function connectDispatcher() {
|
|
348
|
+
const timeout = effectiveConnectAttemptTimeout(process.env.DSH_CONNECT_ATTEMPT_TIMEOUT_MS)
|
|
349
|
+
if (!connectDispatchers.has(timeout)) {
|
|
350
|
+
connectDispatchers.set(timeout, new Agent({ connect: { timeout } }))
|
|
351
|
+
}
|
|
352
|
+
return connectDispatchers.get(timeout)
|
|
353
|
+
}
|
|
354
|
+
|
|
344
355
|
/**
|
|
345
356
|
* @param {any} fetchImpl
|
|
346
357
|
* @param {string} url
|
|
@@ -351,6 +362,7 @@ export async function fetchWithTimeout(fetchImpl, url, init = {}, { timeoutMs =
|
|
|
351
362
|
const safeHeaders = sanitizeRequestHeaders(url, init, { allowedOrigins })
|
|
352
363
|
init = { ...init, headers: safeHeaders }
|
|
353
364
|
const impl = fetchImpl || fetch
|
|
365
|
+
if (!init.dispatcher && impl === globalThis.fetch) init.dispatcher = connectDispatcher()
|
|
354
366
|
if (!timeoutMs || timeoutMs <= 0) return impl(url, init)
|
|
355
367
|
const controller = new AbortController()
|
|
356
368
|
let timer = setTimeout(() => {
|
package/lib/rotate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computePacingRisk, calculateBurnRatePerHour, calculateBurnUrgency } from "./forecast.js"
|
|
2
2
|
import { isQuarantined } from './quarantine.js'
|
|
3
3
|
import { getPinnedAccountRef, pinSession, touchSessionPin, unpinSession, isCacheAwareProvider } from './session-pin.js'
|
|
4
|
-
import { accountCatalogHasModel, proactiveQuotaDecision } from './routing-reliability.js'
|
|
4
|
+
import { accountCatalogHasModel, proactiveQuotaDecision, promptCacheAffinity } from './routing-reliability.js'
|
|
5
5
|
|
|
6
6
|
const SWITCH_CODES = new Set([
|
|
7
7
|
'RATE_LIMIT',
|
|
@@ -390,6 +390,13 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
390
390
|
if (Math.abs(aUrgency - bUrgency) > 0.01) return bUrgency - aUrgency
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// #810: keep session traffic on a warm prompt-cache account.
|
|
394
|
+
if (opts && Number(opts.promptCacheTtlMs) > 0) {
|
|
395
|
+
const aCache = promptCacheAffinity(a, now, opts.promptCacheTtlMs).score
|
|
396
|
+
const bCache = promptCacheAffinity(b, now, opts.promptCacheTtlMs).score
|
|
397
|
+
if (Math.abs(aCache - bCache) > 0.01) return bCache - aCache
|
|
398
|
+
}
|
|
399
|
+
|
|
393
400
|
// 0. Auto-Pacing: lower pacing risk preferred (#352)
|
|
394
401
|
const aRisk = Number(a.pacingRisk) || 0
|
|
395
402
|
const bRisk = Number(b.pacingRisk) || 0
|
|
@@ -449,6 +456,7 @@ export function pickAccountWithTrace(accounts, nowMs, opts) {
|
|
|
449
456
|
healthScore: chosen.healthScore ?? 100,
|
|
450
457
|
urgency: calculateBurnUrgency(getAccountRemainingPercent(chosen), chosen.quota && chosen.quota.resetAt, now, opts && opts.drainSafetyPercent != null ? Number(opts.drainSafetyPercent) : 5),
|
|
451
458
|
defaultAccount: Boolean(opts && opts.defaultRef && chosenRef === opts.defaultRef),
|
|
459
|
+
cacheAffinity: opts && Number(opts.promptCacheTtlMs) > 0 ? promptCacheAffinity(chosen, now, opts.promptCacheTtlMs) : null,
|
|
452
460
|
},
|
|
453
461
|
},
|
|
454
462
|
}
|
package/lib/stream-rotate.js
CHANGED
|
@@ -54,6 +54,9 @@ export async function* streamWithRotation({
|
|
|
54
54
|
model: options && options.model,
|
|
55
55
|
onlyRef: options && options.onlyRef,
|
|
56
56
|
defaultRef: options && options.defaultRef,
|
|
57
|
+
promptCacheTtlMs: options && options.promptCacheTtlMs,
|
|
58
|
+
proactiveGuard: options && options.proactiveGuard,
|
|
59
|
+
proactiveMinRemaining: options && options.proactiveMinRemaining,
|
|
57
60
|
family: modelFamily(options && options.provider, options && options.model),
|
|
58
61
|
sessionId: options && options.sessionId,
|
|
59
62
|
tag: options && options.tag,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.34",
|
|
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",
|