@goodandready/dsh-model-sync 0.3.14 → 0.4.1
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 +2 -141
- package/README.ru.md +8 -125
- package/README.zh.md +8 -10
- package/lib/client.js +121 -13
- package/lib/http.js +30 -4
- package/lib/index.js +9 -1
- package/lib/models.js +84 -2
- package/lib/reconcile.js +14 -2
- package/lib/synchronizer-helpers.js +113 -0
- package/lib/synchronizer-prober.js +50 -0
- package/lib/synchronizer-transfer.js +118 -0
- package/lib/synchronizer.js +31 -264
- package/lib/updater.js +362 -0
- package/package.json +6 -2
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createProviderService } from './provider-service.js'
|
|
|
3
3
|
import { createModelSynchronizer } from './synchronizer.js'
|
|
4
4
|
import { createSyncScheduler } from './scheduler.js'
|
|
5
5
|
import { registerHttpApi } from './http.js'
|
|
6
|
+
import { registerPluginUpdater } from './updater.js'
|
|
6
7
|
|
|
7
8
|
export const name = '@goodandready/dsh-model-sync'
|
|
8
9
|
export const inject = ['llm', 'settings', 'webServer', 'credentials']
|
|
@@ -143,7 +144,9 @@ export function apply(ctx, config) {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
let settings
|
|
146
|
-
try { settings = ctx.get('settings') } catch {
|
|
147
|
+
try { settings = ctx.get('settings') } catch (e) {
|
|
148
|
+
if (typeof ctx.logger?.debug === 'function') ctx.logger.debug('[dsh-model-sync] settings service not immediately available:', e)
|
|
149
|
+
}
|
|
147
150
|
if (!bindSettings(settings, ctx)) {
|
|
148
151
|
ctx.inject(['settings'], (sctx) => {
|
|
149
152
|
bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
|
|
@@ -163,6 +166,11 @@ export function apply(ctx, config) {
|
|
|
163
166
|
for (const dispose of registrations) if (typeof dispose === 'function') dispose()
|
|
164
167
|
}
|
|
165
168
|
}, 'dsh-model-sync: http api')
|
|
169
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
170
|
+
packageName: name,
|
|
171
|
+
endpoint: '/api/dsh-model-sync/update',
|
|
172
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
173
|
+
}), 'dsh-model-sync: plugin updater')
|
|
166
174
|
ctx.effect(() => {
|
|
167
175
|
if (!liveConfig.enabled) return () => {}
|
|
168
176
|
ctx.provide('modelSync', service)
|
package/lib/models.js
CHANGED
|
@@ -92,7 +92,14 @@ function normalizeCapabilities(row) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const sources = [
|
|
95
|
+
const sources = [
|
|
96
|
+
row?.capabilities,
|
|
97
|
+
row?.supportedCapabilities,
|
|
98
|
+
row?.supported_capabilities,
|
|
99
|
+
row?.supportedFeatures,
|
|
100
|
+
row?.supported_features,
|
|
101
|
+
row?.features,
|
|
102
|
+
]
|
|
96
103
|
for (const source of sources) {
|
|
97
104
|
if (Array.isArray(source)) {
|
|
98
105
|
for (const token of source) {
|
|
@@ -148,6 +155,74 @@ function normalizeCapabilities(row) {
|
|
|
148
155
|
return Object.keys(result).length > 0 ? result : undefined
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
|
|
159
|
+
function isGroqMinijinjaModel(id) {
|
|
160
|
+
if (typeof id !== 'string') return false
|
|
161
|
+
const lower = id.toLowerCase().replace(/^groq\//, '')
|
|
162
|
+
return (
|
|
163
|
+
lower.startsWith('qwen') ||
|
|
164
|
+
lower.startsWith('meta-llama') ||
|
|
165
|
+
lower.startsWith('llama') ||
|
|
166
|
+
lower.startsWith('mistral') ||
|
|
167
|
+
lower.startsWith('mixtral') ||
|
|
168
|
+
lower.startsWith('gemma') ||
|
|
169
|
+
lower.startsWith('deepseek')
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeCompat(provider, row, id) {
|
|
174
|
+
const source = row?.compat && typeof row.compat === 'object' ? { ...row.compat } : {}
|
|
175
|
+
const isGroq = provider === 'groq'
|
|
176
|
+
const isDeepSeek = provider === 'deepseek'
|
|
177
|
+
|
|
178
|
+
let defaultSupportsDeveloperRole
|
|
179
|
+
if (isGroq && id && isGroqMinijinjaModel(id)) {
|
|
180
|
+
defaultSupportsDeveloperRole = false
|
|
181
|
+
} else if (isDeepSeek) {
|
|
182
|
+
defaultSupportsDeveloperRole = false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (source.supportsDeveloperRole === undefined && defaultSupportsDeveloperRole !== undefined) {
|
|
186
|
+
source.supportsDeveloperRole = defaultSupportsDeveloperRole
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Object.keys(source).length > 0 ? source : undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeInput(row) {
|
|
193
|
+
const modalitySources = [row?.inputModalities, row?.input_modalities, row?.modalities]
|
|
194
|
+
for (const source of modalitySources) {
|
|
195
|
+
if (Array.isArray(source) && source.length > 0) {
|
|
196
|
+
const set = new Set()
|
|
197
|
+
for (const item of source) {
|
|
198
|
+
if (typeof item !== 'string') continue
|
|
199
|
+
const val = item.trim().toLowerCase()
|
|
200
|
+
if (val === 'image' || val === 'vision') set.add('image')
|
|
201
|
+
else if (val === 'text') set.add('text')
|
|
202
|
+
else if (val) set.add(val)
|
|
203
|
+
}
|
|
204
|
+
const order = ['text', 'image']
|
|
205
|
+
const result = []
|
|
206
|
+
for (const k of order) {
|
|
207
|
+
if (set.has(k)) {
|
|
208
|
+
result.push(k)
|
|
209
|
+
set.delete(k)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const k of set) {
|
|
213
|
+
result.push(k)
|
|
214
|
+
}
|
|
215
|
+
if (result.length > 0) return result
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Array.isArray(row?.input)) {
|
|
220
|
+
return row.input.map((item) => (typeof item === 'string' ? item.trim() : item)).filter(Boolean)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
151
226
|
function normalizeTags(row) {
|
|
152
227
|
const source = row?.tags ?? row?.labels
|
|
153
228
|
if (!Array.isArray(source)) return undefined
|
|
@@ -208,6 +283,9 @@ export function normalizeModel(provider, row) {
|
|
|
208
283
|
row = { id: row }
|
|
209
284
|
}
|
|
210
285
|
if (!row || typeof row !== 'object') return null
|
|
286
|
+
const effectiveProvider = (typeof provider === 'string' && provider.trim())
|
|
287
|
+
? provider.trim()
|
|
288
|
+
: (typeof row.provider === 'string' ? row.provider.trim() : '')
|
|
211
289
|
const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
|
|
212
290
|
if (!id) return null
|
|
213
291
|
const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
|
|
@@ -218,13 +296,17 @@ export function normalizeModel(provider, row) {
|
|
|
218
296
|
const tags = normalizeTags(row)
|
|
219
297
|
const aliases = normalizeAliases(row, id)
|
|
220
298
|
const lifecycle = normalizeLifecycle(row)
|
|
299
|
+
const input = normalizeInput(row)
|
|
300
|
+
const compat = normalizeCompat(effectiveProvider, row, id)
|
|
221
301
|
return {
|
|
222
|
-
provider,
|
|
302
|
+
provider: effectiveProvider || provider,
|
|
223
303
|
id,
|
|
224
304
|
name,
|
|
225
305
|
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
226
306
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
227
307
|
...(row.description ? { description: String(row.description) } : {}),
|
|
308
|
+
...(input === undefined ? {} : { input }),
|
|
309
|
+
...(compat === undefined ? {} : { compat }),
|
|
228
310
|
...(capabilities === undefined ? {} : { capabilities }),
|
|
229
311
|
...(pricing === undefined ? {} : { pricing }),
|
|
230
312
|
...(tags === undefined ? {} : { tags }),
|
package/lib/reconcile.js
CHANGED
|
@@ -18,8 +18,20 @@ export function reconcileModels(provider, current, discovered, {
|
|
|
18
18
|
const stale = []
|
|
19
19
|
for (const row of before) {
|
|
20
20
|
const fresh = advertisedById.get(row.id)
|
|
21
|
-
if (fresh)
|
|
22
|
-
|
|
21
|
+
if (fresh) {
|
|
22
|
+
const merged = {
|
|
23
|
+
...row,
|
|
24
|
+
...fresh,
|
|
25
|
+
...(row.capabilities || fresh.capabilities ? {
|
|
26
|
+
capabilities: { ...(fresh.capabilities ?? {}), ...(row.capabilities ?? {}) },
|
|
27
|
+
} : {}),
|
|
28
|
+
...(row.compat || fresh.compat ? {
|
|
29
|
+
compat: { ...(fresh.compat ?? {}), ...(row.compat ?? {}) },
|
|
30
|
+
} : {}),
|
|
31
|
+
...(fresh.input?.length ? { input: fresh.input } : row.input ? { input: row.input } : {}),
|
|
32
|
+
}
|
|
33
|
+
next.push(merged)
|
|
34
|
+
} else if (renamedBefore.has(row.id)) {
|
|
23
35
|
// An explicit provider alias is safe evidence that the old id was renamed.
|
|
24
36
|
} else {
|
|
25
37
|
stale.push(row)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { statusOfError } from './reliability.js'
|
|
2
|
+
|
|
3
|
+
export const PIAI_NS = 'llm-pi-ai'
|
|
4
|
+
export const DEFAULT_RELIABILITY = Object.freeze({
|
|
5
|
+
timeoutMs: 15000,
|
|
6
|
+
retryAttempts: 3,
|
|
7
|
+
retryBaseDelayMs: 250,
|
|
8
|
+
retryMaxDelayMs: 4000,
|
|
9
|
+
concurrency: 4,
|
|
10
|
+
circuitBreakerFailures: 3,
|
|
11
|
+
circuitBreakerCooldownMs: 300000,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export function boundedInteger(value, fallback, minimum, maximum) {
|
|
15
|
+
const number = Number(value)
|
|
16
|
+
if (!Number.isFinite(number)) return fallback
|
|
17
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(number)))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function reliabilityOptions(config, options) {
|
|
21
|
+
const source = { ...config, ...options }
|
|
22
|
+
const baseDelay = boundedInteger(source.retryBaseDelayMs, DEFAULT_RELIABILITY.retryBaseDelayMs, 0, 60000)
|
|
23
|
+
const cooldownMs = source.circuitBreakerCooldownMs !== undefined
|
|
24
|
+
? boundedInteger(source.circuitBreakerCooldownMs, DEFAULT_RELIABILITY.circuitBreakerCooldownMs, 0, 3600000)
|
|
25
|
+
: boundedInteger(Number(source.circuitBreakerCooldownMinutes) * 60000, DEFAULT_RELIABILITY.circuitBreakerCooldownMs, 0, 3600000)
|
|
26
|
+
return {
|
|
27
|
+
timeoutMs: boundedInteger(source.requestTimeoutMs, DEFAULT_RELIABILITY.timeoutMs, 100, 120000),
|
|
28
|
+
retryAttempts: boundedInteger(source.retryAttempts, DEFAULT_RELIABILITY.retryAttempts, 1, 6),
|
|
29
|
+
retryBaseDelayMs: baseDelay,
|
|
30
|
+
retryMaxDelayMs: Math.max(baseDelay, boundedInteger(source.retryMaxDelayMs, DEFAULT_RELIABILITY.retryMaxDelayMs, 0, 120000)),
|
|
31
|
+
concurrency: boundedInteger(source.concurrency, DEFAULT_RELIABILITY.concurrency, 1, 32),
|
|
32
|
+
circuitBreakerFailures: boundedInteger(source.circuitBreakerFailures, DEFAULT_RELIABILITY.circuitBreakerFailures, 0, 20),
|
|
33
|
+
circuitBreakerCooldownMs: cooldownMs,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function settingsOf(ctx) {
|
|
38
|
+
try { return (typeof ctx.get === 'function' ? (ctx.get('settings') ?? ctx.settings) : ctx.settings) } catch { return ctx.settings }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function llmOf(ctx) {
|
|
42
|
+
try {
|
|
43
|
+
const llm = typeof ctx.get === 'function' ? ctx.get('llm') : null
|
|
44
|
+
return (llm && typeof llm === 'object' && (llm.listConfigurableProviders || llm.listProviders)) ? llm : ctx.llm
|
|
45
|
+
} catch {
|
|
46
|
+
return ctx.llm
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function descriptorOf(settings, ns) {
|
|
51
|
+
try { return settings?.describe?.({ redactSecrets: true })?.find((row) => row.ns === ns) }
|
|
52
|
+
catch { return undefined }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function profileOf(settings, provider) {
|
|
56
|
+
try { return settings?.get(PIAI_NS)?.providers?.[provider] ?? {} }
|
|
57
|
+
catch { return {} }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function errorMessage(error) {
|
|
61
|
+
return error instanceof Error ? error.message : String(error)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function diagnosticMessage(error) {
|
|
65
|
+
return errorMessage(error).replace(/(api[_-]?key|token|secret|authorization|password)\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]').slice(0, 160)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function adapterErrorType(error) {
|
|
69
|
+
if (error?.adapterCode === 'auth') return 'auth'
|
|
70
|
+
if (error?.adapterCode === 'rate-limit') return 'rate-limit'
|
|
71
|
+
if (error?.adapterCode === 'schema') return 'schema'
|
|
72
|
+
if (error?.adapterCode === 'endpoint') return 'endpoint'
|
|
73
|
+
if (error?.code === 'CREDENTIALS_UNAVAILABLE' || error?.code === 'AUTH') return 'credential'
|
|
74
|
+
const status = statusOfError(error)
|
|
75
|
+
if (status === 401 || status === 403) return 'auth'
|
|
76
|
+
if (status === 429) return 'rate-limit'
|
|
77
|
+
if (status) return 'http'
|
|
78
|
+
return 'endpoint'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
|
|
82
|
+
let lastError
|
|
83
|
+
for (let i = 0; i < attempts; i++) {
|
|
84
|
+
const section = settings?.get?.(ns)
|
|
85
|
+
const descriptor = descriptorOf(settings, ns)
|
|
86
|
+
if (!section || !descriptor || typeof settings.replace !== 'function') {
|
|
87
|
+
throw Object.assign(new Error(`${ns} settings are not writable`), { code: 'SETTINGS_UNAVAILABLE' })
|
|
88
|
+
}
|
|
89
|
+
const next = patchFn(structuredClone(section))
|
|
90
|
+
try {
|
|
91
|
+
await settings.replace(ns, next, descriptor.revision)
|
|
92
|
+
return next
|
|
93
|
+
} catch (err) {
|
|
94
|
+
lastError = err
|
|
95
|
+
const isConflict = err?.code === 'SETTINGS_CONFLICT' || err?.status === 409 || /conflict|revision/i.test(err?.message || '')
|
|
96
|
+
if (isConflict && i < attempts - 1) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
throw err
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw lastError
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function rotationRefs(settings, provider) {
|
|
106
|
+
try {
|
|
107
|
+
const rows = settings?.get?.('dsh-key-rotation')?.providers
|
|
108
|
+
if (!Array.isArray(rows)) return []
|
|
109
|
+
return rows.filter((row) => row?.provider === provider).flatMap((row) => Array.isArray(row.keys) ? row.keys : [])
|
|
110
|
+
} catch {
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function createModelProber({ health }) {
|
|
2
|
+
async function tryModel({ provider, model } = {}) {
|
|
3
|
+
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
4
|
+
if (typeof model !== 'string' || !model) throw new Error('model is required')
|
|
5
|
+
const start = Date.now()
|
|
6
|
+
const result = await health({ provider })
|
|
7
|
+
const entry = result?.results?.find((r) => r.provider === provider)
|
|
8
|
+
return {
|
|
9
|
+
provider,
|
|
10
|
+
model,
|
|
11
|
+
ok: entry?.status === 'ok',
|
|
12
|
+
latencyMs: Date.now() - start,
|
|
13
|
+
status: entry?.status ?? 'unknown',
|
|
14
|
+
...(entry?.status !== 'ok' ? { error: entry?.message || entry?.status || 'probe failed' } : {}),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function batchTryModels({ provider, models } = {}) {
|
|
19
|
+
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
20
|
+
if (!Array.isArray(models)) throw new Error('models must be an array')
|
|
21
|
+
const list = [...new Set(models.filter((m) => typeof m === 'string' && m.trim()).map((m) => m.trim()))].slice(0, 50)
|
|
22
|
+
const results = []
|
|
23
|
+
const concurrency = 3
|
|
24
|
+
for (let i = 0; i < list.length; i += concurrency) {
|
|
25
|
+
const chunk = list.slice(i, i + concurrency)
|
|
26
|
+
const chunkResults = await Promise.all(
|
|
27
|
+
chunk.map((model) => tryModel({ provider, model }).catch((err) => ({
|
|
28
|
+
provider,
|
|
29
|
+
model,
|
|
30
|
+
ok: false,
|
|
31
|
+
latencyMs: 0,
|
|
32
|
+
status: 'error',
|
|
33
|
+
error: err instanceof Error ? err.message : String(err)
|
|
34
|
+
})))
|
|
35
|
+
)
|
|
36
|
+
results.push(...chunkResults)
|
|
37
|
+
}
|
|
38
|
+
const reachable = results.filter((r) => r.ok).length
|
|
39
|
+
return {
|
|
40
|
+
provider,
|
|
41
|
+
results,
|
|
42
|
+
summary: { total: results.length, reachable, unreachable: results.length - reachable }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
tryModel,
|
|
48
|
+
batchTryModels,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function createConfigTransfer({ getConfig = () => ({}), saveConfig = async () => {}, listProviders = () => [], validatePolicy } = {}) {
|
|
2
|
+
function exportConfig() {
|
|
3
|
+
const config = getConfig() ?? {}
|
|
4
|
+
const pmap = {}
|
|
5
|
+
for (const p of listProviders()) {
|
|
6
|
+
if (p.configured) {
|
|
7
|
+
pmap[p.provider] = {
|
|
8
|
+
policy: config.modelPolicies?.[p.provider] ?? {},
|
|
9
|
+
selectedModels: config.modelSelections?.[p.provider] ?? []
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
version: '1.0',
|
|
15
|
+
exportedAt: new Date().toISOString(),
|
|
16
|
+
plugin: '@goodandready/dsh-model-sync',
|
|
17
|
+
providers: pmap,
|
|
18
|
+
aliases: config.aliases ?? {},
|
|
19
|
+
scheduler: {
|
|
20
|
+
scheduleEnabled: config.scheduleEnabled ?? false,
|
|
21
|
+
intervalMinutes: config.intervalMinutes ?? 60,
|
|
22
|
+
autoApply: config.autoApply ?? false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function importConfig(data) {
|
|
28
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
29
|
+
throw Object.assign(new Error('invalid config payload'), { code: 'INVALID_CONFIG' })
|
|
30
|
+
}
|
|
31
|
+
if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
32
|
+
const config = getConfig() ?? {}
|
|
33
|
+
const importedProviders = []
|
|
34
|
+
const importedAliases = []
|
|
35
|
+
const patch = {}
|
|
36
|
+
|
|
37
|
+
if (data.providers && typeof data.providers === 'object') {
|
|
38
|
+
const currentPolicies = structuredClone(config.modelPolicies ?? {})
|
|
39
|
+
const currentSelections = structuredClone(config.modelSelections ?? {})
|
|
40
|
+
for (const [pname, pdata] of Object.entries(data.providers)) {
|
|
41
|
+
if (pdata && typeof pdata === 'object') {
|
|
42
|
+
if (pdata.policy) {
|
|
43
|
+
currentPolicies[pname] = typeof validatePolicy === 'function' ? validatePolicy(pdata.policy) : pdata.policy
|
|
44
|
+
importedProviders.push(pname)
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(pdata.selectedModels)) {
|
|
47
|
+
currentSelections[pname] = pdata.selectedModels.filter((m) => typeof m === 'string')
|
|
48
|
+
importedProviders.push(pname)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
patch.modelPolicies = currentPolicies
|
|
53
|
+
patch.modelSelections = currentSelections
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (data.aliases && typeof data.aliases === 'object') {
|
|
57
|
+
const currentAliases = structuredClone(config.aliases ?? {})
|
|
58
|
+
for (const [k, v] of Object.entries(data.aliases)) {
|
|
59
|
+
if (typeof k === 'string' && /^[a-zA-Z0-9_\-.]{1,64}$/.test(k.trim()) && v && typeof v === 'object' && typeof v.provider === 'string' && typeof v.model === 'string') {
|
|
60
|
+
currentAliases[k.trim()] = { provider: v.provider, model: v.model, updatedAt: Date.now() }
|
|
61
|
+
importedAliases.push(k.trim())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
patch.aliases = currentAliases
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (data.scheduler && typeof data.scheduler === 'object') {
|
|
68
|
+
if (typeof data.scheduler.scheduleEnabled === 'boolean') patch.scheduleEnabled = data.scheduler.scheduleEnabled
|
|
69
|
+
if (typeof data.scheduler.intervalMinutes === 'number') patch.intervalMinutes = data.scheduler.intervalMinutes
|
|
70
|
+
if (typeof data.scheduler.autoApply === 'boolean') patch.autoApply = data.scheduler.autoApply
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await saveConfig(patch)
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
importedProviders: [...new Set(importedProviders)],
|
|
77
|
+
importedAliases: [...new Set(importedAliases)]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getAliases() {
|
|
82
|
+
const config = getConfig() ?? {}
|
|
83
|
+
return config.aliases ?? {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function setAlias({ alias, provider, model } = {}) {
|
|
87
|
+
if (typeof alias !== 'string' || !/^[a-zA-Z0-9_\-.]{1,64}$/.test(alias.trim())) {
|
|
88
|
+
throw Object.assign(new Error('alias must be 1-64 alphanumeric characters or _-.'), { code: 'INVALID_ALIAS' })
|
|
89
|
+
}
|
|
90
|
+
const trimmedAlias = alias.trim()
|
|
91
|
+
if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
|
|
92
|
+
if (typeof model !== 'string' || !model) throw new Error('model is required')
|
|
93
|
+
if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
94
|
+
const config = getConfig() ?? {}
|
|
95
|
+
const currentAliases = structuredClone(config.aliases ?? {})
|
|
96
|
+
currentAliases[trimmedAlias] = { provider, model, updatedAt: Date.now() }
|
|
97
|
+
await saveConfig({ aliases: currentAliases })
|
|
98
|
+
return { alias: trimmedAlias, provider, model, aliases: currentAliases }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function deleteAlias({ alias } = {}) {
|
|
102
|
+
if (typeof alias !== 'string' || !alias) throw new Error('alias is required')
|
|
103
|
+
if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
|
|
104
|
+
const config = getConfig() ?? {}
|
|
105
|
+
const currentAliases = structuredClone(config.aliases ?? {})
|
|
106
|
+
delete currentAliases[alias.trim()]
|
|
107
|
+
await saveConfig({ aliases: currentAliases })
|
|
108
|
+
return { alias: alias.trim(), deleted: true, aliases: currentAliases }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
exportConfig,
|
|
113
|
+
importConfig,
|
|
114
|
+
getAliases,
|
|
115
|
+
setAlias,
|
|
116
|
+
deleteAlias,
|
|
117
|
+
}
|
|
118
|
+
}
|