mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 08:32:17 +00:00
fix: 优化模型价格文件更新策略
This commit is contained in:
62
.github/workflows/sync-model-pricing.yml
vendored
Normal file
62
.github/workflows/sync-model-pricing.yml
vendored
Normal 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
17
config/pricingSource.js
Normal 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}`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user