@goodandready/dsh-model-sync 0.3.12 → 0.3.14
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 +34 -0
- package/README.ru.md +34 -0
- package/README.zh.md +34 -0
- package/lib/adapter-registry.js +20 -3
- package/lib/client.js +994 -81
- package/lib/generic-adapter.js +23 -5
- package/lib/http.js +298 -237
- package/lib/index.js +172 -163
- package/lib/models.js +300 -282
- package/lib/policy.js +25 -2
- package/lib/reliability.js +6 -2
- package/lib/synchronizer.js +157 -3
- package/package.json +1 -1
package/lib/models.js
CHANGED
|
@@ -1,282 +1,300 @@
|
|
|
1
|
-
const FIELD_ALIASES = Object.freeze({
|
|
2
|
-
contextWindow: [
|
|
3
|
-
'contextWindow', 'context_window', 'contextLength', 'context_length',
|
|
4
|
-
'maxContextTokens', 'max_context_tokens', 'maxContextLength', 'max_context_length',
|
|
5
|
-
'inputTokenLimit', 'input_token_limit',
|
|
6
|
-
],
|
|
7
|
-
maxTokens: [
|
|
8
|
-
'maxTokens', 'max_tokens', 'max_output_tokens', 'maxOutputTokens',
|
|
9
|
-
'outputTokenLimit', 'output_token_limit',
|
|
10
|
-
],
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
const CAPABILITY_ALIASES = Object.freeze({
|
|
14
|
-
vision: ['vision', 'supportsVision', 'supports_vision', 'imageInput', 'image_input'],
|
|
15
|
-
tools: ['tools', 'toolUse', 'tool_use', 'functionCalling', 'function_calling', 'supportsTools', 'supports_tools'],
|
|
16
|
-
reasoning: ['reasoning', 'supportsReasoning', 'supports_reasoning', 'thinking', 'extendedThinking', 'extended_thinking'],
|
|
17
|
-
embeddings: ['embeddings', 'embedding', 'supportsEmbeddings', 'supports_embeddings'],
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (value
|
|
70
|
-
if (value
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (typeof
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
1
|
+
const FIELD_ALIASES = Object.freeze({
|
|
2
|
+
contextWindow: [
|
|
3
|
+
'contextWindow', 'context_window', 'contextLength', 'context_length',
|
|
4
|
+
'maxContextTokens', 'max_context_tokens', 'maxContextLength', 'max_context_length',
|
|
5
|
+
'inputTokenLimit', 'input_token_limit',
|
|
6
|
+
],
|
|
7
|
+
maxTokens: [
|
|
8
|
+
'maxTokens', 'max_tokens', 'max_output_tokens', 'maxOutputTokens',
|
|
9
|
+
'outputTokenLimit', 'output_token_limit',
|
|
10
|
+
],
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const CAPABILITY_ALIASES = Object.freeze({
|
|
14
|
+
vision: ['vision', 'supportsVision', 'supports_vision', 'imageInput', 'image_input'],
|
|
15
|
+
tools: ['tools', 'toolUse', 'tool_use', 'functionCalling', 'function_calling', 'supportsTools', 'supports_tools'],
|
|
16
|
+
reasoning: ['reasoning', 'supportsReasoning', 'supports_reasoning', 'thinking', 'extendedThinking', 'extended_thinking'],
|
|
17
|
+
embeddings: ['embeddings', 'embedding', 'supportsEmbeddings', 'supports_embeddings'],
|
|
18
|
+
code: ['code', 'supportsCode', 'supports_code', 'coding', 'codeSpecialized', 'code_specialized'],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const CAPABILITY_TOKENS = Object.freeze({
|
|
22
|
+
vision: new Set(['vision', 'image', 'imageinput', 'multimodal', 'visioninput']),
|
|
23
|
+
tools: new Set(['tools', 'tooluse', 'toolchoice', 'functioncalling', 'functioncall', 'functions']),
|
|
24
|
+
reasoning: new Set(['reasoning', 'reasoningeffort', 'thinking', 'extendedthinking', 'thought', 'cot']),
|
|
25
|
+
embeddings: new Set(['embedding', 'embeddings']),
|
|
26
|
+
code: new Set(['code', 'coding', 'codespecialized', 'codegen']),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const ID_ALIASES = Object.freeze(['aliases', 'alias', 'previousIds', 'previous_ids', 'previousId', 'previous_id'])
|
|
30
|
+
|
|
31
|
+
const PRICING_ALIASES = Object.freeze({
|
|
32
|
+
inputPerToken: ['inputPerToken', 'input_per_token', 'inputPricePerToken', 'input_price_per_token', 'inputTokenPrice', 'input_token_price'],
|
|
33
|
+
outputPerToken: ['outputPerToken', 'output_per_token', 'outputPricePerToken', 'output_price_per_token', 'outputTokenPrice', 'output_token_price'],
|
|
34
|
+
cacheReadPerToken: ['cacheReadPerToken', 'cache_read_per_token', 'cacheReadPricePerToken', 'cache_read_price_per_token', 'cachedInputPerToken', 'cached_input_per_token'],
|
|
35
|
+
cacheWritePerToken: ['cacheWritePerToken', 'cache_write_per_token', 'cacheWritePricePerToken', 'cache_write_price_per_token'],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function numericValue(value, { allowZero = false } = {}) {
|
|
39
|
+
if (typeof value !== 'number' && typeof value !== 'string') return undefined
|
|
40
|
+
if (typeof value === 'string' && value.trim() === '') return undefined
|
|
41
|
+
const clean = typeof value === 'string' ? value.trim().replace(/,/g, '') : value
|
|
42
|
+
let number = Number(clean)
|
|
43
|
+
if (typeof clean === 'string' && !Number.isFinite(number)) {
|
|
44
|
+
const match = clean.match(/^(\d+(?:\.\d+)?)\s*([kmgt])(i?b)?$/i)
|
|
45
|
+
if (match) {
|
|
46
|
+
const base = Number(match[1])
|
|
47
|
+
const unit = match[2].toLowerCase()
|
|
48
|
+
const isBinary = Boolean(match[3] && match[3].toLowerCase().startsWith('i'))
|
|
49
|
+
const mult = unit === 'k' ? (isBinary ? 1024 : 1000)
|
|
50
|
+
: unit === 'm' ? (isBinary ? 1048576 : 1000000)
|
|
51
|
+
: unit === 'g' ? (isBinary ? 1073741824 : 1000000000)
|
|
52
|
+
: 1
|
|
53
|
+
number = Math.round(base * mult)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isFinite(number) || (allowZero ? number < 0 : number <= 0)) return undefined
|
|
57
|
+
return number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function firstNumber(row, fields, options) {
|
|
61
|
+
for (const field of fields) {
|
|
62
|
+
const value = numericValue(row?.[field], options)
|
|
63
|
+
if (value !== undefined) return value
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function booleanValue(value) {
|
|
69
|
+
if (value === true || value === false) return value
|
|
70
|
+
if (typeof value !== 'string') return undefined
|
|
71
|
+
if (value.trim().toLowerCase() === 'true') return true
|
|
72
|
+
if (value.trim().toLowerCase() === 'false') return false
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function capabilityToken(value) {
|
|
77
|
+
if (typeof value !== 'string') return undefined
|
|
78
|
+
const token = value.trim().toLowerCase().replaceAll(/[_-]/g, '')
|
|
79
|
+
return Object.entries(CAPABILITY_TOKENS).find(([, tokens]) => tokens.has(token))?.[0]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeCapabilities(row) {
|
|
83
|
+
const result = {}
|
|
84
|
+
const assign = (key, value) => {
|
|
85
|
+
const normalized = booleanValue(value)
|
|
86
|
+
if (normalized !== undefined) result[key] = normalized
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
|
|
90
|
+
for (const field of fields) {
|
|
91
|
+
if (row?.[field] !== undefined) assign(key, row[field])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sources = [row?.capabilities, row?.supportedCapabilities, row?.supported_capabilities]
|
|
96
|
+
for (const source of sources) {
|
|
97
|
+
if (Array.isArray(source)) {
|
|
98
|
+
for (const token of source) {
|
|
99
|
+
const key = capabilityToken(token)
|
|
100
|
+
if (key) result[key] = true
|
|
101
|
+
}
|
|
102
|
+
} else if (source && typeof source === 'object') {
|
|
103
|
+
for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
|
|
104
|
+
for (const field of fields) {
|
|
105
|
+
if (source[field] !== undefined) assign(key, source[field])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const [token, value] of Object.entries(source)) {
|
|
109
|
+
const key = capabilityToken(token)
|
|
110
|
+
if (key) assign(key, value)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const modalitySources = [row?.modalities, row?.inputModalities, row?.input_modalities]
|
|
116
|
+
for (const source of modalitySources) {
|
|
117
|
+
if (!Array.isArray(source)) continue
|
|
118
|
+
for (const token of source) {
|
|
119
|
+
const key = capabilityToken(token)
|
|
120
|
+
if (key) result[key] = true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const parameterSources = [row?.supportedParameters, row?.supported_parameters]
|
|
125
|
+
for (const source of parameterSources) {
|
|
126
|
+
if (!Array.isArray(source)) continue
|
|
127
|
+
for (const token of source) {
|
|
128
|
+
const key = capabilityToken(token)
|
|
129
|
+
if (key) result[key] = true
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Model ID & name heuristic markers for modern models
|
|
134
|
+
const idStr = String(row?.id ?? row?.slug ?? row?.model ?? '').toLowerCase()
|
|
135
|
+
if (!result.reasoning && (/(^|[_-])(o1|o3|r1|qwq|deepseek-r1|thinking)($|[_-])/i.test(idStr) || idStr.includes('-r1') || idStr.includes('reasoning') || idStr.includes('thinking'))) {
|
|
136
|
+
result.reasoning = true
|
|
137
|
+
}
|
|
138
|
+
if (!result.code && (/(^|[_-])(coder|code|starcoder|codellama)($|[_-])/i.test(idStr) || idStr.includes('coder') || idStr.includes('code-'))) {
|
|
139
|
+
result.code = true
|
|
140
|
+
}
|
|
141
|
+
if (!result.vision && (/(^|[_-])(vision|vl|multimodal|4v|4o|flash)($|[_-])/i.test(idStr) || idStr.includes('-vl') || idStr.includes('-vision'))) {
|
|
142
|
+
// Only set vision if clearly indicated by vision/vl tags
|
|
143
|
+
if (/(^|[_-])(vision|vl)($|[_-])/i.test(idStr) || idStr.includes('-vl') || idStr.includes('-vision')) {
|
|
144
|
+
result.vision = true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeTags(row) {
|
|
152
|
+
const source = row?.tags ?? row?.labels
|
|
153
|
+
if (!Array.isArray(source)) return undefined
|
|
154
|
+
const tags = [...new Set(source
|
|
155
|
+
.filter((tag) => typeof tag === 'string' && tag.trim())
|
|
156
|
+
.map((tag) => tag.trim())
|
|
157
|
+
.slice(0, 50))]
|
|
158
|
+
return tags.length > 0 ? tags : undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeAliases(row, id) {
|
|
162
|
+
const aliases = []
|
|
163
|
+
for (const field of ID_ALIASES) {
|
|
164
|
+
const source = row?.[field]
|
|
165
|
+
const values = Array.isArray(source) ? source : source === undefined || source === null ? [] : [source]
|
|
166
|
+
for (const value of values) {
|
|
167
|
+
if (typeof value !== 'string') continue
|
|
168
|
+
const alias = value.trim()
|
|
169
|
+
if (alias && alias !== id && !aliases.includes(alias)) aliases.push(alias)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return aliases.slice(0, 20)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeLifecycle(row) {
|
|
176
|
+
const source = row?.lifecycle && typeof row.lifecycle === 'object' ? row.lifecycle : {}
|
|
177
|
+
const deprecated = booleanValue(source.deprecated ?? row?.deprecated ?? row?.isDeprecated)
|
|
178
|
+
const date = source.deprecationDate ?? source.deprecation_date ?? source.expirationDate ?? source.expiration_date ?? source.expiresAt ?? source.expires_at ?? row?.deprecationDate ?? row?.deprecation_date ?? row?.expirationDate ?? row?.expiration_date ?? row?.expiresAt ?? row?.expires_at
|
|
179
|
+
const deprecationDate = typeof date === 'string' && date.trim() ? date.trim() : undefined
|
|
180
|
+
if (deprecated === undefined && deprecationDate === undefined) return undefined
|
|
181
|
+
return {
|
|
182
|
+
...(deprecated === undefined ? {} : { deprecated }),
|
|
183
|
+
...(deprecationDate === undefined ? {} : { deprecationDate }),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizePricing(row) {
|
|
188
|
+
const source = row?.pricing ?? row?.price ?? row?.cost
|
|
189
|
+
if (!source || typeof source !== 'object') return undefined
|
|
190
|
+
const inputPerToken = firstNumber(source, PRICING_ALIASES.inputPerToken, { allowZero: true })
|
|
191
|
+
const outputPerToken = firstNumber(source, PRICING_ALIASES.outputPerToken, { allowZero: true })
|
|
192
|
+
const cacheReadPerToken = firstNumber(source, PRICING_ALIASES.cacheReadPerToken, { allowZero: true })
|
|
193
|
+
const cacheWritePerToken = firstNumber(source, PRICING_ALIASES.cacheWritePerToken, { allowZero: true })
|
|
194
|
+
if (inputPerToken === undefined && outputPerToken === undefined && cacheReadPerToken === undefined && cacheWritePerToken === undefined) return undefined
|
|
195
|
+
const pricing = {
|
|
196
|
+
...(inputPerToken === undefined ? {} : { inputPerToken }),
|
|
197
|
+
...(outputPerToken === undefined ? {} : { outputPerToken }),
|
|
198
|
+
...(cacheReadPerToken === undefined ? {} : { cacheReadPerToken }),
|
|
199
|
+
...(cacheWritePerToken === undefined ? {} : { cacheWritePerToken }),
|
|
200
|
+
}
|
|
201
|
+
if (typeof source.currency === 'string' && source.currency.trim()) pricing.currency = source.currency.trim()
|
|
202
|
+
if (typeof source.unit === 'string' && source.unit.trim()) pricing.unit = source.unit.trim()
|
|
203
|
+
return pricing
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function normalizeModel(provider, row) {
|
|
207
|
+
if (typeof row === 'string') {
|
|
208
|
+
row = { id: row }
|
|
209
|
+
}
|
|
210
|
+
if (!row || typeof row !== 'object') return null
|
|
211
|
+
const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
|
|
212
|
+
if (!id) return null
|
|
213
|
+
const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
|
|
214
|
+
const contextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
|
|
215
|
+
const maxTokens = firstNumber(row, FIELD_ALIASES.maxTokens)
|
|
216
|
+
const capabilities = normalizeCapabilities(row)
|
|
217
|
+
const pricing = normalizePricing(row)
|
|
218
|
+
const tags = normalizeTags(row)
|
|
219
|
+
const aliases = normalizeAliases(row, id)
|
|
220
|
+
const lifecycle = normalizeLifecycle(row)
|
|
221
|
+
return {
|
|
222
|
+
provider,
|
|
223
|
+
id,
|
|
224
|
+
name,
|
|
225
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
226
|
+
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
227
|
+
...(row.description ? { description: String(row.description) } : {}),
|
|
228
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
229
|
+
...(pricing === undefined ? {} : { pricing }),
|
|
230
|
+
...(tags === undefined ? {} : { tags }),
|
|
231
|
+
...(aliases.length === 0 ? {} : { aliases }),
|
|
232
|
+
...(lifecycle === undefined ? {} : { lifecycle }),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function normalizeModels(provider, rows) {
|
|
237
|
+
const out = []
|
|
238
|
+
const seen = new Set()
|
|
239
|
+
for (const row of rows ?? []) {
|
|
240
|
+
const model = normalizeModel(provider, row)
|
|
241
|
+
if (!model || seen.has(model.id)) continue
|
|
242
|
+
seen.add(model.id)
|
|
243
|
+
out.push(model)
|
|
244
|
+
}
|
|
245
|
+
return out
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function stableStringify(value) {
|
|
249
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`
|
|
250
|
+
if (value && typeof value === 'object') {
|
|
251
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`
|
|
252
|
+
}
|
|
253
|
+
return JSON.stringify(value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function diffModels(previous, next) {
|
|
257
|
+
const sortModels = (rows) => normalizeModels('', rows).sort((left, right) => left.id.localeCompare(right.id))
|
|
258
|
+
const beforeRows = sortModels(previous)
|
|
259
|
+
const afterRows = sortModels(next)
|
|
260
|
+
const before = new Map(beforeRows.map((row) => [row.id, row]))
|
|
261
|
+
const after = new Map(afterRows.map((row) => [row.id, row]))
|
|
262
|
+
const added = []
|
|
263
|
+
const removed = []
|
|
264
|
+
const renamed = []
|
|
265
|
+
const changed = []
|
|
266
|
+
const renamedBefore = new Set()
|
|
267
|
+
for (const [id, model] of after) {
|
|
268
|
+
if (!before.has(id)) continue
|
|
269
|
+
if (stableStringify(before.get(id)) !== stableStringify(model)) {
|
|
270
|
+
changed.push({ before: before.get(id), after: model })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const [id, model] of after) {
|
|
274
|
+
if (before.has(id)) continue
|
|
275
|
+
const match = beforeRows.find((candidate) => {
|
|
276
|
+
if (renamedBefore.has(candidate.id)) return false
|
|
277
|
+
return model.aliases?.includes(candidate.id) || candidate.aliases?.includes(model.id)
|
|
278
|
+
})
|
|
279
|
+
if (match) {
|
|
280
|
+
renamed.push({ before: match, after: model })
|
|
281
|
+
renamedBefore.add(match.id)
|
|
282
|
+
} else {
|
|
283
|
+
added.push(model)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const [id, model] of before) {
|
|
287
|
+
if (!after.has(id) && !renamedBefore.has(id)) removed.push(model)
|
|
288
|
+
}
|
|
289
|
+
added.sort((left, right) => left.id.localeCompare(right.id))
|
|
290
|
+
removed.sort((left, right) => left.id.localeCompare(right.id))
|
|
291
|
+
renamed.sort((left, right) => left.before.id.localeCompare(right.before.id))
|
|
292
|
+
changed.sort((left, right) => left.before.id.localeCompare(right.before.id))
|
|
293
|
+
return {
|
|
294
|
+
added,
|
|
295
|
+
removed,
|
|
296
|
+
renamed,
|
|
297
|
+
changed,
|
|
298
|
+
hasChanges: added.length > 0 || removed.length > 0 || renamed.length > 0 || changed.length > 0,
|
|
299
|
+
}
|
|
300
|
+
}
|