| #!/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) |
| }) |
|
|