#!/usr/bin/env tsx
import * as fs from 'fs'
import * as path from 'path'
interface ModelsDevProvider {
id: string
env: string[]
npm: string
api: string
name: string
doc: string
models: Record<string, ModelsDevModel>
}
interface ModelsDevModel {
id: string
name: string
family: string
attachment: boolean
reasoning: boolean
tool_call: boolean
temperature: boolean
structured_output?: boolean
knowledge?: string
release_date?: string
last_updated?: string
modalities: {
input: string[]
output: string[]
}
open_weights: boolean
limit: {
context: number
output: number
}
cost: {
input: number
output: number
cache_read?: number
cache_write?: number
}
}
interface ConfigProvider {
id: string
name: string
website: string
apiKeyUrl: string
apiBaseUrl: string
logoUrl: string
supportsToolCalls: boolean
supportsReasoning: boolean
supportsStreaming: boolean
models: ConfigModel[]
}
interface ConfigModel {
id: string
name: string
contextWindow: number
maxTokens: number
maxOutputTokens?: number
price: {
input: number
output: number
currency: string
per: string
}
capabilities: {
imageInput: boolean
imageOutput: boolean
audioInput: boolean
audioOutput: boolean
toolCalls: boolean
reasoning: boolean
structuredOutputs: boolean
functionCalling: boolean
}
isDefault: boolean
isDeprecated: boolean
}
interface Config {
version: string
lastUpdated: string
providers: ConfigProvider[]
}
const PROVIDER_ID_MAP: Record<string, string> = {
openai: 'openai',
anthropic: 'anthropic',
deepseek: 'deepseek',
zhipuai: 'zhipuai',
moonshotai: 'moonshotai',
google: 'google',
xai: 'xai',
}
const PROVIDER_URLS: Record<string, { website: string; apiKeyUrl: string }> = {
openai: {
website: 'https://openai.com',
apiKeyUrl: 'https://platform.openai.com/api-keys',
},
anthropic: {
website: 'https://anthropic.com',
apiKeyUrl: 'https://console.anthropic.com',
},
deepseek: {
website: 'https://deepseek.com',
apiKeyUrl: 'https://platform.deepseek.com',
},
zhipuai: {
website: 'https://zhipu.ai',
apiKeyUrl: 'https://zhipu.ai/manage-apikey/apikey-list',
},
moonshotai: {
website: 'https://moonshot.ai',
apiKeyUrl: 'https://platform.moonshot.ai',
},
google: {
website: 'https://ai.google.com',
apiKeyUrl: 'https://makersuite.google.com/app/apikey',
},
xai: {
website: 'https://x.ai',
apiKeyUrl: 'https://console.x.ai',
},
}
const PROVIDER_LOGOS: Record<string, string> = {
openai:
'https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/2048px-ChatGPT_logo.svg.png',
anthropic:
'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Anthropic-logo.svg/1200px-Anthropic-logo.svg.png',
deepseek: 'https://deepseek.com/favicon.ico',
zhipuai: 'https://zhipu.ai/favicon.ico',
moonshotai: 'https://moonshot.ai/favicon.ico',
google:
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/2560px-Google_2015_logo.svg.png',
xai: 'https://x.ai/favicon.ico',
}
async function fetchModelsDevData(): Promise<Record<string, ModelsDevProvider>> {
const response = await fetch('https://models.dev/api.json')
if (!response.ok) {
throw new Error(`Failed to fetch models.dev data: ${response.status} ${response.statusText}`)
}
return await response.json()
}
function loadTempModels(): Record<string, ModelsDevProvider> {
const tempPath = path.join(process.cwd(), 'temp/models.json')
if (!fs.existsSync(tempPath)) {
return {}
}
const content = fs.readFileSync(tempPath, 'utf-8').trim()
if (!content) {
return {}
}
try {
return JSON.parse(content)
} catch {
return {}
}
}
function loadCurrentConfig(): Config {
const configPath = path.join(process.cwd(), 'lib/ai-providers-config.json')
if (!fs.existsSync(configPath)) {
return createDefaultConfig()
}
const content = fs.readFileSync(configPath, 'utf-8').trim()
if (!content) {
return createDefaultConfig()
}
try {
return JSON.parse(content)
} catch {
return createDefaultConfig()
}
}
function createDefaultConfig(): Config {
return {
version: '1.0.0',
lastUpdated: new Date().toISOString().split('T')[0],
providers: [],
}
}
function saveConfig(config: Config): void {
const configPath = path.join(process.cwd(), 'lib/ai-providers-config.json')
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
}
function transformModelFromDev(
modelId: string,
modelData: ModelsDevModel,
isDefault: boolean = false
): ConfigModel {
const modalitiesInput = modelData.modalities?.input || []
const modalitiesOutput = modelData.modalities?.output || []
return {
id: modelData.id,
name: modelData.name,
contextWindow: modelData.limit?.context || 0,
maxTokens: modelData.limit?.output || 4096,
maxOutputTokens: modelData.limit?.output || 4096,
price: {
input: modelData.cost?.input || 0,
output: modelData.cost?.output || 0,
currency: 'USD',
per: '1M tokens',
},
capabilities: {
imageInput: modalitiesInput.includes('image'),
imageOutput: modalitiesOutput.includes('image'),
audioInput: modalitiesInput.includes('audio'),
audioOutput: modalitiesOutput.includes('audio'),
toolCalls: modelData.tool_call || false,
reasoning: modelData.reasoning || false,
structuredOutputs: modelData.structured_output || true,
functionCalling: modelData.tool_call || false,
},
isDefault,
isDeprecated: false,
}
}
function pickDefaultByLastUpdated(models: ConfigModel[], devProvider: ModelsDevProvider): void {
if (models.length === 0) return
let defaultModel = models[0]
let latestDate = ''
for (const model of models) {
const lastUpdated = devProvider.models[model.id]?.last_updated
if (lastUpdated && lastUpdated > latestDate) {
latestDate = lastUpdated
defaultModel = model
}
}
models.forEach(m => {
m.isDefault = false
})
defaultModel.isDefault = true
}
function updateProviderFromDev(
existingProvider: ConfigProvider,
devProvider: ModelsDevProvider
): ConfigProvider {
const providerId = PROVIDER_ID_MAP[existingProvider.id] || existingProvider.id
const providerUrls = PROVIDER_URLS[providerId] || {
website: '',
apiKeyUrl: '',
}
const models: ConfigModel[] = []
const devModelIds = Object.keys(devProvider.models)
const existingModelIds = existingProvider.models.map(m => m.id)
const allModelIds = new Set([...existingModelIds, ...devModelIds])
for (const modelId of allModelIds) {
const devModel = devProvider.models[modelId]
const existingModel = existingProvider.models.find(m => m.id === modelId)
if (devModel) {
models.push(transformModelFromDev(modelId, devModel, false))
} else if (existingModel) {
models.push({ ...existingModel })
}
}
pickDefaultByLastUpdated(models, devProvider)
return {
...existingProvider,
id: existingProvider.id,
name: devProvider.name || existingProvider.name,
website: providerUrls.website || existingProvider.website,
apiKeyUrl: providerUrls.apiKeyUrl || existingProvider.apiKeyUrl,
apiBaseUrl: devProvider.api || existingProvider.apiBaseUrl,
logoUrl: PROVIDER_LOGOS[providerId] || existingProvider.logoUrl,
supportsToolCalls: models.some(m => m.capabilities.toolCalls),
supportsReasoning: models.some(m => m.capabilities.reasoning),
supportsStreaming: true,
models,
}
}
function createProviderFromDev(providerId: string, devProvider: ModelsDevProvider): ConfigProvider {
const providerUrls = PROVIDER_URLS[providerId] || {
website: '',
apiKeyUrl: '',
}
const models: ConfigModel[] = []
const devModelIds = Object.keys(devProvider.models)
for (const modelId of devModelIds) {
const devModel = devProvider.models[modelId]
if (devModel) {
models.push(transformModelFromDev(modelId, devModel, false))
}
}
pickDefaultByLastUpdated(models, devProvider)
return {
id: providerId,
name: devProvider.name || providerId,
website: providerUrls.website || '',
apiKeyUrl: providerUrls.apiKeyUrl || '',
apiBaseUrl: devProvider.api || '',
logoUrl: PROVIDER_LOGOS[providerId] || '',
supportsToolCalls: models.some(m => m.capabilities.toolCalls),
supportsReasoning: models.some(m => m.capabilities.reasoning),
supportsStreaming: true,
models,
}
}
async function main(): Promise<void> {
console.log('📦 Loading temp/models.json...')
const tempModels = loadTempModels()
console.log('📦 Loading current config...')
const currentConfig = loadCurrentConfig()
console.log('🌐 Fetching latest data from models.dev...')
let devData: Record<string, ModelsDevProvider>
try {
devData = await fetchModelsDevData()
} catch (err) {
console.log(' - Fetch failed, falling back to temp data...')
devData = tempModels
}
console.log('🔄 Updating providers...')
const updatedProviders: ConfigProvider[] = []
if (currentConfig.providers.length === 0) {
console.log(' - Creating new providers from temp models and dev data...')
for (const [providerId, tempProvider] of Object.entries(tempModels)) {
if (PROVIDER_ID_MAP[providerId]) {
console.log(` - Creating ${tempProvider.name} from temp...`)
const newProvider = createProviderFromDev(providerId, tempProvider)
updatedProviders.push(newProvider)
}
}
for (const [providerId, devProvider] of Object.entries(devData)) {
if (PROVIDER_ID_MAP[providerId] && !updatedProviders.find(p => p.id === providerId)) {
console.log(` - Creating ${devProvider.name} from dev...`)
const newProvider = createProviderFromDev(providerId, devProvider)
updatedProviders.push(newProvider)
}
}
} else {
for (const existingProvider of currentConfig.providers) {
const providerId = PROVIDER_ID_MAP[existingProvider.id]
if (!providerId) {
console.log(` - Skipping ${existingProvider.name} (not in provider map)`)
continue
}
const tempProvider = tempModels[providerId]
const devProvider = devData[providerId]
if (tempProvider) {
console.log(` - Updating ${existingProvider.name} from temp...`)
const updatedProvider = updateProviderFromDev(existingProvider, tempProvider)
updatedProviders.push(updatedProvider)
} else if (devProvider) {
console.log(` - Updating ${existingProvider.name} from dev...`)
const updatedProvider = updateProviderFromDev(existingProvider, devProvider)
updatedProviders.push(updatedProvider)
} else {
console.log(` - ${existingProvider.name} (no new data)`)
updatedProviders.push(existingProvider)
}
}
for (const providerId of Object.keys(PROVIDER_ID_MAP)) {
if (!updatedProviders.find(p => p.id === providerId)) {
const tempProvider = tempModels[providerId]
const devProvider = devData[providerId]
if (tempProvider) {
console.log(` - Adding ${providerId} from temp...`)
const newProvider = createProviderFromDev(providerId, tempProvider)
updatedProviders.push(newProvider)
} else if (devProvider) {
console.log(` - Adding ${providerId} from dev...`)
const newProvider = createProviderFromDev(providerId, devProvider)
updatedProviders.push(newProvider)
}
}
}
}
const updatedConfig: Config = {
version: currentConfig.version || '1.0.0',
lastUpdated: new Date().toISOString().split('T')[0],
providers: updatedProviders,
}
console.log('💾 Saving updated config...')
saveConfig(updatedConfig)
console.log('✅ Done! Config has been updated.')
}
main().catch(err => {
console.error('❌ Error:', err)
process.exit(1)
})