Skip to content

Commit 24f25d2

Browse files
clean up
1 parent 04709a9 commit 24f25d2

1 file changed

Lines changed: 43 additions & 229 deletions

File tree

packages/openops/src/lib/ai/sync-models.ts

Lines changed: 43 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,16 @@
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-
171
import { 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

215
interface 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

3913
interface 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-
8836
async 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-
10044
function 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(/const\s+\w+Models\s*=\s*\[([\s\S]*?)\];/);
63+
if (!match) return [];
13064

131-
const modelsArrayMatch = content.match(
132-
/const\s+\w+Models\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(/const\s+(\w+Models)\s*=\s*\[([\s\S]*?)\];/);
76+
if (!match) return;
16477

165-
const modelsArrayMatch = content.match(
166-
/const\s+(\w+Models)\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
/const\s+\w+Models\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

226104
async 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

326140
if (require.main === module) {

0 commit comments

Comments
 (0)