@goodandready/dsh-model-sync 0.3.13 → 0.4.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 +27 -0
- package/README.ru.md +27 -0
- package/README.zh.md +27 -0
- package/lib/client.js +1104 -92
- package/lib/http.js +82 -5
- package/lib/index.js +180 -163
- package/lib/policy.js +24 -1
- 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 +38 -128
- package/lib/updater.js +362 -0
- package/package.json +6 -2
|
@@ -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
|
+
}
|
package/lib/synchronizer.js
CHANGED
|
@@ -9,117 +9,23 @@ import { appendHistory, createHistoryEntry, historyEntry, listHistory } from './
|
|
|
9
9
|
import { lifecycleOptions, lifecycleStatus, selectableModels, updateLifecycle } from './lifecycle.js'
|
|
10
10
|
import { appendNotification, listNotifications, createSyncReport, updateNotification } from './reporting.js'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function reliabilityOptions(config, options) {
|
|
30
|
-
const source = { ...config, ...options }
|
|
31
|
-
const baseDelay = boundedInteger(source.retryBaseDelayMs, DEFAULT_RELIABILITY.retryBaseDelayMs, 0, 60000)
|
|
32
|
-
const cooldownMs = source.circuitBreakerCooldownMs !== undefined
|
|
33
|
-
? boundedInteger(source.circuitBreakerCooldownMs, DEFAULT_RELIABILITY.circuitBreakerCooldownMs, 0, 3600000)
|
|
34
|
-
: boundedInteger(Number(source.circuitBreakerCooldownMinutes) * 60000, DEFAULT_RELIABILITY.circuitBreakerCooldownMs, 0, 3600000)
|
|
35
|
-
return {
|
|
36
|
-
timeoutMs: boundedInteger(source.requestTimeoutMs, DEFAULT_RELIABILITY.timeoutMs, 100, 120000),
|
|
37
|
-
retryAttempts: boundedInteger(source.retryAttempts, DEFAULT_RELIABILITY.retryAttempts, 1, 6),
|
|
38
|
-
retryBaseDelayMs: baseDelay,
|
|
39
|
-
retryMaxDelayMs: Math.max(baseDelay, boundedInteger(source.retryMaxDelayMs, DEFAULT_RELIABILITY.retryMaxDelayMs, 0, 120000)),
|
|
40
|
-
concurrency: boundedInteger(source.concurrency, DEFAULT_RELIABILITY.concurrency, 1, 32),
|
|
41
|
-
circuitBreakerFailures: boundedInteger(source.circuitBreakerFailures, DEFAULT_RELIABILITY.circuitBreakerFailures, 0, 20),
|
|
42
|
-
circuitBreakerCooldownMs: cooldownMs,
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function settingsOf(ctx) {
|
|
47
|
-
try { return (typeof ctx.get === 'function' ? (ctx.get('settings') ?? ctx.settings) : ctx.settings) } catch { return ctx.settings }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function llmOf(ctx) {
|
|
51
|
-
try {
|
|
52
|
-
const llm = typeof ctx.get === 'function' ? ctx.get('llm') : null
|
|
53
|
-
return (llm && typeof llm === 'object' && (llm.listConfigurableProviders || llm.listProviders)) ? llm : ctx.llm
|
|
54
|
-
} catch {
|
|
55
|
-
return ctx.llm
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function descriptorOf(settings, ns) {
|
|
60
|
-
try { return settings?.describe?.({ redactSecrets: true })?.find((row) => row.ns === ns) }
|
|
61
|
-
catch { return undefined }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function profileOf(settings, provider) {
|
|
65
|
-
try { return settings?.get(PIAI_NS)?.providers?.[provider] ?? {} }
|
|
66
|
-
catch { return {} }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function errorMessage(error) {
|
|
70
|
-
return error instanceof Error ? error.message : String(error)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function diagnosticMessage(error) {
|
|
74
|
-
return errorMessage(error).replace(/(api[_-]?key|token|secret|authorization|password)\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]').slice(0, 160)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function adapterErrorType(error) {
|
|
78
|
-
if (error?.adapterCode === 'auth') return 'auth'
|
|
79
|
-
if (error?.adapterCode === 'rate-limit') return 'rate-limit'
|
|
80
|
-
if (error?.adapterCode === 'schema') return 'schema'
|
|
81
|
-
if (error?.adapterCode === 'endpoint') return 'endpoint'
|
|
82
|
-
if (error?.code === 'CREDENTIALS_UNAVAILABLE' || error?.code === 'AUTH') return 'credential'
|
|
83
|
-
const status = statusOfError(error)
|
|
84
|
-
if (status === 401 || status === 403) return 'auth'
|
|
85
|
-
if (status === 429) return 'rate-limit'
|
|
86
|
-
if (status) return 'http'
|
|
87
|
-
return 'endpoint'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
|
|
91
|
-
let lastError
|
|
92
|
-
for (let i = 0; i < attempts; i++) {
|
|
93
|
-
const section = settings?.get?.(ns)
|
|
94
|
-
const descriptor = descriptorOf(settings, ns)
|
|
95
|
-
if (!section || !descriptor || typeof settings.replace !== 'function') {
|
|
96
|
-
throw Object.assign(new Error(`${ns} settings are not writable`), { code: 'SETTINGS_UNAVAILABLE' })
|
|
97
|
-
}
|
|
98
|
-
const next = patchFn(structuredClone(section))
|
|
99
|
-
try {
|
|
100
|
-
await settings.replace(ns, next, descriptor.revision)
|
|
101
|
-
return next
|
|
102
|
-
} catch (err) {
|
|
103
|
-
lastError = err
|
|
104
|
-
const isConflict = err?.code === 'SETTINGS_CONFLICT' || err?.status === 409 || /conflict|revision/i.test(err?.message || '')
|
|
105
|
-
if (isConflict && i < attempts - 1) {
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
108
|
-
throw err
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
throw lastError
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function rotationRefs(settings, provider) {
|
|
115
|
-
try {
|
|
116
|
-
const rows = settings?.get?.('dsh-key-rotation')?.providers
|
|
117
|
-
if (!Array.isArray(rows)) return []
|
|
118
|
-
return rows.filter((row) => row?.provider === provider).flatMap((row) => Array.isArray(row.keys) ? row.keys : [])
|
|
119
|
-
} catch {
|
|
120
|
-
return []
|
|
121
|
-
}
|
|
122
|
-
}
|
|
12
|
+
import {
|
|
13
|
+
PIAI_NS,
|
|
14
|
+
DEFAULT_RELIABILITY,
|
|
15
|
+
boundedInteger,
|
|
16
|
+
reliabilityOptions,
|
|
17
|
+
settingsOf,
|
|
18
|
+
llmOf,
|
|
19
|
+
descriptorOf,
|
|
20
|
+
profileOf,
|
|
21
|
+
errorMessage,
|
|
22
|
+
diagnosticMessage,
|
|
23
|
+
adapterErrorType,
|
|
24
|
+
replaceWithRetry,
|
|
25
|
+
rotationRefs,
|
|
26
|
+
} from './synchronizer-helpers.js'
|
|
27
|
+
import { createConfigTransfer } from './synchronizer-transfer.js'
|
|
28
|
+
import { createModelProber } from './synchronizer-prober.js'
|
|
123
29
|
|
|
124
30
|
export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfig = async () => {}, fetchImpl = fetch, adapterImplementations = {} } = {}) {
|
|
125
31
|
const resolveCredential = createCredentialResolver(ctx)
|
|
@@ -531,22 +437,9 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
531
437
|
return transientNotifications.find((row) => row.id === id)
|
|
532
438
|
}
|
|
533
439
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const start = Date.now()
|
|
538
|
-
// Use health as lightweight probe; latency is the signal
|
|
539
|
-
const result = await health({ provider })
|
|
540
|
-
const entry = result?.results?.find((r) => r.provider === provider)
|
|
541
|
-
return {
|
|
542
|
-
provider,
|
|
543
|
-
model,
|
|
544
|
-
ok: entry?.status === 'ok',
|
|
545
|
-
latencyMs: Date.now() - start,
|
|
546
|
-
status: entry?.status ?? 'unknown',
|
|
547
|
-
...(entry?.status !== 'ok' ? { error: entry?.message || entry?.status || 'probe failed' } : {}),
|
|
548
|
-
}
|
|
549
|
-
}
|
|
440
|
+
const prober = createModelProber({ health })
|
|
441
|
+
const tryModel = prober.tryModel
|
|
442
|
+
const batchTryModels = prober.batchTryModels
|
|
550
443
|
|
|
551
444
|
async function rollbackHistory({ historyId, provider } = {}) {
|
|
552
445
|
if (typeof historyId !== 'string' || !historyId) throw Object.assign(new Error('historyId is required'), { code: 'INVALID_HISTORY' })
|
|
@@ -632,6 +525,17 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
632
525
|
}
|
|
633
526
|
return { provider, selectedModels, models: nextModels, availableModels: available, selectableModels: safeAvailable, applied: true }
|
|
634
527
|
}
|
|
528
|
+
const transfer = createConfigTransfer({
|
|
529
|
+
getConfig,
|
|
530
|
+
saveConfig,
|
|
531
|
+
listProviders: providers,
|
|
532
|
+
validatePolicy,
|
|
533
|
+
})
|
|
534
|
+
const exportConfig = transfer.exportConfig
|
|
535
|
+
const importConfig = transfer.importConfig
|
|
536
|
+
const getAliases = transfer.getAliases
|
|
537
|
+
const setAlias = transfer.setAlias
|
|
538
|
+
const deleteAlias = transfer.deleteAlias
|
|
635
539
|
|
|
636
540
|
return Object.freeze({
|
|
637
541
|
listProviders: providers,
|
|
@@ -645,12 +549,18 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
|
|
|
645
549
|
listPolicies,
|
|
646
550
|
health,
|
|
647
551
|
tryModel,
|
|
552
|
+
batchTryModels,
|
|
553
|
+
exportConfig,
|
|
554
|
+
importConfig,
|
|
555
|
+
getAliases,
|
|
556
|
+
setAlias,
|
|
557
|
+
deleteAlias,
|
|
648
558
|
credentialDiagnostics,
|
|
649
559
|
history,
|
|
650
560
|
report: () => structuredClone(lastReport),
|
|
651
561
|
notifications: notificationLedger,
|
|
652
562
|
updateNotification: updateNotificationState,
|
|
653
563
|
rollbackHistory,
|
|
654
|
-
status: () => ({ running: Boolean(running), lastRun, lastHealth, lastReport: structuredClone(lastReport), notifications: notificationLedger(), history: history(), policies: listPolicies(), lifecycle: structuredClone(getConfig()?.modelLifecycle ?? {}) }),
|
|
564
|
+
status: () => ({ running: Boolean(running), lastRun, lastHealth, lastReport: structuredClone(lastReport), notifications: notificationLedger(), history: history(), policies: listPolicies(), lifecycle: structuredClone(getConfig()?.modelLifecycle ?? {}), aliases: structuredClone(getConfig()?.aliases ?? {}) }),
|
|
655
565
|
})
|
|
656
566
|
}
|