@goodandready/dsh-subscriptions 0.6.12 → 0.6.13

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.
Files changed (2) hide show
  1. package/lib/index.js +169 -15
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ import { createSubscriptionsService } from './subscriptions.js'
20
20
  import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
21
21
  import { quotaSnapshot } from './ratelimit.js'
22
22
  import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
23
+ import { credentialRef } from '@deepseek-ai/dsh-credentials'
23
24
  import { startLoopback } from './loopback.js'
24
25
  import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
25
26
  import { createResetCreditService } from './reset-credits.js'
@@ -57,6 +58,11 @@ export function apply(ctx, config) {
57
58
  const scope = sctx.settings.register(NS, Config, { base: config })
58
59
  settingsApi = scope
59
60
  getConfig = () => scope.get() ?? config
61
+ Promise.resolve().then(() => {
62
+ reconcileSlots().catch((err) => {
63
+ if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
64
+ })
65
+ })
60
66
  sctx.effect(() => () => {
61
67
  settingsApi = undefined
62
68
  getConfig = () => config
@@ -243,23 +249,126 @@ export function apply(ctx, config) {
243
249
  })
244
250
 
245
251
  let handle
252
+ let registeredByUs = new Set()
253
+ function disposeAdapterHandle(h) {
254
+ if (!h) return
255
+ try {
256
+ if (typeof h === 'function') h()
257
+ else if (typeof h.dispose === 'function') h.dispose()
258
+ } catch (err) {
259
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] dispose handle error: ' + (err && err.message || err))
260
+ }
261
+ }
262
+
246
263
  async function syncAdapter() {
247
- const providers = await store.loggedInProviders()
248
- if (!handle && providers.length) {
249
- handle = ctx.llm.registerAdapter(providers, adapter)
264
+ const desired = await store.loggedInProviders()
265
+ const globalProviders = ctx.llm && typeof ctx.llm.listProviders === 'function'
266
+ ? ctx.llm.listProviders().map((p) => p.id)
267
+ : []
268
+ const globalSet = new Set(globalProviders)
269
+
270
+ const claimable = desired.filter((p) => !globalSet.has(p) || registeredByUs.has(p))
271
+ const skipped = desired.filter((p) => globalSet.has(p) && !registeredByUs.has(p))
272
+
273
+ if (skipped.length && logger && logger.info) {
274
+ logger.info(`[dsh-subscriptions] syncAdapter skipping already-bound providers: ${skipped.join(', ')}`)
275
+ }
276
+
277
+ if (!handle && claimable.length) {
278
+ handle = ctx.llm.registerAdapter(claimable, adapter)
279
+ registeredByUs = new Set(claimable)
250
280
  return
251
281
  }
252
- if (!handle) return
253
- if (!providers.length) {
254
- try { handle() } catch { /* already gone */ }
282
+ if (!handle) {
283
+ registeredByUs.clear()
284
+ return
285
+ }
286
+ if (!claimable.length) {
287
+ disposeAdapterHandle(handle)
255
288
  handle = undefined
289
+ registeredByUs.clear()
256
290
  return
257
291
  }
292
+ if (typeof handle.replace === 'function') {
293
+ try {
294
+ handle.replace(claimable)
295
+ registeredByUs = new Set(claimable)
296
+ return
297
+ } catch (err) {
298
+ if (logger && logger.warn) logger.warn('[dsh-subscriptions] handle.replace failed: ' + (err && err.message || err))
299
+ disposeAdapterHandle(handle)
300
+ handle = undefined
301
+ registeredByUs.clear()
302
+ }
303
+ } else {
304
+ disposeAdapterHandle(handle)
305
+ handle = undefined
306
+ registeredByUs.clear()
307
+ }
308
+
309
+ const currentGlobal = new Set((ctx.llm && typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []).map((p) => p.id))
310
+ const reClaimable = desired.filter((p) => !currentGlobal.has(p))
311
+ if (reClaimable.length) {
312
+ handle = ctx.llm.registerAdapter(reClaimable, adapter)
313
+ registeredByUs = new Set(reClaimable)
314
+ }
315
+ }
316
+
317
+ async function reconcileSlots() {
318
+ const api = settingsApi
319
+ if (!api || typeof api.replace !== 'function') return
320
+ const creds = ctx.credentials
321
+ if (!creds || typeof creds.describe !== 'function') return
322
+
258
323
  try {
259
- handle.replace(providers)
260
- } catch {
261
- try { handle() } catch { /* disposed */ }
262
- handle = ctx.llm.registerAdapter(providers, adapter)
324
+ const existing = normalizeSlots(live().slots)
325
+ const existingRefs = new Set(existing.map((s) => s.ref))
326
+ const candidateProviders = PROVIDERS.filter((p) => isProvider(p))
327
+ const additions = []
328
+
329
+ for (const provider of candidateProviders) {
330
+ for (let index = 1; index <= 10; index++) {
331
+ const ref = oauthRef(provider, index)
332
+ if (existingRefs.has(ref)) continue
333
+ let d
334
+ try {
335
+ d = await creds.describe(credentialRef(ref))
336
+ } catch (err) {
337
+ if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] describe ${ref} error: ` + (err && err.message || err))
338
+ continue
339
+ }
340
+ if (d && d.configured) {
341
+ let label = d.label || ''
342
+ try {
343
+ const blob = await store.loadBlob(ref).catch(() => null)
344
+ if (blob && (blob.email || blob.label)) {
345
+ label = blob.email || blob.label || label
346
+ }
347
+ } catch (loadErr) {
348
+ if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] loadBlob ${ref} error: ` + (loadErr && loadErr.message || loadErr))
349
+ }
350
+ additions.push({ provider, index, label })
351
+ }
352
+ }
353
+ }
354
+
355
+ if (!additions.length) return
356
+ if (logger && logger.info) {
357
+ logger.info(`[dsh-subscriptions reconcile] restoring orphaned vault slots: ${additions.map((a) => `${a.provider}#${a.index}`).join(', ')}`)
358
+ }
359
+ const currentSlots = Array.isArray(live().slots) ? live().slots : []
360
+ const newSlots = [...currentSlots, ...additions]
361
+ const parsed = Config({ ...live(), slots: stripLegacySlots(newSlots) })
362
+ await api.replace(parsed)
363
+ syncCustomVendors()
364
+ await syncAdapter()
365
+ refreshModels().catch((err) => {
366
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions reconcile] refreshModels error: ' + (err && err.message || err))
367
+ })
368
+ } catch (e) {
369
+ if (logger && logger.warn) {
370
+ logger.warn('[dsh-subscriptions reconcile] failed: ' + (e && e.message || e))
371
+ }
263
372
  }
264
373
  }
265
374
 
@@ -471,22 +580,67 @@ export function apply(ctx, config) {
471
580
  }
472
581
 
473
582
  ctx.effect(() => {
474
- syncAdapter().catch(() => { /* first paint */ })
475
- syncOllama().catch(() => { /* first paint */ })
583
+ syncAdapter().catch((err) => {
584
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncAdapter initial error: ' + (err && err.message || err))
585
+ })
586
+ syncOllama().catch((err) => {
587
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncOllama initial error: ' + (err && err.message || err))
588
+ })
589
+ reconcileSlots().catch((err) => {
590
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] reconcileSlots effect error: ' + (err && err.message || err))
591
+ })
592
+
593
+ let retries = 0
594
+ let retryTimer
595
+ const scheduleRetry = () => {
596
+ if (retries >= 30) return
597
+ retryTimer = setTimeout(retrySync, 1000)
598
+ if (retryTimer && typeof retryTimer.unref === 'function') retryTimer.unref()
599
+ }
600
+ const retrySync = async () => {
601
+ try {
602
+ const loggedIn = await store.loggedInProviders()
603
+ if (!loggedIn.length) return
604
+ const registered = ctx.llm && typeof ctx.llm.listProviders === 'function'
605
+ ? ctx.llm.listProviders().map((p) => p.id)
606
+ : []
607
+ const regSet = new Set(registered)
608
+ const pending = loggedIn.filter((p) => !regSet.has(p))
609
+ if (pending.length === 0) return
610
+ retries += 1
611
+ await syncAdapter()
612
+ if (retries < 30) {
613
+ scheduleRetry()
614
+ }
615
+ } catch (err) {
616
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] retrySync error: ' + (err && err.message || err))
617
+ if (retries < 30) {
618
+ scheduleRetry()
619
+ }
620
+ }
621
+ }
622
+ scheduleRetry()
623
+
476
624
  // #75: eager usage refresh at startup so the windows (5h/7d) land in the blob immediately
477
625
  // and the active subscription chip shows 5h/7d/... right away instead of waiting for probeInterval.
478
626
  const eager = async () => {
479
627
  try {
480
628
  for (const slot of normalizeSlots(live().slots)) {
481
- store.refreshUsage(slot.provider).catch(() => {})
629
+ store.refreshUsage(slot.provider).catch((err) => {
630
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
631
+ })
482
632
  }
483
- } catch (err) { /* best-effort */ }
633
+ } catch (err) {
634
+ if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager refresh error: ' + (err && err.message || err))
635
+ }
484
636
  }
485
637
  eager()
486
638
  return () => {
639
+ if (retryTimer) clearTimeout(retryTimer)
487
640
  if (handle) {
488
- try { handle() } catch { /* ignore */ }
641
+ disposeAdapterHandle(handle)
489
642
  handle = undefined
643
+ registeredByUs.clear()
490
644
  }
491
645
  }
492
646
  }, 'dsh-subscriptions: llm adapter')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
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",