@goodandready/dsh-model-sync 0.3.0 → 0.3.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/lib/models.js CHANGED
@@ -1,254 +1,260 @@
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
- const CAPABILITY_TOKENS = Object.freeze({
21
- vision: new Set(['vision', 'image', 'imageinput', 'multimodal']),
22
- tools: new Set(['tools', 'tooluse', 'toolchoice', 'functioncalling', 'functioncall']),
23
- reasoning: new Set(['reasoning', 'reasoningeffort', 'thinking', 'extendedthinking']),
24
- embeddings: new Set(['embedding', 'embeddings']),
25
- })
26
-
27
- const ID_ALIASES = Object.freeze(['aliases', 'alias', 'previousIds', 'previous_ids', 'previousId', 'previous_id'])
28
-
29
- const PRICING_ALIASES = Object.freeze({
30
- inputPerToken: ['inputPerToken', 'input_per_token', 'inputPricePerToken', 'input_price_per_token'],
31
- outputPerToken: ['outputPerToken', 'output_per_token', 'outputPricePerToken', 'output_price_per_token'],
32
- })
33
-
34
- function numericValue(value, { allowZero = false } = {}) {
35
- if (typeof value !== 'number' && typeof value !== 'string') return undefined
36
- if (typeof value === 'string' && value.trim() === '') return undefined
37
- const number = Number(value)
38
- if (!Number.isFinite(number) || (allowZero ? number < 0 : number <= 0)) return undefined
39
- return number
40
- }
41
-
42
- function firstNumber(row, fields, options) {
43
- for (const field of fields) {
44
- const value = numericValue(row?.[field], options)
45
- if (value !== undefined) return value
46
- }
47
- return undefined
48
- }
49
-
50
- function booleanValue(value) {
51
- if (value === true || value === false) return value
52
- if (typeof value !== 'string') return undefined
53
- if (value.trim().toLowerCase() === 'true') return true
54
- if (value.trim().toLowerCase() === 'false') return false
55
- return undefined
56
- }
57
-
58
- function capabilityToken(value) {
59
- if (typeof value !== 'string') return undefined
60
- const token = value.trim().toLowerCase().replaceAll(/[_-]/g, '')
61
- return Object.entries(CAPABILITY_TOKENS).find(([, tokens]) => tokens.has(token))?.[0]
62
- }
63
-
64
- function normalizeCapabilities(row) {
65
- const result = {}
66
- const assign = (key, value) => {
67
- const normalized = booleanValue(value)
68
- if (normalized !== undefined) result[key] = normalized
69
- }
70
-
71
- for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
72
- for (const field of fields) {
73
- if (row?.[field] !== undefined) assign(key, row[field])
74
- }
75
- }
76
-
77
- const sources = [row?.capabilities, row?.supportedCapabilities, row?.supported_capabilities]
78
- for (const source of sources) {
79
- if (Array.isArray(source)) {
80
- for (const token of source) {
81
- const key = capabilityToken(token)
82
- if (key) result[key] = true
83
- }
84
- } else if (source && typeof source === 'object') {
85
- for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
86
- for (const field of fields) {
87
- if (source[field] !== undefined) assign(key, source[field])
88
- }
89
- }
90
- for (const [token, value] of Object.entries(source)) {
91
- const key = capabilityToken(token)
92
- if (key) assign(key, value)
93
- }
94
- }
95
- }
96
-
97
- const modalitySources = [row?.modalities, row?.inputModalities, row?.input_modalities]
98
- for (const source of modalitySources) {
99
- if (!Array.isArray(source)) continue
100
- for (const token of source) {
101
- const key = capabilityToken(token)
102
- if (key) result[key] = true
103
- }
104
- }
105
-
106
- const parameterSources = [row?.supportedParameters, row?.supported_parameters]
107
- for (const source of parameterSources) {
108
- if (!Array.isArray(source)) continue
109
- for (const token of source) {
110
- const key = capabilityToken(token)
111
- if (key) result[key] = true
112
- }
113
- }
114
- return Object.keys(result).length > 0 ? result : undefined
115
- }
116
-
117
- function normalizeTags(row) {
118
- const source = row?.tags ?? row?.labels
119
- if (!Array.isArray(source)) return undefined
120
- const tags = [...new Set(source
121
- .filter((tag) => typeof tag === 'string' && tag.trim())
122
- .map((tag) => tag.trim())
123
- .slice(0, 50))]
124
- return tags.length > 0 ? tags : undefined
125
- }
126
-
127
- function normalizeAliases(row, id) {
128
- const aliases = []
129
- for (const field of ID_ALIASES) {
130
- const source = row?.[field]
131
- const values = Array.isArray(source) ? source : source === undefined || source === null ? [] : [source]
132
- for (const value of values) {
133
- if (typeof value !== 'string') continue
134
- const alias = value.trim()
135
- if (alias && alias !== id && !aliases.includes(alias)) aliases.push(alias)
136
- }
137
- }
138
- return aliases.slice(0, 20)
139
- }
140
-
141
- function normalizeLifecycle(row) {
142
- const source = row?.lifecycle && typeof row.lifecycle === 'object' ? row.lifecycle : {}
143
- const deprecated = booleanValue(source.deprecated ?? row?.deprecated ?? row?.isDeprecated)
144
- 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
145
- const deprecationDate = typeof date === 'string' && date.trim() ? date.trim() : undefined
146
- if (deprecated === undefined && deprecationDate === undefined) return undefined
147
- return {
148
- ...(deprecated === undefined ? {} : { deprecated }),
149
- ...(deprecationDate === undefined ? {} : { deprecationDate }),
150
- }
151
- }
152
-
153
- function normalizePricing(row) {
154
- const source = row?.pricing ?? row?.price ?? row?.cost
155
- if (!source || typeof source !== 'object') return undefined
156
- const inputPerToken = firstNumber(source, PRICING_ALIASES.inputPerToken, { allowZero: true })
157
- const outputPerToken = firstNumber(source, PRICING_ALIASES.outputPerToken, { allowZero: true })
158
- if (inputPerToken === undefined && outputPerToken === undefined) return undefined
159
- const pricing = {
160
- ...(inputPerToken === undefined ? {} : { inputPerToken }),
161
- ...(outputPerToken === undefined ? {} : { outputPerToken }),
162
- }
163
- if (typeof source.currency === 'string' && source.currency.trim()) pricing.currency = source.currency.trim()
164
- if (typeof source.unit === 'string' && source.unit.trim()) pricing.unit = source.unit.trim()
165
- return pricing
166
- }
167
-
168
- export function normalizeModel(provider, row) {
169
- if (typeof row === 'string') {
170
- row = { id: row }
171
- }
172
- if (!row || typeof row !== 'object') return null
173
- const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
174
- if (!id) return null
175
- const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
176
- const contextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
177
- const maxTokens = firstNumber(row, FIELD_ALIASES.maxTokens)
178
- const capabilities = normalizeCapabilities(row)
179
- const pricing = normalizePricing(row)
180
- const tags = normalizeTags(row)
181
- const aliases = normalizeAliases(row, id)
182
- const lifecycle = normalizeLifecycle(row)
183
- return {
184
- provider,
185
- id,
186
- name,
187
- ...(contextWindow === undefined ? {} : { contextWindow }),
188
- ...(maxTokens === undefined ? {} : { maxTokens }),
189
- ...(row.description ? { description: String(row.description) } : {}),
190
- ...(capabilities === undefined ? {} : { capabilities }),
191
- ...(pricing === undefined ? {} : { pricing }),
192
- ...(tags === undefined ? {} : { tags }),
193
- ...(aliases.length === 0 ? {} : { aliases }),
194
- ...(lifecycle === undefined ? {} : { lifecycle }),
195
- }
196
- }
197
-
198
- export function normalizeModels(provider, rows) {
199
- const out = []
200
- const seen = new Set()
201
- for (const row of rows ?? []) {
202
- const model = normalizeModel(provider, row)
203
- if (!model || seen.has(model.id)) continue
204
- seen.add(model.id)
205
- out.push(model)
206
- }
207
- return out
208
- }
209
-
210
- export function diffModels(previous, next) {
211
- const sortModels = (rows) => normalizeModels('', rows).sort((left, right) => left.id.localeCompare(right.id))
212
- const beforeRows = sortModels(previous)
213
- const afterRows = sortModels(next)
214
- const before = new Map(beforeRows.map((row) => [row.id, row]))
215
- const after = new Map(afterRows.map((row) => [row.id, row]))
216
- const added = []
217
- const removed = []
218
- const renamed = []
219
- const changed = []
220
- const renamedBefore = new Set()
221
- for (const [id, model] of after) {
222
- if (!before.has(id)) continue
223
- if (JSON.stringify(before.get(id)) !== JSON.stringify(model)) {
224
- changed.push({ before: before.get(id), after: model })
225
- }
226
- }
227
- for (const [id, model] of after) {
228
- if (before.has(id)) continue
229
- const match = beforeRows.find((candidate) => {
230
- if (renamedBefore.has(candidate.id)) return false
231
- return model.aliases?.includes(candidate.id) || candidate.aliases?.includes(model.id)
232
- })
233
- if (match) {
234
- renamed.push({ before: match, after: model })
235
- renamedBefore.add(match.id)
236
- } else {
237
- added.push(model)
238
- }
239
- }
240
- for (const [id, model] of before) {
241
- if (!after.has(id) && !renamedBefore.has(id)) removed.push(model)
242
- }
243
- added.sort((left, right) => left.id.localeCompare(right.id))
244
- removed.sort((left, right) => left.id.localeCompare(right.id))
245
- renamed.sort((left, right) => left.before.id.localeCompare(right.before.id))
246
- changed.sort((left, right) => left.before.id.localeCompare(right.before.id))
247
- return {
248
- added,
249
- removed,
250
- renamed,
251
- changed,
252
- hasChanges: added.length > 0 || removed.length > 0 || renamed.length > 0 || changed.length > 0,
253
- }
254
- }
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
+ const CAPABILITY_TOKENS = Object.freeze({
21
+ vision: new Set(['vision', 'image', 'imageinput', 'multimodal']),
22
+ tools: new Set(['tools', 'tooluse', 'toolchoice', 'functioncalling', 'functioncall']),
23
+ reasoning: new Set(['reasoning', 'reasoningeffort', 'thinking', 'extendedthinking']),
24
+ embeddings: new Set(['embedding', 'embeddings']),
25
+ })
26
+
27
+ const ID_ALIASES = Object.freeze(['aliases', 'alias', 'previousIds', 'previous_ids', 'previousId', 'previous_id'])
28
+
29
+ const PRICING_ALIASES = Object.freeze({
30
+ inputPerToken: ['inputPerToken', 'input_per_token', 'inputPricePerToken', 'input_price_per_token', 'inputTokenPrice', 'input_token_price'],
31
+ outputPerToken: ['outputPerToken', 'output_per_token', 'outputPricePerToken', 'output_price_per_token', 'outputTokenPrice', 'output_token_price'],
32
+ cacheReadPerToken: ['cacheReadPerToken', 'cache_read_per_token', 'cacheReadPricePerToken', 'cache_read_price_per_token', 'cachedInputPerToken', 'cached_input_per_token'],
33
+ cacheWritePerToken: ['cacheWritePerToken', 'cache_write_per_token', 'cacheWritePricePerToken', 'cache_write_price_per_token'],
34
+ })
35
+
36
+ function numericValue(value, { allowZero = false } = {}) {
37
+ if (typeof value !== 'number' && typeof value !== 'string') return undefined
38
+ if (typeof value === 'string' && value.trim() === '') return undefined
39
+ const number = Number(value)
40
+ if (!Number.isFinite(number) || (allowZero ? number < 0 : number <= 0)) return undefined
41
+ return number
42
+ }
43
+
44
+ function firstNumber(row, fields, options) {
45
+ for (const field of fields) {
46
+ const value = numericValue(row?.[field], options)
47
+ if (value !== undefined) return value
48
+ }
49
+ return undefined
50
+ }
51
+
52
+ function booleanValue(value) {
53
+ if (value === true || value === false) return value
54
+ if (typeof value !== 'string') return undefined
55
+ if (value.trim().toLowerCase() === 'true') return true
56
+ if (value.trim().toLowerCase() === 'false') return false
57
+ return undefined
58
+ }
59
+
60
+ function capabilityToken(value) {
61
+ if (typeof value !== 'string') return undefined
62
+ const token = value.trim().toLowerCase().replaceAll(/[_-]/g, '')
63
+ return Object.entries(CAPABILITY_TOKENS).find(([, tokens]) => tokens.has(token))?.[0]
64
+ }
65
+
66
+ function normalizeCapabilities(row) {
67
+ const result = {}
68
+ const assign = (key, value) => {
69
+ const normalized = booleanValue(value)
70
+ if (normalized !== undefined) result[key] = normalized
71
+ }
72
+
73
+ for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
74
+ for (const field of fields) {
75
+ if (row?.[field] !== undefined) assign(key, row[field])
76
+ }
77
+ }
78
+
79
+ const sources = [row?.capabilities, row?.supportedCapabilities, row?.supported_capabilities]
80
+ for (const source of sources) {
81
+ if (Array.isArray(source)) {
82
+ for (const token of source) {
83
+ const key = capabilityToken(token)
84
+ if (key) result[key] = true
85
+ }
86
+ } else if (source && typeof source === 'object') {
87
+ for (const [key, fields] of Object.entries(CAPABILITY_ALIASES)) {
88
+ for (const field of fields) {
89
+ if (source[field] !== undefined) assign(key, source[field])
90
+ }
91
+ }
92
+ for (const [token, value] of Object.entries(source)) {
93
+ const key = capabilityToken(token)
94
+ if (key) assign(key, value)
95
+ }
96
+ }
97
+ }
98
+
99
+ const modalitySources = [row?.modalities, row?.inputModalities, row?.input_modalities]
100
+ for (const source of modalitySources) {
101
+ if (!Array.isArray(source)) continue
102
+ for (const token of source) {
103
+ const key = capabilityToken(token)
104
+ if (key) result[key] = true
105
+ }
106
+ }
107
+
108
+ const parameterSources = [row?.supportedParameters, row?.supported_parameters]
109
+ for (const source of parameterSources) {
110
+ if (!Array.isArray(source)) continue
111
+ for (const token of source) {
112
+ const key = capabilityToken(token)
113
+ if (key) result[key] = true
114
+ }
115
+ }
116
+ return Object.keys(result).length > 0 ? result : undefined
117
+ }
118
+
119
+ function normalizeTags(row) {
120
+ const source = row?.tags ?? row?.labels
121
+ if (!Array.isArray(source)) return undefined
122
+ const tags = [...new Set(source
123
+ .filter((tag) => typeof tag === 'string' && tag.trim())
124
+ .map((tag) => tag.trim())
125
+ .slice(0, 50))]
126
+ return tags.length > 0 ? tags : undefined
127
+ }
128
+
129
+ function normalizeAliases(row, id) {
130
+ const aliases = []
131
+ for (const field of ID_ALIASES) {
132
+ const source = row?.[field]
133
+ const values = Array.isArray(source) ? source : source === undefined || source === null ? [] : [source]
134
+ for (const value of values) {
135
+ if (typeof value !== 'string') continue
136
+ const alias = value.trim()
137
+ if (alias && alias !== id && !aliases.includes(alias)) aliases.push(alias)
138
+ }
139
+ }
140
+ return aliases.slice(0, 20)
141
+ }
142
+
143
+ function normalizeLifecycle(row) {
144
+ const source = row?.lifecycle && typeof row.lifecycle === 'object' ? row.lifecycle : {}
145
+ const deprecated = booleanValue(source.deprecated ?? row?.deprecated ?? row?.isDeprecated)
146
+ 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
147
+ const deprecationDate = typeof date === 'string' && date.trim() ? date.trim() : undefined
148
+ if (deprecated === undefined && deprecationDate === undefined) return undefined
149
+ return {
150
+ ...(deprecated === undefined ? {} : { deprecated }),
151
+ ...(deprecationDate === undefined ? {} : { deprecationDate }),
152
+ }
153
+ }
154
+
155
+ function normalizePricing(row) {
156
+ const source = row?.pricing ?? row?.price ?? row?.cost
157
+ if (!source || typeof source !== 'object') return undefined
158
+ const inputPerToken = firstNumber(source, PRICING_ALIASES.inputPerToken, { allowZero: true })
159
+ const outputPerToken = firstNumber(source, PRICING_ALIASES.outputPerToken, { allowZero: true })
160
+ const cacheReadPerToken = firstNumber(source, PRICING_ALIASES.cacheReadPerToken, { allowZero: true })
161
+ const cacheWritePerToken = firstNumber(source, PRICING_ALIASES.cacheWritePerToken, { allowZero: true })
162
+ if (inputPerToken === undefined && outputPerToken === undefined && cacheReadPerToken === undefined && cacheWritePerToken === undefined) return undefined
163
+ const pricing = {
164
+ ...(inputPerToken === undefined ? {} : { inputPerToken }),
165
+ ...(outputPerToken === undefined ? {} : { outputPerToken }),
166
+ ...(cacheReadPerToken === undefined ? {} : { cacheReadPerToken }),
167
+ ...(cacheWritePerToken === undefined ? {} : { cacheWritePerToken }),
168
+ }
169
+ if (typeof source.currency === 'string' && source.currency.trim()) pricing.currency = source.currency.trim()
170
+ if (typeof source.unit === 'string' && source.unit.trim()) pricing.unit = source.unit.trim()
171
+ return pricing
172
+ }
173
+
174
+ export function normalizeModel(provider, row) {
175
+ if (typeof row === 'string') {
176
+ row = { id: row }
177
+ }
178
+ if (!row || typeof row !== 'object') return null
179
+ const id = String(row.id ?? row.slug ?? row.model ?? '').trim()
180
+ if (!id) return null
181
+ const name = String(row.name ?? row.displayName ?? row.display_name ?? id).trim() || id
182
+ const contextWindow = firstNumber(row, FIELD_ALIASES.contextWindow)
183
+ const maxTokens = firstNumber(row, FIELD_ALIASES.maxTokens)
184
+ const capabilities = normalizeCapabilities(row)
185
+ const pricing = normalizePricing(row)
186
+ const tags = normalizeTags(row)
187
+ const aliases = normalizeAliases(row, id)
188
+ const lifecycle = normalizeLifecycle(row)
189
+ return {
190
+ provider,
191
+ id,
192
+ name,
193
+ ...(contextWindow === undefined ? {} : { contextWindow }),
194
+ ...(maxTokens === undefined ? {} : { maxTokens }),
195
+ ...(row.description ? { description: String(row.description) } : {}),
196
+ ...(capabilities === undefined ? {} : { capabilities }),
197
+ ...(pricing === undefined ? {} : { pricing }),
198
+ ...(tags === undefined ? {} : { tags }),
199
+ ...(aliases.length === 0 ? {} : { aliases }),
200
+ ...(lifecycle === undefined ? {} : { lifecycle }),
201
+ }
202
+ }
203
+
204
+ export function normalizeModels(provider, rows) {
205
+ const out = []
206
+ const seen = new Set()
207
+ for (const row of rows ?? []) {
208
+ const model = normalizeModel(provider, row)
209
+ if (!model || seen.has(model.id)) continue
210
+ seen.add(model.id)
211
+ out.push(model)
212
+ }
213
+ return out
214
+ }
215
+
216
+ export function diffModels(previous, next) {
217
+ const sortModels = (rows) => normalizeModels('', rows).sort((left, right) => left.id.localeCompare(right.id))
218
+ const beforeRows = sortModels(previous)
219
+ const afterRows = sortModels(next)
220
+ const before = new Map(beforeRows.map((row) => [row.id, row]))
221
+ const after = new Map(afterRows.map((row) => [row.id, row]))
222
+ const added = []
223
+ const removed = []
224
+ const renamed = []
225
+ const changed = []
226
+ const renamedBefore = new Set()
227
+ for (const [id, model] of after) {
228
+ if (!before.has(id)) continue
229
+ if (JSON.stringify(before.get(id)) !== JSON.stringify(model)) {
230
+ changed.push({ before: before.get(id), after: model })
231
+ }
232
+ }
233
+ for (const [id, model] of after) {
234
+ if (before.has(id)) continue
235
+ const match = beforeRows.find((candidate) => {
236
+ if (renamedBefore.has(candidate.id)) return false
237
+ return model.aliases?.includes(candidate.id) || candidate.aliases?.includes(model.id)
238
+ })
239
+ if (match) {
240
+ renamed.push({ before: match, after: model })
241
+ renamedBefore.add(match.id)
242
+ } else {
243
+ added.push(model)
244
+ }
245
+ }
246
+ for (const [id, model] of before) {
247
+ if (!after.has(id) && !renamedBefore.has(id)) removed.push(model)
248
+ }
249
+ added.sort((left, right) => left.id.localeCompare(right.id))
250
+ removed.sort((left, right) => left.id.localeCompare(right.id))
251
+ renamed.sort((left, right) => left.before.id.localeCompare(right.before.id))
252
+ changed.sort((left, right) => left.before.id.localeCompare(right.before.id))
253
+ return {
254
+ added,
255
+ removed,
256
+ renamed,
257
+ changed,
258
+ hasChanges: added.length > 0 || removed.length > 0 || renamed.length > 0 || changed.length > 0,
259
+ }
260
+ }