@goodandready/dsh-model-sync 0.1.0 → 0.2.0
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/README.md +11 -1
- package/lib/client.js +11 -6
- package/lib/http.js +26 -3
- package/lib/index.js +13 -2
- package/lib/synchronizer.js +67 -4
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ OAuth, subscription, and tool-only routes are intentionally outside the scope.
|
|
|
12
12
|
|
|
13
13
|
## Status
|
|
14
14
|
|
|
15
|
-
Runtime, scheduler, HTTP API, reconciliation, and Settings UI are implemented.
|
|
15
|
+
Runtime, scheduler, HTTP API, reconciliation, model allowlists, and Settings UI are implemented. The release workflow includes staging and production verification before publication.
|
|
16
16
|
|
|
17
17
|
## Development
|
|
18
18
|
|
|
@@ -33,3 +33,13 @@ Apply is explicit:
|
|
|
33
33
|
```json
|
|
34
34
|
{ "provider": "openai", "dryRun": false, "removeMissing": false }
|
|
35
35
|
```
|
|
36
|
+
|
|
37
|
+
Omit `provider` to discover all enabled API-key providers in one run. The Settings UI exposes this as **Refresh all**.
|
|
38
|
+
|
|
39
|
+
`POST /dsh-model-sync/selection` stores and applies a per-provider model allowlist:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "provider": "openai", "models": ["gpt-5", "gpt-5-mini"] }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
An empty `models` array means all models in the latest available catalog. The selected catalog is written to DSH's `llm-pi-ai` settings, so the standard DSH model picker refreshes from the resulting list.
|
package/lib/client.js
CHANGED
|
@@ -5,10 +5,10 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
const React = require('react')
|
|
7
7
|
const en = {
|
|
8
|
-
title: 'Model synchronization', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run',
|
|
8
|
+
title: 'Model synchronization', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', selectModels: 'Choose models', saveSelection: 'Save and apply selection', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.',
|
|
9
9
|
}
|
|
10
10
|
const ru = {
|
|
11
|
-
title: 'Синхронизация моделей', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск',
|
|
11
|
+
title: 'Синхронизация моделей', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.',
|
|
12
12
|
}
|
|
13
13
|
const API = '/dsh-model-sync'
|
|
14
14
|
function useLocale(ctx) {
|
|
@@ -17,16 +17,21 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
function ModelSyncSection({ ctx }) {
|
|
18
18
|
const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en
|
|
19
19
|
const [status, setStatus] = React.useState(null); const [provider, setProvider] = React.useState(''); const [dryRun, setDryRun] = React.useState(true); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState('')
|
|
20
|
+
const [selectionProvider, setSelectionProvider] = React.useState(''); const [selectionDraft, setSelectionDraft] = React.useState([])
|
|
20
21
|
const load = React.useCallback(() => { setError(''); return fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
|
|
21
22
|
React.useEffect(() => { load(); const timer = setInterval(load, 15000); return () => clearInterval(timer) }, [load])
|
|
22
|
-
const
|
|
23
|
-
const
|
|
23
|
+
const execute = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider || undefined, dryRun }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setStatus).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
24
|
+
const openSelection = (row) => { const available = row.availableModels ?? row.models ?? []; setSelectionProvider(row.provider); setSelectionDraft(row.selectedModels?.length ? row.selectedModels : available.map((model) => model.id)) }
|
|
25
|
+
const closeSelection = () => { setSelectionProvider(''); setSelectionDraft([]) }
|
|
26
|
+
const saveSelection = () => { const row = rows.find((item) => item.provider === selectionProvider); const available = row?.availableModels ?? row?.models ?? []; const models = selectionDraft.length === available.length ? [] : selectionDraft; setBusy(true); setError(''); fetch(`${API}/selection`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: selectionProvider, models }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); closeSelection() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
27
|
+
const rows = (status?.providers ?? []).filter((row) => row.configured); const selectedRow = rows.find((row) => row.provider === selectionProvider); const choices = selectedRow?.availableModels ?? selectedRow?.models ?? []; const button = (label, onClick) => React.createElement('button', { type: 'button', disabled: busy, onClick, style: { marginRight: 8 } }, label)
|
|
24
28
|
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 680 } },
|
|
25
29
|
React.createElement('p', null, t.desc),
|
|
26
30
|
React.createElement('div', null, React.createElement('select', { value: provider, onChange: (e) => setProvider(e.target.value) }, React.createElement('option', { value: '' }, t.all), rows.map((row) => React.createElement('option', { key: row.provider, value: row.provider }, row.provider))), React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: dryRun, onChange: (e) => setDryRun(e.target.checked) }), ` ${t.dryRun}`)),
|
|
27
|
-
React.createElement('div', null, button(t.refresh, load), button(dryRun ? t.run : t.apply,
|
|
31
|
+
React.createElement('div', null, button(t.refresh, load), button(t.refreshAll, () => execute(undefined)), button(dryRun ? t.run : t.apply, () => execute(provider || undefined))),
|
|
28
32
|
status?.lastRun ? React.createElement('small', null, `${t.lastRun}: ${new Date(status.lastRun.finishedAt).toLocaleString()}`) : null, status?.running || busy ? React.createElement('small', null, t.running) : null, error ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, error) : null, status === null ? React.createElement('small', null, t.loading) : null, rows.length === 0 && status ? React.createElement('small', null, t.noProviders) : null,
|
|
29
|
-
rows.map((row) => React.createElement('div', { key: row.provider }, `${row.provider}: ${row.configured ? t.ok : t.dormant} · ${row.models?.length ?? 0} models`)),
|
|
33
|
+
rows.map((row) => React.createElement('div', { key: row.provider, style: { display: 'flex', alignItems: 'center', gap: 8 } }, React.createElement('span', null, `${row.provider}: ${row.configured ? t.ok : t.dormant} · ${row.models?.length ?? 0} models`), button(t.selectModels, () => openSelection(row)))),
|
|
34
|
+
selectionProvider ? React.createElement('div', { style: { border: '1px solid var(--dsw-alias-border-primary)', padding: 12, borderRadius: 6 } }, React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`), React.createElement('div', { style: { margin: '8px 0' } }, choices.length === 0 ? React.createElement('small', null, t.noModels) : choices.map((model) => React.createElement('label', { key: model.id, style: { display: 'block', margin: '4px 0' } }, React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }), ` ${model.name ?? model.id} (${model.id})`))), React.createElement('small', null, t.pickerNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.saveSelection, saveSelection), button(t.cancel, closeSelection))) : null,
|
|
30
35
|
)
|
|
31
36
|
}
|
|
32
37
|
function apply(ctx) { ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-model-sync', order: 25, label: () => 'Model Sync' }, (props) => React.createElement(ModelSyncSection, { ...props, ctx }))) }
|
package/lib/http.js
CHANGED
|
@@ -40,6 +40,23 @@ export function normalizeRunRequest(body) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function normalizeSelectionRequest(body) {
|
|
44
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('request body must be an object')
|
|
45
|
+
if (typeof body.provider !== 'string' || !body.provider || body.provider.length > 128) throw new Error('provider must be a short string')
|
|
46
|
+
if (!Array.isArray(body.models) || body.models.length > 5000) throw new Error('models must be an array')
|
|
47
|
+
const models = []
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
for (const model of body.models) {
|
|
50
|
+
if (typeof model !== 'string' || model.length > 512) throw new Error('model ids must be short strings')
|
|
51
|
+
const id = model.trim()
|
|
52
|
+
if (id && !seen.has(id)) {
|
|
53
|
+
seen.add(id)
|
|
54
|
+
models.push(id)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { provider: body.provider, models }
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export function registerHttpApi(ctx, synchronizer) {
|
|
44
61
|
const makeHandler = (route) => async (req, res) => {
|
|
45
62
|
if (!trusted(req)) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin request rejected' } })
|
|
@@ -49,16 +66,22 @@ export function registerHttpApi(ctx, synchronizer) {
|
|
|
49
66
|
}
|
|
50
67
|
if (req.method !== 'POST') return json(res, 405, { error: { code: 'method', message: 'POST only' } })
|
|
51
68
|
let options
|
|
52
|
-
try { options = normalizeRunRequest(await readJson(req)) }
|
|
69
|
+
try { options = route === 'selection' ? normalizeSelectionRequest(await readJson(req)) : normalizeRunRequest(await readJson(req)) }
|
|
53
70
|
catch (error) { return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } }) }
|
|
54
|
-
try {
|
|
71
|
+
try {
|
|
72
|
+
const result = route === 'selection'
|
|
73
|
+
? await synchronizer.setModelSelection(options.provider, options.models)
|
|
74
|
+
: await synchronizer.run(options)
|
|
75
|
+
return json(res, 200, { ...result, providers: synchronizer.listProviders() })
|
|
76
|
+
}
|
|
55
77
|
catch (error) {
|
|
56
|
-
const code = error?.code === 'SETTINGS_CONFLICT' ? 409 : error?.code === 'SETTINGS_UNAVAILABLE' ? 503 : 500
|
|
78
|
+
const code = error?.code === 'SETTINGS_CONFLICT' ? 409 : error?.code === 'SETTINGS_UNAVAILABLE' || error?.code === 'CONFIG_UNAVAILABLE' ? 503 : error?.code === 'PROVIDER_NOT_CONFIGURED' || error?.code === 'UNKNOWN_MODEL' ? 400 : 500
|
|
57
79
|
return json(res, code, { error: { code: error?.code ?? 'run-failed', message: error instanceof Error ? error.message : String(error) } })
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
82
|
return [
|
|
61
83
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/status`, handler: makeHandler('status') }),
|
|
62
84
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/run`, handler: makeHandler('run') }),
|
|
85
|
+
ctx.webServer.register({ kind: 'exact', path: `${BASE}/selection`, handler: makeHandler('selection') }),
|
|
63
86
|
]
|
|
64
87
|
}
|
package/lib/index.js
CHANGED
|
@@ -17,23 +17,34 @@ export const Config = Schema.object({
|
|
|
17
17
|
provider: Schema.string().required(),
|
|
18
18
|
enabled: Schema.boolean().default(true),
|
|
19
19
|
})).default([]),
|
|
20
|
+
modelSelections: Schema.dict(Schema.array(Schema.string())).default({}),
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
export function apply(ctx, config) {
|
|
23
24
|
let liveConfig = Config(structuredClone(config || {}))
|
|
24
25
|
let reconfigure = () => {}
|
|
26
|
+
let saveConfig = async () => {
|
|
27
|
+
throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
28
|
+
}
|
|
25
29
|
const settings = ctx.get('settings')
|
|
26
30
|
if (settings?.settings?.register) {
|
|
27
31
|
const scope = settings.settings.register(NS, Config, { base: config })
|
|
28
32
|
liveConfig = Config(scope.get() || config)
|
|
29
|
-
|
|
33
|
+
const watch = scope.watch ?? scope.onChange
|
|
34
|
+
ctx.effect(() => watch?.call(scope, (next) => {
|
|
30
35
|
liveConfig = Config(next || config)
|
|
31
36
|
reconfigure()
|
|
32
37
|
}), 'dsh-model-sync: settings')
|
|
38
|
+
saveConfig = async (patch) => {
|
|
39
|
+
if (typeof scope.update === 'function') return scope.update(patch)
|
|
40
|
+
const next = { ...scope.get(), ...patch }
|
|
41
|
+
if (typeof scope.replace === 'function') return scope.replace(next)
|
|
42
|
+
throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
43
|
+
}
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
const service = createProviderService(ctx)
|
|
36
|
-
const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig })
|
|
47
|
+
const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
|
|
37
48
|
const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
|
|
38
49
|
reconfigure = () => scheduler.reconfigure()
|
|
39
50
|
scheduler.start()
|
package/lib/synchronizer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { listApiKeyProviders } from './inventory.js'
|
|
|
2
2
|
import { createProviderAdapterRegistry } from './adapter-registry.js'
|
|
3
3
|
import { createCredentialResolver } from './credentials.js'
|
|
4
4
|
import { reconcileModels, catalogPatch } from './reconcile.js'
|
|
5
|
+
import { normalizeModels } from './models.js'
|
|
5
6
|
|
|
6
7
|
const PIAI_NS = 'llm-pi-ai'
|
|
7
8
|
|
|
@@ -23,14 +24,31 @@ function errorMessage(error) {
|
|
|
23
24
|
return error instanceof Error ? error.message : String(error)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function createModelSynchronizer(ctx, { getConfig = () => ({}), fetchImpl = fetch } = {}) {
|
|
27
|
+
export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfig = async () => {}, fetchImpl = fetch } = {}) {
|
|
27
28
|
const resolveCredential = createCredentialResolver(ctx)
|
|
28
29
|
const registry = createProviderAdapterRegistry({ resolveCredential, fetchImpl })
|
|
29
30
|
let running = null
|
|
30
31
|
let lastRun = null
|
|
31
32
|
|
|
32
33
|
function providers() {
|
|
33
|
-
|
|
34
|
+
const config = getConfig() ?? {}
|
|
35
|
+
const lastResults = new Map((lastRun?.results ?? []).map((row) => [row.provider, row]))
|
|
36
|
+
return listApiKeyProviders(ctx.llm, settingsOf(ctx)).map((row) => {
|
|
37
|
+
const selection = Array.isArray(config.modelSelections?.[row.provider])
|
|
38
|
+
? [...new Set(config.modelSelections[row.provider].filter((id) => typeof id === 'string' && id))]
|
|
39
|
+
: []
|
|
40
|
+
const latest = lastResults.get(row.provider)
|
|
41
|
+
return {
|
|
42
|
+
...row,
|
|
43
|
+
selectedModels: selection,
|
|
44
|
+
availableModels: latest?.status === 'ok' ? latest.availableModels ?? latest.models ?? row.models : row.models,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selectionFor(config, provider) {
|
|
50
|
+
const selection = config?.modelSelections?.[provider]
|
|
51
|
+
return Array.isArray(selection) ? [...new Set(selection.filter((id) => typeof id === 'string' && id))] : []
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
async function discover(row, { signal } = {}) {
|
|
@@ -68,8 +86,21 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), fetchImpl
|
|
|
68
86
|
}
|
|
69
87
|
const settings = settingsOf(ctx)
|
|
70
88
|
const current = profileOf(settings, row.provider).models ?? row.models
|
|
71
|
-
const
|
|
72
|
-
|
|
89
|
+
const selectedModels = selectionFor(config, row.provider)
|
|
90
|
+
const advertised = selectedModels.length
|
|
91
|
+
? discovered.models.filter((model) => selectedModels.includes(model.id))
|
|
92
|
+
: discovered.models
|
|
93
|
+
const effectiveRemoveMissing = removeMissing || selectedModels.length > 0
|
|
94
|
+
const reconciliation = reconcileModels(row.provider, current, advertised, { removeMissing: effectiveRemoveMissing })
|
|
95
|
+
results.push({
|
|
96
|
+
...discovered,
|
|
97
|
+
availableModels: discovered.models,
|
|
98
|
+
selectedModels,
|
|
99
|
+
advertised,
|
|
100
|
+
...reconciliation,
|
|
101
|
+
removeMissing: effectiveRemoveMissing,
|
|
102
|
+
changed: reconciliation.changed,
|
|
103
|
+
})
|
|
73
104
|
}
|
|
74
105
|
const result = { startedAt, finishedAt: Date.now(), dryRun, removeMissing, results, applied: false }
|
|
75
106
|
if (!dryRun && results.some((row) => row.status === 'ok' && row.changed)) {
|
|
@@ -93,11 +124,43 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), fetchImpl
|
|
|
93
124
|
return running
|
|
94
125
|
}
|
|
95
126
|
|
|
127
|
+
async function setModelSelection(provider, models) {
|
|
128
|
+
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
129
|
+
if (!Array.isArray(models)) throw new Error('models must be an array')
|
|
130
|
+
const selectedModels = [...new Set(models.filter((id) => typeof id === 'string' && id.trim()).map((id) => id.trim()))]
|
|
131
|
+
const config = getConfig() ?? {}
|
|
132
|
+
const row = providers().find((item) => item.provider === provider)
|
|
133
|
+
if (!row) throw Object.assign(new Error(`provider is not configured: ${provider}`), { code: 'PROVIDER_NOT_CONFIGURED' })
|
|
134
|
+
const latest = lastRun?.results?.find((item) => item.provider === provider && item.status === 'ok')
|
|
135
|
+
const available = normalizeModels(provider, latest?.availableModels ?? latest?.models ?? row.availableModels ?? row.models)
|
|
136
|
+
const availableById = new Map(available.map((model) => [model.id, model]))
|
|
137
|
+
const unknown = selectedModels.filter((id) => !availableById.has(id))
|
|
138
|
+
if (unknown.length > 0) throw Object.assign(new Error(`unknown models for ${provider}: ${unknown.join(', ')}`), { code: 'UNKNOWN_MODEL' })
|
|
139
|
+
if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
140
|
+
|
|
141
|
+
const settings = settingsOf(ctx)
|
|
142
|
+
const section = settings?.get?.(PIAI_NS)
|
|
143
|
+
const descriptor = descriptorOf(settings, PIAI_NS)
|
|
144
|
+
if (!section || !descriptor || typeof settings.replace !== 'function') {
|
|
145
|
+
throw Object.assign(new Error('llm-pi-ai settings are not writable'), { code: 'SETTINGS_UNAVAILABLE' })
|
|
146
|
+
}
|
|
147
|
+
const current = normalizeModels(provider, profileOf(settings, provider).models ?? row.models)
|
|
148
|
+
const nextModels = selectedModels.length
|
|
149
|
+
? selectedModels.map((id) => availableById.get(id))
|
|
150
|
+
: (available.length > 0 ? available : current)
|
|
151
|
+
const next = catalogPatch(section, provider, nextModels)
|
|
152
|
+
await settings.replace(PIAI_NS, next, descriptor.revision)
|
|
153
|
+
const modelSelections = { ...(config.modelSelections ?? {}), [provider]: selectedModels }
|
|
154
|
+
await saveConfig({ modelSelections })
|
|
155
|
+
return { provider, selectedModels, models: nextModels, availableModels: available, applied: true }
|
|
156
|
+
}
|
|
157
|
+
|
|
96
158
|
return Object.freeze({
|
|
97
159
|
listProviders: providers,
|
|
98
160
|
getProvider: (provider) => providers().find((row) => row.provider === provider) ?? null,
|
|
99
161
|
discover,
|
|
100
162
|
run,
|
|
163
|
+
setModelSelection,
|
|
101
164
|
status: () => ({ running: Boolean(running), lastRun }),
|
|
102
165
|
})
|
|
103
166
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-model-sync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"models",
|
|
10
|
+
"api-key",
|
|
11
|
+
"catalog"
|
|
12
|
+
],
|
|
6
13
|
"repository": {
|
|
7
14
|
"type": "git",
|
|
8
15
|
"url": "https://github.com/GooDAnDReaDY/dsh-model-sync.git"
|
|
@@ -19,7 +26,12 @@
|
|
|
19
26
|
"./package.json": "./package.json",
|
|
20
27
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
21
28
|
},
|
|
22
|
-
"files": [
|
|
29
|
+
"files": [
|
|
30
|
+
"lib",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
23
35
|
"dsh": {
|
|
24
36
|
"bundle": {
|
|
25
37
|
"patch": "./cordis.patch.yml"
|