@goodandready/dsh-subscriptions 0.7.28 → 0.7.29
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/adapter-manager.js +32 -14
- package/lib/adapter.js +36 -8
- package/lib/client.js +41 -6
- package/lib/models-catalog.js +57 -0
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PROVIDERS, isProvider, oauthRef } from './refs.js'
|
|
|
5
5
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
6
6
|
import { getVendor } from './vendors/index.js'
|
|
7
7
|
import { Config, plainConfig } from './config-schema.js'
|
|
8
|
+
import { MODEL_CATALOG_TTL_MS, catalogSnapshotFresh } from './models-catalog.js'
|
|
8
9
|
|
|
9
10
|
export function createAdapterManager({
|
|
10
11
|
ctx,
|
|
@@ -38,6 +39,8 @@ export function createAdapterManager({
|
|
|
38
39
|
const adapter = new SubscriptionAdapter({
|
|
39
40
|
live: () => live(),
|
|
40
41
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
42
|
+
getDynamicModels: (ref) => store.getDynamicModels(ref),
|
|
43
|
+
rememberDynamicModels: (ref, models) => store.rememberDynamicModels(ref, models),
|
|
41
44
|
loadBlob: (ref) => store.loadBlob(ref),
|
|
42
45
|
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
43
46
|
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
@@ -243,26 +246,28 @@ export function createAdapterManager({
|
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
|
|
247
|
-
const providerModelCache = new Map()
|
|
249
|
+
const lastModelRefreshAt = new Map()
|
|
248
250
|
|
|
249
|
-
async function refreshModels() {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
251
|
+
async function refreshModels({ force = false } = {}) {
|
|
252
|
+
const now = Date.now()
|
|
253
|
+
const slots = normalizeSlots(live().slots)
|
|
254
|
+
for (const slot of slots) {
|
|
255
|
+
const last = lastModelRefreshAt.get(slot.ref)
|
|
256
|
+
if (!force && catalogSnapshotFresh(last, now, MODEL_CATALOG_TTL_MS)) continue
|
|
254
257
|
try {
|
|
255
|
-
const slot = normalizeSlots(live().slots).find((x) => x.provider === provider)
|
|
256
|
-
if (!slot) continue
|
|
257
258
|
const blob = await store.loadBlob(slot.ref).catch(() => null)
|
|
258
259
|
if (!blob) continue
|
|
259
|
-
const fresh = await store.ensureFresh(provider, blob, slot.ref)
|
|
260
|
-
const cfg = vendorConfig(provider, live())
|
|
261
|
-
const liveModels = await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => null)
|
|
260
|
+
const fresh = await store.ensureFresh(slot.provider, blob, slot.ref)
|
|
261
|
+
const cfg = vendorConfig(slot.provider, live())
|
|
262
|
+
const liveModels = await getVendor(slot.provider).listModels(fresh, cfg, fetch).catch(() => null)
|
|
263
|
+
// #828: empty/broken live catalogs must not erase last-known-good.
|
|
262
264
|
if (Array.isArray(liveModels) && liveModels.length) {
|
|
263
|
-
|
|
265
|
+
if (typeof store.rememberDynamicModels === 'function') {
|
|
266
|
+
store.rememberDynamicModels(slot.ref, liveModels)
|
|
267
|
+
}
|
|
268
|
+
lastModelRefreshAt.set(slot.ref, now)
|
|
264
269
|
}
|
|
265
|
-
} catch { /*
|
|
270
|
+
} catch { /* keep the previous snapshot */ }
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
@@ -326,9 +331,22 @@ export function createAdapterManager({
|
|
|
326
331
|
}
|
|
327
332
|
eager()
|
|
328
333
|
|
|
334
|
+
// #799: refresh at startup and hourly. Per-ref snapshots younger than
|
|
335
|
+
// five minutes are skipped, and failures keep last-known-good.
|
|
336
|
+
refreshModels().catch((err) => {
|
|
337
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] model refresh error: ' + (err && err.message || err))
|
|
338
|
+
})
|
|
339
|
+
const modelTimer = setInterval(() => {
|
|
340
|
+
refreshModels().catch((err) => {
|
|
341
|
+
if (logger && logger.debug) logger.debug('[dsh-subscriptions] hourly model refresh error: ' + (err && err.message || err))
|
|
342
|
+
})
|
|
343
|
+
}, 60 * 60 * 1000)
|
|
344
|
+
if (modelTimer && typeof modelTimer.unref === 'function') modelTimer.unref()
|
|
345
|
+
|
|
329
346
|
return () => {
|
|
330
347
|
adapterDisposed = true
|
|
331
348
|
if (retryTimer) clearTimeout(retryTimer)
|
|
349
|
+
clearInterval(modelTimer)
|
|
332
350
|
if (handle) {
|
|
333
351
|
disposeAdapterHandle(handle)
|
|
334
352
|
handle = undefined
|
package/lib/adapter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveEffectiveReasoning, getModelReasoningCapabilities } from './reasoning-effort.js'
|
|
2
|
-
import { inferModelCapabilities, isDeprecatedId } from './models-catalog.js'
|
|
2
|
+
import { inferModelCapabilities, isDeprecatedId, mergeAccountCatalogs, resolveCatalogSource } from './models-catalog.js'
|
|
3
3
|
import { fetchWithTimeout } from "./http.js"
|
|
4
4
|
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
5
5
|
import { displayName } from './refs.js'
|
|
@@ -54,10 +54,12 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
54
54
|
|
|
55
55
|
async listModels(provider) {
|
|
56
56
|
const accounts = await this.deps.listAccounts(provider)
|
|
57
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
58
|
+
const builtin = modelCatalog(provider, cfg ? cfg.models : undefined)
|
|
57
59
|
const targetAccount = accounts.find((a) => a.hasToken)
|
|
58
|
-
let
|
|
60
|
+
let targetLive = null
|
|
61
|
+
|
|
59
62
|
if (targetAccount) {
|
|
60
|
-
const cfg = this.deps.vendorConfig(provider)
|
|
61
63
|
const baseFetch = pickFetch(this.deps, targetAccount.ref)
|
|
62
64
|
const listModelsFetch = (url, init, opts) =>
|
|
63
65
|
fetchWithTimeout(baseFetch, url, init, { timeoutMs: 15000, ...(opts || {}) })
|
|
@@ -68,13 +70,39 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
68
70
|
targetAccount.ref,
|
|
69
71
|
)
|
|
70
72
|
const live = await getVendor(provider).listModels(blob, cfg, listModelsFetch)
|
|
71
|
-
if (Array.isArray(live) && live.length)
|
|
72
|
-
|
|
73
|
+
if (Array.isArray(live) && live.length) {
|
|
74
|
+
targetLive = live
|
|
75
|
+
if (typeof this.deps.rememberDynamicModels === 'function') {
|
|
76
|
+
this.deps.rememberDynamicModels(targetAccount.ref, live)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch { /* #828: keep last-known-good / built-in catalog */ }
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
|
|
82
|
+
// #797: build the picker union from each account's own catalog. Live data
|
|
83
|
+
// is only fetched for the primary account here; background refresh fills
|
|
84
|
+
// the rest without blocking the picker.
|
|
85
|
+
const entries = []
|
|
86
|
+
for (const account of accounts) {
|
|
87
|
+
if (!account || !account.hasToken) continue
|
|
88
|
+
const cached = typeof this.deps.getDynamicModels === 'function'
|
|
89
|
+
? this.deps.getDynamicModels(account.ref)
|
|
90
|
+
: null
|
|
91
|
+
const resolved = resolveCatalogSource({
|
|
92
|
+
live: account === targetAccount ? targetLive : null,
|
|
93
|
+
cached,
|
|
94
|
+
builtin,
|
|
95
|
+
})
|
|
96
|
+
entries.push({
|
|
97
|
+
ref: account.ref,
|
|
98
|
+
label: account.label || account.email || account.ref,
|
|
99
|
+
models: resolved.models,
|
|
100
|
+
source: resolved.source,
|
|
101
|
+
})
|
|
77
102
|
}
|
|
103
|
+
if (!entries.length) entries.push({ models: builtin, source: 'builtin' })
|
|
104
|
+
|
|
105
|
+
let models = mergeAccountCatalogs(entries)
|
|
78
106
|
// #94: optionally hide test/preview/beta/legacy ids from the picker.
|
|
79
107
|
if (typeof this.deps.hideDeprecatedModels === 'function' && this.deps.hideDeprecatedModels()) {
|
|
80
108
|
models = models.filter((m) => !isDeprecatedId(String((m && m.id) || '')))
|
package/lib/client.js
CHANGED
|
@@ -292,6 +292,9 @@ window.__ModuleLoader__.load({
|
|
|
292
292
|
"poolModelsNone": "None",
|
|
293
293
|
"poolModelsSelected": "selected",
|
|
294
294
|
"poolModelsHint": "Automatic allows every current and future model. None removes this account from model routing. An explicit list permits only checked model ids.",
|
|
295
|
+
"poolModelsSearch": "Search models…",
|
|
296
|
+
"poolModelsCount": "{shown} shown · {selected} selected · {total} total",
|
|
297
|
+
"poolModelsNoMatches": "No models match this search.",
|
|
295
298
|
"poolModelsNoCatalog": "No model catalog yet; sync models or use Automatic.",
|
|
296
299
|
"tags": "Tags",
|
|
297
300
|
"tagsPlaceholder": "team, prod",
|
|
@@ -675,6 +678,9 @@ window.__ModuleLoader__.load({
|
|
|
675
678
|
"poolModelsNone": "无",
|
|
676
679
|
"poolModelsSelected": "已选",
|
|
677
680
|
"poolModelsHint": "自动允许当前及未来全部模型。无会将该账户移出模型调度。显式列表只允许勾选的模型 ID。",
|
|
681
|
+
"poolModelsSearch": "搜索模型…",
|
|
682
|
+
"poolModelsCount": "显示 {shown} · 已选 {selected} · 共 {total}",
|
|
683
|
+
"poolModelsNoMatches": "没有匹配的模型。",
|
|
678
684
|
"poolModelsNoCatalog": "暂无模型目录;请同步模型或使用自动。",
|
|
679
685
|
"tags": "标签",
|
|
680
686
|
"tagsPlaceholder": "team, prod",
|
|
@@ -1047,6 +1053,7 @@ window.__ModuleLoader__.load({
|
|
|
1047
1053
|
const [modelQuery, setModelQuery] = React.useState('')
|
|
1048
1054
|
const [modelSort, setModelSort] = React.useState('name')
|
|
1049
1055
|
const [modelLimit, setModelLimit] = React.useState(40)
|
|
1056
|
+
const [poolModelQuery, setPoolModelQuery] = React.useState({})
|
|
1050
1057
|
const [paste, setPaste] = React.useState({})
|
|
1051
1058
|
const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
|
|
1052
1059
|
const [loginLinks, setLoginLinks] = React.useState({}) // #595 provider:index -> fallback auth URL when a popup is blocked
|
|
@@ -2753,6 +2760,14 @@ h('div', { className: 'dsub-row' },
|
|
|
2753
2760
|
.concat(poolModels || [])
|
|
2754
2761
|
.filter(Boolean)
|
|
2755
2762
|
)).sort()
|
|
2763
|
+
const poolQuery = String(poolModelQuery[key] || '').trim().toLowerCase()
|
|
2764
|
+
const visiblePoolModelIds = poolQuery
|
|
2765
|
+
? poolModelIds.filter((id) => String(id).toLowerCase().includes(poolQuery))
|
|
2766
|
+
: poolModelIds
|
|
2767
|
+
const selectedPoolCount = poolModels ? poolModels.length : poolModelIds.length
|
|
2768
|
+
const poolModeLabel = !poolModels
|
|
2769
|
+
? t('poolModelsAutomatic')
|
|
2770
|
+
: (poolModels.length ? (poolModels.length + ' ' + t('poolModelsSelected')) : t('poolModelsNone'))
|
|
2756
2771
|
const avatarSrc = safeExternalUrl(account.avatarUrl, true)
|
|
2757
2772
|
const avatarEl = (avatarSrc ? h('img', {
|
|
2758
2773
|
src: avatarSrc,
|
|
@@ -3063,9 +3078,24 @@ h('div', { className: 'dsub-row' },
|
|
|
3063
3078
|
),
|
|
3064
3079
|
h('details', { className: 'dsub-details' },
|
|
3065
3080
|
h('summary', { className: 'dsub-summary' },
|
|
3066
|
-
'🎯 ' + t('poolModelsTitle') + ' · ' +
|
|
3081
|
+
'🎯 ' + t('poolModelsTitle') + ' · ' + poolModeLabel
|
|
3067
3082
|
),
|
|
3068
3083
|
h('div', { className: 'dsub-sub' }, t('poolModelsHint')),
|
|
3084
|
+
h('div', { className: 'dsub-row' },
|
|
3085
|
+
h('input', {
|
|
3086
|
+
className: 'dsub-inp dsub-grow',
|
|
3087
|
+
value: poolModelQuery[key] || '',
|
|
3088
|
+
placeholder: t('poolModelsSearch'),
|
|
3089
|
+
'aria-label': t('poolModelsSearch'),
|
|
3090
|
+
onChange: (e) => setPoolModelQuery((q) => Object.assign({}, q, { [key]: e.target.value })),
|
|
3091
|
+
}),
|
|
3092
|
+
),
|
|
3093
|
+
h('div', { className: 'dsub-sub' },
|
|
3094
|
+
t('poolModelsCount')
|
|
3095
|
+
.replace('{shown}', String(visiblePoolModelIds.length))
|
|
3096
|
+
.replace('{selected}', String(selectedPoolCount))
|
|
3097
|
+
.replace('{total}', String(poolModelIds.length))
|
|
3098
|
+
),
|
|
3069
3099
|
h('div', { className: 'dsub-row' },
|
|
3070
3100
|
h('button', {
|
|
3071
3101
|
type: 'button', className: 'dsub-mini' + (!poolModels ? ' dsub-btnActive' : ''),
|
|
@@ -3073,15 +3103,20 @@ h('div', { className: 'dsub-row' },
|
|
|
3073
3103
|
}, t('poolModelsAutomatic')),
|
|
3074
3104
|
h('button', {
|
|
3075
3105
|
type: 'button', className: 'dsub-mini',
|
|
3076
|
-
onClick: () => updateSlot(row.slot.provider, row.slot.index, { poolModels:
|
|
3106
|
+
onClick: () => updateSlot(row.slot.provider, row.slot.index, { poolModels: visiblePoolModelIds.slice() }),
|
|
3077
3107
|
}, t('poolModelsAll')),
|
|
3078
3108
|
h('button', {
|
|
3079
3109
|
type: 'button', className: 'dsub-mini' + (poolModels && poolModels.length === 0 ? ' dsub-btnActive' : ''),
|
|
3080
|
-
onClick: () =>
|
|
3110
|
+
onClick: () => {
|
|
3111
|
+
const base = poolModels || poolModelIds
|
|
3112
|
+
updateSlot(row.slot.provider, row.slot.index, {
|
|
3113
|
+
poolModels: base.filter((id) => !visiblePoolModelIds.includes(id)),
|
|
3114
|
+
})
|
|
3115
|
+
},
|
|
3081
3116
|
}, t('poolModelsNone')),
|
|
3082
3117
|
),
|
|
3083
|
-
|
|
3084
|
-
|
|
3118
|
+
visiblePoolModelIds.length ? h('div', { className: 'dsub-row', style: { flexWrap: 'wrap', gap: 6 } },
|
|
3119
|
+
visiblePoolModelIds.map((modelId) => {
|
|
3085
3120
|
const checked = poolModels ? poolModels.includes(modelId) : true
|
|
3086
3121
|
return h('label', { key: modelId, className: 'dsub-ref-tag', title: modelId },
|
|
3087
3122
|
h('input', {
|
|
@@ -3103,7 +3138,7 @@ h('div', { className: 'dsub-row' },
|
|
|
3103
3138
|
h('code', null, modelId)
|
|
3104
3139
|
)
|
|
3105
3140
|
})
|
|
3106
|
-
) : h('div', { className: 'dsub-sub' }, t('poolModelsNoCatalog')),
|
|
3141
|
+
) : h('div', { className: 'dsub-sub' }, poolModelIds.length ? t('poolModelsNoMatches') : t('poolModelsNoCatalog')),
|
|
3107
3142
|
),
|
|
3108
3143
|
h('div', { className: 'dsub-row' },
|
|
3109
3144
|
h('span', { className: 'dsub-ref-tag' }, t('tags')),
|
package/lib/models-catalog.js
CHANGED
|
@@ -6,6 +6,63 @@ export function isDeprecatedId(id) {
|
|
|
6
6
|
import { getVendor } from './vendors/index.js'
|
|
7
7
|
import { getAccountRemainingPercent } from './rotate.js'
|
|
8
8
|
|
|
9
|
+
export const MODEL_CATALOG_TTL_MS = 5 * 60 * 1000
|
|
10
|
+
|
|
11
|
+
function catalogList(value) {
|
|
12
|
+
return Array.isArray(value) ? value.filter(Boolean) : []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pick the best available catalog without ever dropping a provider.
|
|
17
|
+
* live > last-known-good cache > built-in vendor catalog.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveCatalogSource({ live, cached, builtin } = {}) {
|
|
20
|
+
const liveList = catalogList(live)
|
|
21
|
+
if (liveList.length) return { models: liveList, source: 'live' }
|
|
22
|
+
const cachedList = catalogList(cached)
|
|
23
|
+
if (cachedList.length) return { models: cachedList, source: 'cached' }
|
|
24
|
+
return { models: catalogList(builtin), source: 'builtin' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function catalogSnapshotFresh(at, nowMs = Date.now(), ttlMs = MODEL_CATALOG_TTL_MS) {
|
|
28
|
+
if (!at || !Number.isFinite(Number(at))) return false
|
|
29
|
+
return Number(nowMs) - Number(at) < Number(ttlMs)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Union per-account catalogs, keeping the accounts that can see each model.
|
|
34
|
+
* Rows are deduplicated by id and never mutated.
|
|
35
|
+
*/
|
|
36
|
+
export function mergeAccountCatalogs(entries = []) {
|
|
37
|
+
const byId = new Map()
|
|
38
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
39
|
+
if (!entry) continue
|
|
40
|
+
for (const model of catalogList(entry.models)) {
|
|
41
|
+
const id = model && (model.id || model.slug || model.name)
|
|
42
|
+
if (!id) continue
|
|
43
|
+
const existing = byId.get(id)
|
|
44
|
+
if (existing) {
|
|
45
|
+
if (entry.ref && !existing.accounts.includes(entry.ref)) existing.accounts.push(entry.ref)
|
|
46
|
+
if (entry.label && !existing.accountLabels.includes(entry.label)) existing.accountLabels.push(entry.label)
|
|
47
|
+
if (entry.source && !existing.catalogSources.includes(entry.source)) existing.catalogSources.push(entry.source)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
byId.set(id, {
|
|
51
|
+
...model,
|
|
52
|
+
accounts: entry.ref ? [entry.ref] : [],
|
|
53
|
+
accountLabels: entry.label ? [entry.label] : [],
|
|
54
|
+
catalogSources: entry.source ? [entry.source] : [],
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Array.from(byId.values())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function modelAllowlistMode(poolModels) {
|
|
62
|
+
if (!Array.isArray(poolModels)) return 'all'
|
|
63
|
+
return poolModels.length ? 'selected' : 'none'
|
|
64
|
+
}
|
|
65
|
+
|
|
9
66
|
export const CAPABILITY_TAGS = {
|
|
10
67
|
VISION: 'vision',
|
|
11
68
|
TOOLS: 'tools',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.29",
|
|
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",
|