1- /**
2- * Syncs AI provider model lists from models.dev
3- *
4- * This script fetches the latest AI model data from models.dev and updates
5- * our provider files accordingly. It filters for text-only models and excludes
6- * embedding models.
7- *
8- * Data source: https://models.dev (MIT License)
9- * API endpoint: https://models.dev/api.json
10- * GitHub: https://github.com/anomalyco/models.dev
11- *
12- * Usage:
13- * npx tsx sync-models.ts # Check for differences
14- * npx tsx sync-models.ts --update # Update provider files
15- */
16-
171import { AiProviderEnum } from '@openops/shared' ;
18- import fs from 'fs' ;
19- import path from 'path' ;
2+ import fs from 'node: fs' ;
3+ import path from 'node: path' ;
204
215interface ModelData {
226 id : string ;
23- name : string ;
24- family : string ;
257 modalities : {
268 input : string [ ] ;
279 output : string [ ] ;
2810 } ;
29- cost ?: {
30- input : number ;
31- output : number ;
32- } ;
33- limit ?: {
34- context : number ;
35- output : number ;
36- } ;
3711}
3812
3913interface ProviderData {
40- id : string ;
41- name : string ;
4214 models : Record < string , ModelData > ;
4315}
4416
@@ -50,7 +22,6 @@ export const MODELS_DEV_KEYS: Partial<Record<AiProviderEnum, string>> = {
5022 [ AiProviderEnum . ANTHROPIC ] : 'anthropic' ,
5123 [ AiProviderEnum . CEREBRAS ] : 'cerebras' ,
5224 [ AiProviderEnum . COHERE ] : 'cohere' ,
53- // [AiProviderEnum.DEEPINFRA]: 'deepinfra', // Temporarily disabled until models.dev PR is merged
5425 [ AiProviderEnum . DEEPSEEK ] : 'deepseek' ,
5526 [ AiProviderEnum . GOOGLE ] : 'google' ,
5627 [ AiProviderEnum . GOOGLE_VERTEX ] : 'google-vertex' ,
@@ -62,29 +33,6 @@ export const MODELS_DEV_KEYS: Partial<Record<AiProviderEnum, string>> = {
6233 [ AiProviderEnum . XAI ] : 'xai' ,
6334} ;
6435
65- function normalizeProviderKey ( key : string ) : string {
66- return key . toLowerCase ( ) . replace ( / [ - _ ] / g, '' ) ;
67- }
68-
69- function getProviderFiles ( ) : string [ ] {
70- const providersDir = path . join ( __dirname , 'providers' ) ;
71- return fs
72- . readdirSync ( providersDir )
73- . filter ( ( file ) => file . endsWith ( '.ts' ) && file !== 'index.ts' )
74- . map ( ( file ) => file . replace ( '.ts' , '' ) ) ;
75- }
76-
77- function findMatchingProviderFile ( modelsDevKey : string ) : string | null {
78- const providerFiles = getProviderFiles ( ) ;
79- const normalizedKey = normalizeProviderKey ( modelsDevKey ) ;
80-
81- return (
82- providerFiles . find (
83- ( file ) => normalizeProviderKey ( file ) === normalizedKey ,
84- ) || null
85- ) ;
86- }
87-
8836async function fetchModelsDevData ( ) : Promise < ModelsDevAPI > {
8937 const response = await fetch ( 'https://models.dev/api.json' ) ;
9038 if ( ! response . ok ) {
@@ -93,234 +41,100 @@ async function fetchModelsDevData(): Promise<ModelsDevAPI> {
9341 return response . json ( ) ;
9442}
9543
96- function isEmbeddingModel ( modelId : string ) : boolean {
97- return modelId . toLowerCase ( ) . includes ( 'embedding' ) ;
98- }
99-
10044function filterTextOnlyModels ( models : Record < string , ModelData > ) : string [ ] {
10145 return Object . values ( models )
10246 . filter ( ( model ) => {
10347 const outputModalities = model . modalities ?. output || [ ] ;
10448 const isTextOnly =
10549 outputModalities . length === 1 && outputModalities [ 0 ] === 'text' ;
106-
107- if ( ! isTextOnly ) return false ;
108-
109- if ( isEmbeddingModel ( model . id ) ) return false ;
110-
111- return true ;
50+ const isEmbedding = model . id . toLowerCase ( ) . includes ( 'embedding' ) ;
51+ return isTextOnly && ! isEmbedding ;
11252 } )
11353 . map ( ( model ) => model . id )
11454 . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
11555}
11656
117- function getProviderFilePath ( providerFileName : string ) : string {
118- return path . join ( __dirname , 'providers' , `${ providerFileName } .ts` ) ;
119- }
120-
121- function getCurrentModels ( providerFileName : string ) : string [ ] {
122- const filePath = getProviderFilePath ( providerFileName ) ;
123-
124- if ( ! fs . existsSync ( filePath ) ) {
125- console . warn ( `⚠️ Provider file not found: ${ filePath } ` ) ;
126- return [ ] ;
127- }
57+ function getCurrentModels ( providerFile : string ) : string [ ] {
58+ const filePath = path . join ( __dirname , 'providers' , `${ providerFile } .ts` ) ;
59+ if ( ! fs . existsSync ( filePath ) ) return [ ] ;
12860
12961 const content = fs . readFileSync ( filePath , 'utf-8' ) ;
62+ const match = content . match ( / c o n s t \s + \w + M o d e l s \s * = \s * \[ ( [ \s \S ] * ?) \] ; / ) ;
63+ if ( ! match ) return [ ] ;
13064
131- const modelsArrayMatch = content . match (
132- / c o n s t \s + \w + M o d e l s \s * = \s * \[ ( [ \s \S ] * ?) \] ; / ,
133- ) ;
134-
135- if ( ! modelsArrayMatch ) {
136- console . warn ( `⚠️ Could not find models array in ${ providerFileName } .ts` ) ;
137- return [ ] ;
138- }
139-
140- const modelsString = modelsArrayMatch [ 1 ] ;
141- const models = modelsString
65+ return match [ 1 ]
14266 . split ( ',' )
143- . map ( ( line ) => {
144- const match = line . match ( / [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / ) ;
145- return match ? match [ 1 ] : null ;
146- } )
147- . filter ( ( model ) : model is string => model !== null ) ;
148-
149- return models . sort ( ) ;
67+ . map ( ( line ) => line . match ( / [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / ) ?. [ 1 ] )
68+ . filter ( ( model ) : model is string => model !== null )
69+ . sort ( ) ;
15070}
15171
152- function updateProviderFile (
153- providerFileName : string ,
154- newModels : string [ ] ,
155- ) : void {
156- const filePath = getProviderFilePath ( providerFileName ) ;
157-
158- if ( ! fs . existsSync ( filePath ) ) {
159- console . warn ( `⚠️ Cannot update: Provider file not found: ${ filePath } ` ) ;
160- return ;
161- }
162-
72+ function updateProviderFile ( providerFile : string , models : string [ ] ) : void {
73+ const filePath = path . join ( __dirname , 'providers' , `${ providerFile } .ts` ) ;
16374 const content = fs . readFileSync ( filePath , 'utf-8' ) ;
75+ const match = content . match ( / c o n s t \s + ( \w + M o d e l s ) \s * = \s * \[ ( [ \s \S ] * ?) \] ; / ) ;
76+ if ( ! match ) return ;
16477
165- const modelsArrayMatch = content . match (
166- / c o n s t \s + ( \w + M o d e l s ) \s * = \s * \[ ( [ \s \S ] * ?) \] ; / ,
167- ) ;
168-
169- if ( ! modelsArrayMatch ) {
170- console . warn (
171- `⚠️ Cannot update: Could not find models array in ${ providerFileName } .ts` ,
172- ) ;
173- return ;
174- }
175-
176- const arrayName = modelsArrayMatch [ 1 ] ;
177-
178- const formattedModels = newModels . map ( ( model ) => ` '${ model } ',` ) . join ( '\n' ) ;
179-
78+ const arrayName = match [ 1 ] ;
79+ const formattedModels = models . map ( ( model ) => ` '${ model } ',` ) . join ( '\n' ) ;
18080 const newArray = `const ${ arrayName } = [\n${ formattedModels } \n];` ;
181-
18281 const updatedContent = content . replace (
18382 / c o n s t \s + \w + M o d e l s \s * = \s * \[ ( [ \s \S ] * ?) \] ; / ,
18483 newArray ,
18584 ) ;
18685
18786 fs . writeFileSync ( filePath , updatedContent , 'utf-8' ) ;
188-
189- console . log ( ` ✅ Updated ${ providerFileName } .ts` ) ;
19087}
19188
192- function compareModels (
193- current : string [ ] ,
194- latest : string [ ] ,
195- ) : {
196- added : string [ ] ;
197- removed : string [ ] ;
198- unchanged : string [ ] ;
199- } {
200- const currentSet = new Set ( current ) ;
201- const latestSet = new Set ( latest ) ;
202-
203- const added = latest . filter ( ( model ) => ! currentSet . has ( model ) ) ;
204- const removed = current . filter ( ( model ) => ! latestSet . has ( model ) ) ;
205- const unchanged = current . filter ( ( model ) => latestSet . has ( model ) ) ;
206-
207- return { added, removed, unchanged } ;
208- }
89+ function findProviderFile ( modelsDevKey : string ) : string | null {
90+ const providersDir = path . join ( __dirname , 'providers' ) ;
91+ const files = fs
92+ . readdirSync ( providersDir )
93+ . filter ( ( file ) => file . endsWith ( '.ts' ) && file !== 'index.ts' )
94+ . map ( ( file ) => file . replace ( '.ts' , '' ) ) ;
20995
210- function checkForUnmappedProviders ( ) : string [ ] {
211- const providerFiles = getProviderFiles ( ) ;
212- const mappedKeys = Object . values ( MODELS_DEV_KEYS ) . filter (
213- ( key ) : key is string => key !== undefined ,
96+ const normalizedKey = modelsDevKey . toLowerCase ( ) . replaceAll ( / [ - _ ] / g, '' ) ;
97+ return (
98+ files . find (
99+ ( file ) => file . toLowerCase ( ) . replaceAll ( / [ - _ ] / g, '' ) === normalizedKey ,
100+ ) || null
214101 ) ;
215-
216- const unmappedFiles = providerFiles . filter ( ( file ) => {
217- const normalizedFile = normalizeProviderKey ( file ) ;
218- return ! mappedKeys . some (
219- ( key ) => normalizeProviderKey ( key ) === normalizedFile ,
220- ) ;
221- } ) ;
222-
223- return unmappedFiles ;
224102}
225103
226104async function main ( ) {
227105 const shouldUpdate = process . argv . includes ( '--update' ) ;
228-
229- console . log ( '🔄 Fetching models from models.dev...\n' ) ;
230-
231- const unmappedProviders = checkForUnmappedProviders ( ) ;
232- if ( unmappedProviders . length > 0 ) {
233- console . log ( '⚠️ Warning: Found provider files not in MODELS_DEV_KEYS:' ) ;
234- unmappedProviders . forEach ( ( file ) =>
235- console . log ( ` - ${ file } .ts (not synced)` ) ,
236- ) ;
237- console . log ( ' Add these to MODELS_DEV_KEYS if they should be synced.\n' ) ;
238- }
239-
240106 const modelsDevData = await fetchModelsDevData ( ) ;
241107
242- console . log ( '📊 Comparing models for each provider:\n' ) ;
243- console . log ( '=' . repeat ( 80 ) ) ;
244-
245- let totalAdded = 0 ;
246- let totalRemoved = 0 ;
247108 let hasChanges = false ;
248109
249110 for ( const [ provider , modelsDevKey ] of Object . entries ( MODELS_DEV_KEYS ) ) {
250111 if ( ! modelsDevKey ) continue ;
251112
252113 const providerData = modelsDevData [ modelsDevKey ] ;
114+ if ( ! providerData ) continue ;
253115
254- if ( ! providerData ) {
255- console . log ( `\n❌ ${ provider } ` ) ;
256- console . log ( ` Provider "${ modelsDevKey } " not found in models.dev` ) ;
257- continue ;
258- }
259-
260- const providerFile = findMatchingProviderFile ( modelsDevKey ) ;
261-
262- if ( ! providerFile ) {
263- console . log ( `\n❌ ${ provider } ` ) ;
264- console . log ( ` No matching provider file found for "${ modelsDevKey } "` ) ;
265- console . log ( ` Available files: ${ getProviderFiles ( ) . join ( ', ' ) } ` ) ;
266- continue ;
267- }
116+ const providerFile = findProviderFile ( modelsDevKey ) ;
117+ if ( ! providerFile ) continue ;
268118
269119 const latestModels = filterTextOnlyModels ( providerData . models ) ;
270120 const currentModels = getCurrentModels ( providerFile ) ;
271121
272- const diff = compareModels ( currentModels , latestModels ) ;
273-
274- const hasProviderChanges = diff . added . length > 0 || diff . removed . length > 0 ;
122+ const added = latestModels . filter ( ( m ) => ! currentModels . includes ( m ) ) ;
123+ const removed = currentModels . filter ( ( m ) => ! latestModels . includes ( m ) ) ;
275124
276- if ( hasProviderChanges ) {
125+ if ( added . length > 0 || removed . length > 0 ) {
277126 hasChanges = true ;
278- }
127+ console . log ( `${ provider } :` ) ;
128+ if ( added . length > 0 ) console . log ( ` +${ added . length } ` ) ;
129+ if ( removed . length > 0 ) console . log ( ` -${ removed . length } ` ) ;
279130
280- const icon = hasProviderChanges ? '🔄' : '✅' ;
281- console . log ( `\n${ icon } ${ provider } (${ modelsDevKey } )` ) ;
282- console . log ( ` File: ${ providerFile } .ts` ) ;
283- console . log ( ` Current: ${ currentModels . length } models` ) ;
284- console . log ( ` Latest: ${ latestModels . length } models` ) ;
285-
286- if ( diff . added . length > 0 ) {
287- console . log ( ` ➕ Added (${ diff . added . length } ):` ) ;
288- diff . added . forEach ( ( model ) => console . log ( ` - ${ model } ` ) ) ;
289- totalAdded += diff . added . length ;
290- }
291-
292- if ( diff . removed . length > 0 ) {
293- console . log ( ` ➖ Removed (${ diff . removed . length } ):` ) ;
294- diff . removed . forEach ( ( model ) => console . log ( ` - ${ model } ` ) ) ;
295- totalRemoved += diff . removed . length ;
296- }
297-
298- if ( ! hasProviderChanges ) {
299- console . log ( ` ✓ No changes` ) ;
300- }
301-
302- if ( hasProviderChanges && shouldUpdate ) {
303- updateProviderFile ( providerFile , latestModels ) ;
131+ if ( shouldUpdate ) {
132+ updateProviderFile ( providerFile , latestModels ) ;
133+ }
304134 }
305135 }
306136
307- console . log ( '\n' + '=' . repeat ( 80 ) ) ;
308- console . log ( '\n📈 Summary:' ) ;
309- console . log ( ` Total models added: ${ totalAdded } ` ) ;
310- console . log ( ` Total models removed: ${ totalRemoved } ` ) ;
311-
312- if ( hasChanges ) {
313- if ( shouldUpdate ) {
314- console . log ( '\n✅ Provider files updated successfully!' ) ;
315- console . log ( ' Please review the changes and commit.' ) ;
316- } else {
317- console . log ( '\n⚠️ Changes detected! Run with --update to apply them.' ) ;
318- }
319- process . exit ( 1 ) ;
320- } else {
321- console . log ( '\n✅ All providers are up to date!' ) ;
322- process . exit ( 0 ) ;
323- }
137+ process . exit ( hasChanges ? 1 : 0 ) ;
324138}
325139
326140if ( require . main === module ) {
0 commit comments