fix: 优化模型价格文件更新策略

This commit is contained in:
shaw
2025-10-16 10:46:45 +08:00
parent d275b0d4b6
commit d606cb2e38
6 changed files with 252 additions and 25 deletions

View File

@@ -0,0 +1,62 @@
name: 同步模型价格数据
on:
schedule:
- cron: '*/10 * * * *'
workflow_dispatch: {}
jobs:
sync-pricing:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: 检出 price-mirror 分支
uses: actions/checkout@v4
with:
ref: price-mirror
fetch-depth: 0
- name: 下载上游价格文件
id: fetch
run: |
set -euo pipefail
curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json \
-o model_prices_and_context_window.json.new
NEW_HASH=$(sha256sum model_prices_and_context_window.json.new | awk '{print $1}')
if [ -f model_prices_and_context_window.sha256 ]; then
OLD_HASH=$(cat model_prices_and_context_window.sha256 | tr -d ' \n\r')
else
OLD_HASH=""
fi
if [ "$NEW_HASH" = "$OLD_HASH" ]; then
echo "价格文件无变化,跳过提交"
echo "changed=false" >> "$GITHUB_OUTPUT"
rm -f model_prices_and_context_window.json.new
exit 0
fi
mv model_prices_and_context_window.json.new model_prices_and_context_window.json
echo "$NEW_HASH" > model_prices_and_context_window.sha256
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "hash=$NEW_HASH" >> "$GITHUB_OUTPUT"
- name: 提交并推送变更
if: steps.fetch.outputs.changed == 'true'
env:
NEW_HASH: ${{ steps.fetch.outputs.hash }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add model_prices_and_context_window.json model_prices_and_context_window.sha256
COMMIT_MSG="chore: 同步模型价格数据"
if [ -n "${NEW_HASH}" ]; then
COMMIT_MSG="$COMMIT_MSG (${NEW_HASH})"
fi
git commit -m "$COMMIT_MSG"
git push origin price-mirror

17
config/pricingSource.js Normal file
View File

@@ -0,0 +1,17 @@
const repository =
process.env.PRICE_MIRROR_REPO || process.env.GITHUB_REPOSITORY || 'Wei-Shaw/claude-relay-service'
const branch = process.env.PRICE_MIRROR_BRANCH || 'price-mirror'
const pricingFileName = process.env.PRICE_MIRROR_FILENAME || 'model_prices_and_context_window.json'
const hashFileName = process.env.PRICE_MIRROR_HASH_FILENAME || 'model_prices_and_context_window.sha256'
const baseUrl = process.env.PRICE_MIRROR_BASE_URL
? process.env.PRICE_MIRROR_BASE_URL.replace(/\/$/, '')
: `https://raw.githubusercontent.com/${repository}/${branch}`
module.exports = {
pricingFileName,
hashFileName,
pricingUrl:
process.env.PRICE_MIRROR_JSON_URL || `${baseUrl}/${pricingFileName}`,
hashUrl: process.env.PRICE_MIRROR_HASH_URL || `${baseUrl}/${hashFileName}`
}

View File

@@ -1,11 +1,11 @@
# Model Pricing Data
This directory contains a local copy of the LiteLLM model pricing data as a fallback mechanism.
This directory contains a local copy of the mirrored model pricing data as a fallback mechanism.
## Source
The original file is maintained by the LiteLLM project:
- Repository: https://github.com/BerriAI/litellm
- File: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
The original file is maintained by the LiteLLM project and mirrored into the `price-mirror` branch of this repository via GitHub Actions:
- Mirror branch (configurable via `PRICE_MIRROR_REPO`): https://raw.githubusercontent.com/<your-repo>/price-mirror/model_prices_and_context_window.json
- Upstream source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
## Purpose
This local copy serves as a fallback when the remote file cannot be downloaded due to:
@@ -22,7 +22,7 @@ The pricingService will:
3. Log a warning when using the fallback file
## Manual Update
To manually update this file with the latest pricing data:
To manually update this file with the latest pricing data (if automation is unavailable):
```bash
curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json -o model_prices_and_context_window.json
```
@@ -34,4 +34,4 @@ The file contains JSON data with model pricing information including:
- Context window sizes
- Model capabilities
Last updated: 2025-08-10
Last updated: 2025-08-10

View File

@@ -2,12 +2,14 @@
/**
* 手动更新模型价格数据脚本
* 从 LiteLLM 仓库下载最新的模型价格和上下文窗口信息
* 从价格镜像分支下载最新的模型价格和上下文窗口信息
*/
const fs = require('fs')
const path = require('path')
const https = require('https')
const crypto = require('crypto')
const pricingSource = require('../config/pricingSource')
// 颜色输出
const colors = {
@@ -32,8 +34,8 @@ const log = {
const config = {
dataDir: path.join(process.cwd(), 'data'),
pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'),
pricingUrl:
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json',
hashFile: path.join(process.cwd(), 'data', 'model_pricing.sha256'),
pricingUrl: pricingSource.pricingUrl,
fallbackFile: path.join(
process.cwd(),
'resources',
@@ -85,8 +87,8 @@ function restoreBackup() {
// 下载价格数据
function downloadPricingData() {
return new Promise((resolve, reject) => {
log.info('Downloading model pricing data from LiteLLM...')
log.info(`URL: ${config.pricingUrl}`)
log.info('正在从价格镜像分支拉取最新的模型价格数据...')
log.info(`拉取地址: ${config.pricingUrl}`)
const request = https.get(config.pricingUrl, (response) => {
if (response.statusCode !== 200) {
@@ -115,7 +117,11 @@ function downloadPricingData() {
}
// 保存到文件
fs.writeFileSync(config.pricingFile, JSON.stringify(jsonData, null, 2))
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)

View File

@@ -17,11 +17,26 @@ const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
const sessionHelper = require('../utils/sessionHelper')
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
const dataDir = path.join(__dirname, '../../data')
const localPricingPath = path.join(dataDir, 'model_pricing.json')
const fallbackPricingPath = path.join(
__dirname,
'../../resources/model-pricing/model_prices_and_context_window.json'
)
// 加载模型定价数据
let modelPricingData = {}
try {
const pricingPath = path.join(__dirname, '../../data/model_pricing.json')
const pricingContent = fs.readFileSync(pricingPath, 'utf8')
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}
if (!fs.existsSync(localPricingPath) && fs.existsSync(fallbackPricingPath)) {
fs.copyFileSync(fallbackPricingPath, localPricingPath)
logger.warn('⚠️ 未找到 data/model_pricing.json已使用备用价格文件初始化')
}
const pricingContent = fs.readFileSync(localPricingPath, 'utf8')
modelPricingData = JSON.parse(pricingContent)
logger.info('✅ Model pricing data loaded successfully')
} catch (error) {

View File

@@ -1,25 +1,31 @@
const fs = require('fs')
const path = require('path')
const https = require('https')
const crypto = require('crypto')
const pricingSource = require('../../config/pricingSource')
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.pricingUrl = pricingSource.pricingUrl
this.hashUrl = pricingSource.hashUrl
this.fallbackFile = path.join(
process.cwd(),
'resources',
'model-pricing',
'model_prices_and_context_window.json'
)
this.localHashFile = path.join(this.dataDir, 'model_pricing.sha256')
this.pricingData = null
this.lastUpdated = null
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
this.hashCheckInterval = 10 * 60 * 1000 // 10分钟哈希校验
this.fileWatcher = null // 文件监听器
this.reloadDebounceTimer = null // 防抖定时器
this.hashCheckTimer = null // 哈希轮询定时器
this.hashSyncInProgress = false // 哈希同步状态
// 硬编码的 1 小时缓存价格(美元/百万 token
// ephemeral_5m 的价格使用 model_pricing.json 中的 cache_creation_input_token_cost
@@ -81,11 +87,17 @@ class PricingService {
// 检查是否需要下载或更新价格数据
await this.checkAndUpdatePricing()
// 初次启动时执行一次哈希校验,确保与远端保持一致
await this.syncWithRemoteHash()
// 设置定时更新
setInterval(() => {
this.checkAndUpdatePricing()
}, this.updateInterval)
// 设置哈希轮询
this.setupHashCheck()
// 设置文件监听器
this.setupFileWatcher()
@@ -145,12 +157,58 @@ class PricingService {
}
}
// 实际的下载逻辑
_downloadFromRemote() {
// 哈希轮询设置
setupHashCheck() {
if (this.hashCheckTimer) {
clearInterval(this.hashCheckTimer)
}
this.hashCheckTimer = setInterval(() => {
this.syncWithRemoteHash()
}, this.hashCheckInterval)
logger.info('🕒 已启用价格文件哈希轮询每10分钟校验一次')
}
// 与远端哈希对比
async syncWithRemoteHash() {
if (this.hashSyncInProgress) {
return
}
this.hashSyncInProgress = true
try {
const remoteHash = await this.fetchRemoteHash()
if (!remoteHash) {
return
}
const localHash = this.computeLocalHash()
if (!localHash) {
logger.info('📄 本地价格文件缺失,尝试下载最新版本')
await this.downloadPricingData()
return
}
if (remoteHash !== localHash) {
logger.info('🔁 检测到远端价格文件更新,开始下载最新数据')
await this.downloadPricingData()
}
} catch (error) {
logger.warn(`⚠️ 哈希校验失败:${error.message}`)
} finally {
this.hashSyncInProgress = false
}
}
// 获取远端哈希值
fetchRemoteHash() {
return new Promise((resolve, reject) => {
const request = https.get(this.pricingUrl, (response) => {
const request = https.get(this.hashUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
reject(new Error(`哈希文件获取失败:HTTP ${response.statusCode}`))
return
}
@@ -160,11 +218,77 @@ class PricingService {
})
response.on('end', () => {
try {
const jsonData = JSON.parse(data)
const hash = data.trim().split(/\s+/)[0]
// 保存到文件
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
if (!hash) {
reject(new Error('哈希文件内容为空'))
return
}
resolve(hash)
})
})
request.on('error', (error) => {
reject(new Error(`网络错误:${error.message}`))
})
request.setTimeout(30000, () => {
request.destroy()
reject(new Error('获取哈希超时30秒'))
})
})
}
// 计算本地文件哈希
computeLocalHash() {
if (!fs.existsSync(this.pricingFile)) {
return null
}
if (fs.existsSync(this.localHashFile)) {
const cached = fs.readFileSync(this.localHashFile, 'utf8').trim()
if (cached) {
return cached
}
}
const fileBuffer = fs.readFileSync(this.pricingFile)
return this.persistLocalHash(fileBuffer)
}
// 写入本地哈希文件
persistLocalHash(content) {
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8')
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
fs.writeFileSync(this.localHashFile, `${hash}\n`)
return hash
}
// 实际的下载逻辑
_downloadFromRemote() {
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
}
const chunks = []
response.on('data', (chunk) => {
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
chunks.push(bufferChunk)
})
response.on('end', () => {
try {
const buffer = Buffer.concat(chunks)
const rawContent = buffer.toString('utf8')
const jsonData = JSON.parse(rawContent)
// 保存到文件并更新哈希
fs.writeFileSync(this.pricingFile, rawContent)
this.persistLocalHash(buffer)
// 更新内存中的数据
this.pricingData = jsonData
@@ -226,8 +350,11 @@ class PricingService {
const fallbackData = fs.readFileSync(this.fallbackFile, 'utf8')
const jsonData = JSON.parse(fallbackData)
const formattedJson = JSON.stringify(jsonData, null, 2)
// 保存到data目录
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
fs.writeFileSync(this.pricingFile, formattedJson)
this.persistLocalHash(formattedJson)
// 更新内存中的数据
this.pricingData = jsonData