mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +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
|
# 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
|
## Source
|
||||||
The original file is maintained by the LiteLLM project:
|
The original file is maintained by the LiteLLM project and mirrored into the `price-mirror` branch of this repository via GitHub Actions:
|
||||||
- Repository: https://github.com/BerriAI/litellm
|
- Mirror branch (configurable via `PRICE_MIRROR_REPO`): https://raw.githubusercontent.com/<your-repo>/price-mirror/model_prices_and_context_window.json
|
||||||
- File: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
- Upstream source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
This local copy serves as a fallback when the remote file cannot be downloaded due to:
|
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
|
3. Log a warning when using the fallback file
|
||||||
|
|
||||||
## Manual Update
|
## 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
|
```bash
|
||||||
curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json -o model_prices_and_context_window.json
|
curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json -o model_prices_and_context_window.json
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动更新模型价格数据脚本
|
* 手动更新模型价格数据脚本
|
||||||
* 从 LiteLLM 仓库下载最新的模型价格和上下文窗口信息
|
* 从价格镜像分支下载最新的模型价格和上下文窗口信息
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const pricingSource = require('../config/pricingSource')
|
||||||
|
|
||||||
// 颜色输出
|
// 颜色输出
|
||||||
const colors = {
|
const colors = {
|
||||||
@@ -32,8 +34,8 @@ const log = {
|
|||||||
const config = {
|
const config = {
|
||||||
dataDir: path.join(process.cwd(), 'data'),
|
dataDir: path.join(process.cwd(), 'data'),
|
||||||
pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'),
|
pricingFile: path.join(process.cwd(), 'data', 'model_pricing.json'),
|
||||||
pricingUrl:
|
hashFile: path.join(process.cwd(), 'data', 'model_pricing.sha256'),
|
||||||
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json',
|
pricingUrl: pricingSource.pricingUrl,
|
||||||
fallbackFile: path.join(
|
fallbackFile: path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'resources',
|
'resources',
|
||||||
@@ -85,8 +87,8 @@ function restoreBackup() {
|
|||||||
// 下载价格数据
|
// 下载价格数据
|
||||||
function downloadPricingData() {
|
function downloadPricingData() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
log.info('Downloading model pricing data from LiteLLM...')
|
log.info('正在从价格镜像分支拉取最新的模型价格数据...')
|
||||||
log.info(`URL: ${config.pricingUrl}`)
|
log.info(`拉取地址: ${config.pricingUrl}`)
|
||||||
|
|
||||||
const request = https.get(config.pricingUrl, (response) => {
|
const request = https.get(config.pricingUrl, (response) => {
|
||||||
if (response.statusCode !== 200) {
|
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 modelCount = Object.keys(jsonData).length
|
||||||
const fileSize = Math.round(fs.statSync(config.pricingFile).size / 1024)
|
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 sessionHelper = require('../utils/sessionHelper')
|
||||||
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
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 = {}
|
let modelPricingData = {}
|
||||||
try {
|
try {
|
||||||
const pricingPath = path.join(__dirname, '../../data/model_pricing.json')
|
if (!fs.existsSync(dataDir)) {
|
||||||
const pricingContent = fs.readFileSync(pricingPath, 'utf8')
|
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)
|
modelPricingData = JSON.parse(pricingContent)
|
||||||
logger.info('✅ Model pricing data loaded successfully')
|
logger.info('✅ Model pricing data loaded successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const https = require('https')
|
const https = require('https')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const pricingSource = require('../../config/pricingSource')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
class PricingService {
|
class PricingService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dataDir = path.join(process.cwd(), 'data')
|
this.dataDir = path.join(process.cwd(), 'data')
|
||||||
this.pricingFile = path.join(this.dataDir, 'model_pricing.json')
|
this.pricingFile = path.join(this.dataDir, 'model_pricing.json')
|
||||||
this.pricingUrl =
|
this.pricingUrl = pricingSource.pricingUrl
|
||||||
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
|
this.hashUrl = pricingSource.hashUrl
|
||||||
this.fallbackFile = path.join(
|
this.fallbackFile = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'resources',
|
'resources',
|
||||||
'model-pricing',
|
'model-pricing',
|
||||||
'model_prices_and_context_window.json'
|
'model_prices_and_context_window.json'
|
||||||
)
|
)
|
||||||
|
this.localHashFile = path.join(this.dataDir, 'model_pricing.sha256')
|
||||||
this.pricingData = null
|
this.pricingData = null
|
||||||
this.lastUpdated = null
|
this.lastUpdated = null
|
||||||
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
|
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
|
||||||
|
this.hashCheckInterval = 10 * 60 * 1000 // 10分钟哈希校验
|
||||||
this.fileWatcher = null // 文件监听器
|
this.fileWatcher = null // 文件监听器
|
||||||
this.reloadDebounceTimer = null // 防抖定时器
|
this.reloadDebounceTimer = null // 防抖定时器
|
||||||
|
this.hashCheckTimer = null // 哈希轮询定时器
|
||||||
|
this.hashSyncInProgress = false // 哈希同步状态
|
||||||
|
|
||||||
// 硬编码的 1 小时缓存价格(美元/百万 token)
|
// 硬编码的 1 小时缓存价格(美元/百万 token)
|
||||||
// ephemeral_5m 的价格使用 model_pricing.json 中的 cache_creation_input_token_cost
|
// ephemeral_5m 的价格使用 model_pricing.json 中的 cache_creation_input_token_cost
|
||||||
@@ -81,11 +87,17 @@ class PricingService {
|
|||||||
// 检查是否需要下载或更新价格数据
|
// 检查是否需要下载或更新价格数据
|
||||||
await this.checkAndUpdatePricing()
|
await this.checkAndUpdatePricing()
|
||||||
|
|
||||||
|
// 初次启动时执行一次哈希校验,确保与远端保持一致
|
||||||
|
await this.syncWithRemoteHash()
|
||||||
|
|
||||||
// 设置定时更新
|
// 设置定时更新
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.checkAndUpdatePricing()
|
this.checkAndUpdatePricing()
|
||||||
}, this.updateInterval)
|
}, this.updateInterval)
|
||||||
|
|
||||||
|
// 设置哈希轮询
|
||||||
|
this.setupHashCheck()
|
||||||
|
|
||||||
// 设置文件监听器
|
// 设置文件监听器
|
||||||
this.setupFileWatcher()
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = https.get(this.pricingUrl, (response) => {
|
const request = https.get(this.hashUrl, (response) => {
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`))
|
reject(new Error(`哈希文件获取失败:HTTP ${response.statusCode}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +218,77 @@ class PricingService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
try {
|
const hash = data.trim().split(/\s+/)[0]
|
||||||
const jsonData = JSON.parse(data)
|
|
||||||
|
|
||||||
// 保存到文件
|
if (!hash) {
|
||||||
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
|
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
|
this.pricingData = jsonData
|
||||||
@@ -226,8 +350,11 @@ class PricingService {
|
|||||||
const fallbackData = fs.readFileSync(this.fallbackFile, 'utf8')
|
const fallbackData = fs.readFileSync(this.fallbackFile, 'utf8')
|
||||||
const jsonData = JSON.parse(fallbackData)
|
const jsonData = JSON.parse(fallbackData)
|
||||||
|
|
||||||
|
const formattedJson = JSON.stringify(jsonData, null, 2)
|
||||||
|
|
||||||
// 保存到data目录
|
// 保存到data目录
|
||||||
fs.writeFileSync(this.pricingFile, JSON.stringify(jsonData, null, 2))
|
fs.writeFileSync(this.pricingFile, formattedJson)
|
||||||
|
this.persistLocalHash(formattedJson)
|
||||||
|
|
||||||
// 更新内存中的数据
|
// 更新内存中的数据
|
||||||
this.pricingData = jsonData
|
this.pricingData = jsonData
|
||||||
|
|||||||
Reference in New Issue
Block a user