|
|
#!/usr/bin/env node |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs') |
|
|
const path = require('path') |
|
|
const https = require('https') |
|
|
const crypto = require('crypto') |
|
|
const pricingSource = require('../config/pricingSource') |
|
|
|
|
|
|
|
|
const colors = { |
|
|
reset: '\x1b[0m', |
|
|
bright: '\x1b[1m', |
|
|
red: '\x1b[31m', |
|
|
green: '\x1b[32m', |
|
|
yellow: '\x1b[33m', |
|
|
blue: '\x1b[36m', |
|
|
magenta: '\x1b[35m' |
|
|
} |
|
|
|
|
|
|
|
|
const log = { |
|
|
info: (msg) => console.log(`${colors.blue}[INFO]${colors.reset} ${msg}`), |
|
|
success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`), |
|
|
error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`), |
|
|
warn: (msg) => console.warn(`${colors.yellow}[WARNING]${colors.reset} ${msg}`) |
|
|
} |
|
|
|
|
|
|
|
|
const config = { |
|
|
dataDir: path.join(process.cwd(), 'data'), |
|
|
pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'), |
|
|
hashFile: path.join(process.cwd(), 'data', 'model_pricing.sha256'), |
|
|
pricingUrl: pricingSource.pricingUrl, |
|
|
fallbackFile: path.join( |
|
|
process.cwd(), |
|
|
'resources', |
|
|
'model-pricing', |
|
|
'model_prices_and_context_window.json' |
|
|
), |
|
|
backupFile: path.join(process.cwd(), 'data', 'model_pricing.backup.json'), |
|
|
timeout: 30000 |
|
|
} |
|
|
|
|
|
|
|
|
function ensureDataDir() { |
|
|
if (!fs.existsSync(config.dataDir)) { |
|
|
fs.mkdirSync(config.dataDir, { recursive: true }) |
|
|
log.info('Created data directory') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function backupExistingFile() { |
|
|
if (fs.existsSync(config.pricingFile)) { |
|
|
try { |
|
|
fs.copyFileSync(config.pricingFile, config.backupFile) |
|
|
log.info('Backed up existing pricing file') |
|
|
return true |
|
|
} catch (error) { |
|
|
log.warn(`Failed to backup existing file: ${error.message}`) |
|
|
return false |
|
|
} |
|
|
} |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
function restoreBackup() { |
|
|
if (fs.existsSync(config.backupFile)) { |
|
|
try { |
|
|
fs.copyFileSync(config.backupFile, config.pricingFile) |
|
|
log.info('Restored from backup') |
|
|
return true |
|
|
} catch (error) { |
|
|
log.error(`Failed to restore backup: ${error.message}`) |
|
|
return false |
|
|
} |
|
|
} |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
function downloadPricingData() { |
|
|
return new Promise((resolve, reject) => { |
|
|
log.info('正在从价格镜像分支拉取最新的模型价格数据...') |
|
|
log.info(`拉取地址: ${config.pricingUrl}`) |
|
|
|
|
|
const request = https.get(config.pricingUrl, (response) => { |
|
|
if (response.statusCode !== 200) { |
|
|
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)) |
|
|
return |
|
|
} |
|
|
|
|
|
let data = '' |
|
|
let downloadedBytes = 0 |
|
|
|
|
|
response.on('data', (chunk) => { |
|
|
data += chunk |
|
|
downloadedBytes += chunk.length |
|
|
|
|
|
process.stdout.write(`\rDownloading... ${Math.round(downloadedBytes / 1024)}KB`) |
|
|
}) |
|
|
|
|
|
response.on('end', () => { |
|
|
process.stdout.write('\n') |
|
|
try { |
|
|
const jsonData = JSON.parse(data) |
|
|
|
|
|
|
|
|
if (typeof jsonData !== 'object' || Object.keys(jsonData).length === 0) { |
|
|
throw new Error('Invalid pricing data structure') |
|
|
} |
|
|
|
|
|
|
|
|
const formattedJson = JSON.stringify(jsonData, null, 2) |
|
|
fs.writeFileSync(config.pricingFile, formattedJson) |
|
|
|
|
|
const hash = crypto.createHash('sha256').update(formattedJson).digest('hex') |
|
|
fs.writeFileSync(config.hashFile, `${hash}\n`) |
|
|
|
|
|
const modelCount = Object.keys(jsonData).length |
|
|
const fileSize = Math.round(fs.statSync(config.pricingFile).size / 1024) |
|
|
|
|
|
log.success(`Downloaded pricing data for ${modelCount} models (${fileSize}KB)`) |
|
|
|
|
|
|
|
|
const claudeModels = Object.keys(jsonData).filter((k) => k.includes('claude')).length |
|
|
const gptModels = Object.keys(jsonData).filter((k) => k.includes('gpt')).length |
|
|
const geminiModels = Object.keys(jsonData).filter((k) => k.includes('gemini')).length |
|
|
|
|
|
log.info('Model breakdown:') |
|
|
log.info(` - Claude models: ${claudeModels}`) |
|
|
log.info(` - GPT models: ${gptModels}`) |
|
|
log.info(` - Gemini models: ${geminiModels}`) |
|
|
log.info(` - Other models: ${modelCount - claudeModels - gptModels - geminiModels}`) |
|
|
|
|
|
resolve(jsonData) |
|
|
} catch (error) { |
|
|
reject(new Error(`Failed to parse pricing data: ${error.message}`)) |
|
|
} |
|
|
}) |
|
|
}) |
|
|
|
|
|
request.on('error', (error) => { |
|
|
reject(new Error(`Network error: ${error.message}`)) |
|
|
}) |
|
|
|
|
|
request.setTimeout(config.timeout, () => { |
|
|
request.destroy() |
|
|
reject(new Error(`Download timeout after ${config.timeout / 1000} seconds`)) |
|
|
}) |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
function useFallback() { |
|
|
log.warn('Attempting to use fallback pricing data...') |
|
|
|
|
|
if (!fs.existsSync(config.fallbackFile)) { |
|
|
log.error(`Fallback file not found: ${config.fallbackFile}`) |
|
|
return false |
|
|
} |
|
|
|
|
|
try { |
|
|
const fallbackData = fs.readFileSync(config.fallbackFile, 'utf8') |
|
|
const jsonData = JSON.parse(fallbackData) |
|
|
|
|
|
|
|
|
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2)) |
|
|
|
|
|
const modelCount = Object.keys(jsonData).length |
|
|
log.warn(`Using fallback pricing data for ${modelCount} models`) |
|
|
log.info('Note: Fallback data may be outdated. Try updating again later.') |
|
|
|
|
|
return true |
|
|
} catch (error) { |
|
|
log.error(`Failed to use fallback: ${error.message}`) |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showCurrentStatus() { |
|
|
if (fs.existsSync(config.pricingFile)) { |
|
|
const stats = fs.statSync(config.pricingFile) |
|
|
const fileAge = Date.now() - stats.mtime.getTime() |
|
|
const ageInHours = Math.round(fileAge / (60 * 60 * 1000)) |
|
|
const ageInDays = Math.floor(ageInHours / 24) |
|
|
|
|
|
let ageString = '' |
|
|
if (ageInDays > 0) { |
|
|
ageString = `${ageInDays} day${ageInDays > 1 ? 's' : ''} and ${ageInHours % 24} hour${ageInHours % 24 !== 1 ? 's' : ''}` |
|
|
} else { |
|
|
ageString = `${ageInHours} hour${ageInHours !== 1 ? 's' : ''}` |
|
|
} |
|
|
|
|
|
log.info(`Current pricing file age: ${ageString}`) |
|
|
|
|
|
try { |
|
|
const data = JSON.parse(fs.readFileSync(config.pricingFile, 'utf8')) |
|
|
log.info(`Current file contains ${Object.keys(data).length} models`) |
|
|
} catch (error) { |
|
|
log.warn('Current file exists but could not be parsed') |
|
|
} |
|
|
} else { |
|
|
log.info('No existing pricing file found') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function main() { |
|
|
console.log(`${colors.bright}${colors.blue}======================================${colors.reset}`) |
|
|
console.log(`${colors.bright} Model Pricing Update Tool${colors.reset}`) |
|
|
console.log( |
|
|
`${colors.bright}${colors.blue}======================================${colors.reset}\n` |
|
|
) |
|
|
|
|
|
|
|
|
showCurrentStatus() |
|
|
console.log('') |
|
|
|
|
|
|
|
|
ensureDataDir() |
|
|
|
|
|
|
|
|
const hasBackup = backupExistingFile() |
|
|
|
|
|
try { |
|
|
|
|
|
await downloadPricingData() |
|
|
|
|
|
|
|
|
if (hasBackup && fs.existsSync(config.backupFile)) { |
|
|
fs.unlinkSync(config.backupFile) |
|
|
log.info('Cleaned up backup file') |
|
|
} |
|
|
|
|
|
console.log(`\n${colors.green}✅ Model pricing updated successfully!${colors.reset}`) |
|
|
process.exit(0) |
|
|
} catch (error) { |
|
|
log.error(`Download failed: ${error.message}`) |
|
|
|
|
|
|
|
|
if (hasBackup) { |
|
|
if (restoreBackup()) { |
|
|
log.info('Original file restored') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (useFallback()) { |
|
|
console.log( |
|
|
`\n${colors.yellow}⚠️ Using fallback data (update completed with warnings)${colors.reset}` |
|
|
) |
|
|
process.exit(0) |
|
|
} else { |
|
|
console.log(`\n${colors.red}❌ Failed to update model pricing${colors.reset}`) |
|
|
process.exit(1) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
process.on('unhandledRejection', (error) => { |
|
|
log.error(`Unhandled error: ${error.message}`) |
|
|
process.exit(1) |
|
|
}) |
|
|
|
|
|
|
|
|
main().catch((error) => { |
|
|
log.error(`Fatal error: ${error.message}`) |
|
|
process.exit(1) |
|
|
}) |
|
|
|