mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-24 21:42:33 +00:00
234 lines
7.1 KiB
JavaScript
234 lines
7.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const logger = require('../utils/logger');
|
|
|
|
class PricingService {
|
|
constructor() {
|
|
this.dataDir = path.join(process.cwd(), 'data');
|
|
this.pricingFile = path.join(this.dataDir, 'model_pricing.json');
|
|
this.pricingUrl = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
this.pricingData = null;
|
|
this.lastUpdated = null;
|
|
this.updateInterval = 24 * 60 * 60 * 1000; // 24小时
|
|
}
|
|
|
|
// 初始化价格服务
|
|
async initialize() {
|
|
try {
|
|
// 确保data目录存在
|
|
if (!fs.existsSync(this.dataDir)) {
|
|
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
logger.info('📁 Created data directory');
|
|
}
|
|
|
|
// 检查是否需要下载或更新价格数据
|
|
await this.checkAndUpdatePricing();
|
|
|
|
// 设置定时更新
|
|
setInterval(() => {
|
|
this.checkAndUpdatePricing();
|
|
}, this.updateInterval);
|
|
|
|
logger.success('💰 Pricing service initialized successfully');
|
|
} catch (error) {
|
|
logger.error('❌ Failed to initialize pricing service:', error);
|
|
}
|
|
}
|
|
|
|
// 检查并更新价格数据
|
|
async checkAndUpdatePricing() {
|
|
try {
|
|
const needsUpdate = this.needsUpdate();
|
|
|
|
if (needsUpdate) {
|
|
logger.info('🔄 Updating model pricing data...');
|
|
await this.downloadPricingData();
|
|
} else {
|
|
// 如果不需要更新,加载现有数据
|
|
await this.loadPricingData();
|
|
}
|
|
} catch (error) {
|
|
logger.error('❌ Failed to check/update pricing:', error);
|
|
// 如果更新失败,尝试加载现有数据
|
|
await this.loadPricingData();
|
|
}
|
|
}
|
|
|
|
// 检查是否需要更新
|
|
needsUpdate() {
|
|
if (!fs.existsSync(this.pricingFile)) {
|
|
logger.info('📋 Pricing file not found, will download');
|
|
return true;
|
|
}
|
|
|
|
const stats = fs.statSync(this.pricingFile);
|
|
const fileAge = Date.now() - stats.mtime.getTime();
|
|
|
|
if (fileAge > this.updateInterval) {
|
|
logger.info(`📋 Pricing file is ${Math.round(fileAge / (60 * 60 * 1000))} hours old, will update`);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// 下载价格数据
|
|
downloadPricingData() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = https.get(this.pricingUrl, (response) => {
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
return;
|
|
}
|
|
|
|
let data = '';
|
|
response.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
|
|
response.on('end', () => {
|
|
try {
|
|
const jsonData = JSON.parse(data);
|
|
|
|
// 保存到文件
|
|
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2));
|
|
|
|
// 更新内存中的数据
|
|
this.pricingData = jsonData;
|
|
this.lastUpdated = new Date();
|
|
|
|
logger.success(`💰 Downloaded pricing data for ${Object.keys(jsonData).length} models`);
|
|
resolve();
|
|
} catch (error) {
|
|
reject(new Error(`Failed to parse pricing data: ${error.message}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
request.on('error', (error) => {
|
|
reject(new Error(`Failed to download pricing data: ${error.message}`));
|
|
});
|
|
|
|
request.setTimeout(30000, () => {
|
|
request.destroy();
|
|
reject(new Error('Download timeout'));
|
|
});
|
|
});
|
|
}
|
|
|
|
// 加载本地价格数据
|
|
async loadPricingData() {
|
|
try {
|
|
if (fs.existsSync(this.pricingFile)) {
|
|
const data = fs.readFileSync(this.pricingFile, 'utf8');
|
|
this.pricingData = JSON.parse(data);
|
|
|
|
const stats = fs.statSync(this.pricingFile);
|
|
this.lastUpdated = stats.mtime;
|
|
|
|
logger.info(`💰 Loaded pricing data for ${Object.keys(this.pricingData).length} models from cache`);
|
|
} else {
|
|
logger.warn('💰 No pricing data file found');
|
|
this.pricingData = {};
|
|
}
|
|
} catch (error) {
|
|
logger.error('❌ Failed to load pricing data:', error);
|
|
this.pricingData = {};
|
|
}
|
|
}
|
|
|
|
// 获取模型价格信息
|
|
getModelPricing(modelName) {
|
|
if (!this.pricingData || !modelName) {
|
|
return null;
|
|
}
|
|
|
|
// 尝试直接匹配
|
|
if (this.pricingData[modelName]) {
|
|
return this.pricingData[modelName];
|
|
}
|
|
|
|
// 尝试模糊匹配(处理版本号等变化)
|
|
const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '');
|
|
|
|
for (const [key, value] of Object.entries(this.pricingData)) {
|
|
const normalizedKey = key.toLowerCase().replace(/[_-]/g, '');
|
|
if (normalizedKey.includes(normalizedModel) || normalizedModel.includes(normalizedKey)) {
|
|
logger.debug(`💰 Found pricing for ${modelName} using fuzzy match: ${key}`);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
logger.debug(`💰 No pricing found for model: ${modelName}`);
|
|
return null;
|
|
}
|
|
|
|
// 计算使用费用
|
|
calculateCost(usage, modelName) {
|
|
const pricing = this.getModelPricing(modelName);
|
|
|
|
if (!pricing) {
|
|
return {
|
|
inputCost: 0,
|
|
outputCost: 0,
|
|
cacheCreateCost: 0,
|
|
cacheReadCost: 0,
|
|
totalCost: 0,
|
|
hasPricing: false
|
|
};
|
|
}
|
|
|
|
const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0);
|
|
const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0);
|
|
const cacheCreateCost = (usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0);
|
|
const cacheReadCost = (usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0);
|
|
|
|
return {
|
|
inputCost,
|
|
outputCost,
|
|
cacheCreateCost,
|
|
cacheReadCost,
|
|
totalCost: inputCost + outputCost + cacheCreateCost + cacheReadCost,
|
|
hasPricing: true,
|
|
pricing: {
|
|
input: pricing.input_cost_per_token || 0,
|
|
output: pricing.output_cost_per_token || 0,
|
|
cacheCreate: pricing.cache_creation_input_token_cost || 0,
|
|
cacheRead: pricing.cache_read_input_token_cost || 0
|
|
}
|
|
};
|
|
}
|
|
|
|
// 格式化价格显示
|
|
formatCost(cost) {
|
|
if (cost === 0) return '$0.000000';
|
|
if (cost < 0.000001) return `$${cost.toExponential(2)}`;
|
|
if (cost < 0.01) return `$${cost.toFixed(6)}`;
|
|
if (cost < 1) return `$${cost.toFixed(4)}`;
|
|
return `$${cost.toFixed(2)}`;
|
|
}
|
|
|
|
// 获取服务状态
|
|
getStatus() {
|
|
return {
|
|
initialized: this.pricingData !== null,
|
|
lastUpdated: this.lastUpdated,
|
|
modelCount: this.pricingData ? Object.keys(this.pricingData).length : 0,
|
|
nextUpdate: this.lastUpdated ? new Date(this.lastUpdated.getTime() + this.updateInterval) : null
|
|
};
|
|
}
|
|
|
|
// 强制更新价格数据
|
|
async forceUpdate() {
|
|
try {
|
|
await this.downloadPricingData();
|
|
return { success: true, message: 'Pricing data updated successfully' };
|
|
} catch (error) {
|
|
logger.error('❌ Force update failed:', error);
|
|
return { success: false, message: error.message };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new PricingService(); |