mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge pull request #914 from sczheng189/main
mod: 修改opus周限额为Claude模型的周限额
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -20,7 +20,6 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"google-auth-library": "^10.1.0",
|
"google-auth-library": "^10.1.0",
|
||||||
"heapdump": "^0.3.15",
|
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"inquirer": "^8.2.6",
|
"inquirer": "^8.2.6",
|
||||||
@@ -5399,19 +5398,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/heapdump": {
|
|
||||||
"version": "0.3.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz",
|
|
||||||
"integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"nan": "^2.13.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/helmet": {
|
"node_modules/helmet": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz",
|
||||||
@@ -7027,12 +7013,6 @@
|
|||||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/nan": {
|
|
||||||
"version": "2.24.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz",
|
|
||||||
"integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ class Application {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💰 启动回填:本周 Claude 周费用(用于 API Key 维度周限额)
|
||||||
|
try {
|
||||||
|
logger.info('💰 Backfilling current-week Claude weekly cost...')
|
||||||
|
const weeklyClaudeCostInitService = require('./services/weeklyClaudeCostInitService')
|
||||||
|
await weeklyClaudeCostInitService.backfillCurrentWeekClaudeCosts()
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('⚠️ Weekly Claude cost backfill failed (startup continues):', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
// 🕐 初始化Claude账户会话窗口
|
// 🕐 初始化Claude账户会话窗口
|
||||||
logger.info('🕐 Initializing Claude account session windows...')
|
logger.info('🕐 Initializing Claude account session windows...')
|
||||||
const claudeAccountService = require('./services/claudeAccountService')
|
const claudeAccountService = require('./services/claudeAccountService')
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const ClientValidator = require('../validators/clientValidator')
|
|||||||
const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator')
|
const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator')
|
||||||
const claudeRelayConfigService = require('../services/claudeRelayConfigService')
|
const claudeRelayConfigService = require('../services/claudeRelayConfigService')
|
||||||
const { calculateWaitTimeStats } = require('../utils/statsHelper')
|
const { calculateWaitTimeStats } = require('../utils/statsHelper')
|
||||||
|
const { isClaudeFamilyModel } = require('../utils/modelHelper')
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
@@ -1239,20 +1240,20 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 Opus 周费用限制(仅对 Opus 模型生效)
|
// 检查 Claude 周费用限制
|
||||||
const weeklyOpusCostLimit = validation.keyData.weeklyOpusCostLimit || 0
|
const weeklyOpusCostLimit = validation.keyData.weeklyOpusCostLimit || 0
|
||||||
if (weeklyOpusCostLimit > 0) {
|
if (weeklyOpusCostLimit > 0) {
|
||||||
// 从请求中获取模型信息
|
// 从请求中获取模型信息
|
||||||
const requestBody = req.body || {}
|
const requestBody = req.body || {}
|
||||||
const model = requestBody.model || ''
|
const model = requestBody.model || ''
|
||||||
|
|
||||||
// 判断是否为 Opus 模型
|
// 判断是否为 Claude 模型
|
||||||
if (model && model.toLowerCase().includes('claude-opus')) {
|
if (isClaudeFamilyModel(model)) {
|
||||||
const weeklyOpusCost = validation.keyData.weeklyOpusCost || 0
|
const weeklyOpusCost = validation.keyData.weeklyOpusCost || 0
|
||||||
|
|
||||||
if (weeklyOpusCost >= weeklyOpusCostLimit) {
|
if (weeklyOpusCost >= weeklyOpusCostLimit) {
|
||||||
logger.security(
|
logger.security(
|
||||||
`💰 Weekly Opus cost limit exceeded for key: ${validation.keyData.id} (${
|
`💰 Weekly Claude cost limit exceeded for key: ${validation.keyData.id} (${
|
||||||
validation.keyData.name
|
validation.keyData.name
|
||||||
}), cost: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
}), cost: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
||||||
)
|
)
|
||||||
@@ -1266,17 +1267,17 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
resetDate.setHours(0, 0, 0, 0)
|
resetDate.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
return res.status(429).json({
|
return res.status(429).json({
|
||||||
error: 'Weekly Opus cost limit exceeded',
|
error: 'Weekly Claude cost limit exceeded',
|
||||||
message: `已达到 Opus 模型周费用限制 ($${weeklyOpusCostLimit})`,
|
message: `已达到 Claude 模型周费用限制 ($${weeklyOpusCostLimit})`,
|
||||||
currentCost: weeklyOpusCost,
|
currentCost: weeklyOpusCost,
|
||||||
costLimit: weeklyOpusCostLimit,
|
costLimit: weeklyOpusCostLimit,
|
||||||
resetAt: resetDate.toISOString() // 下周一重置
|
resetAt: resetDate.toISOString() // 下周一重置
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录当前 Opus 费用使用情况
|
// 记录当前 Claude 费用使用情况
|
||||||
logger.api(
|
logger.api(
|
||||||
`💰 Opus weekly cost usage for key: ${validation.keyData.id} (${
|
`💰 Claude weekly cost usage for key: ${validation.keyData.id} (${
|
||||||
validation.keyData.name
|
validation.keyData.name
|
||||||
}), current: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
}), current: $${weeklyOpusCost.toFixed(2)}/$${weeklyOpusCostLimit}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1081,8 +1081,13 @@ class RedisClient {
|
|||||||
// 💰 获取本周 Opus 费用
|
// 💰 获取本周 Opus 费用
|
||||||
async getWeeklyOpusCost(keyId) {
|
async getWeeklyOpusCost(keyId) {
|
||||||
const currentWeek = getWeekStringInTimezone()
|
const currentWeek = getWeekStringInTimezone()
|
||||||
const costKey = `usage:opus:weekly:${keyId}:${currentWeek}`
|
const costKey = `usage:claude:weekly:${keyId}:${currentWeek}`
|
||||||
const cost = await this.client.get(costKey)
|
let cost = await this.client.get(costKey)
|
||||||
|
// 向后兼容:如果新 key 不存在,则回退读取旧的(仅 Opus 口径)周费用 key。
|
||||||
|
if (cost === null || cost === undefined) {
|
||||||
|
const legacyKey = `usage:opus:weekly:${keyId}:${currentWeek}`
|
||||||
|
cost = await this.client.get(legacyKey)
|
||||||
|
}
|
||||||
const result = parseFloat(cost || 0)
|
const result = parseFloat(cost || 0)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`💰 Getting weekly Opus cost for ${keyId}, week: ${currentWeek}, key: ${costKey}, value: ${cost}, result: ${result}`
|
`💰 Getting weekly Opus cost for ${keyId}, week: ${currentWeek}, key: ${costKey}, value: ${cost}, result: ${result}`
|
||||||
@@ -1090,11 +1095,12 @@ class RedisClient {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 💰 增加本周 Opus 费用
|
// 💰 增加本周 Claude 费用
|
||||||
async incrementWeeklyOpusCost(keyId, amount) {
|
async incrementWeeklyOpusCost(keyId, amount) {
|
||||||
const currentWeek = getWeekStringInTimezone()
|
const currentWeek = getWeekStringInTimezone()
|
||||||
const weeklyKey = `usage:opus:weekly:${keyId}:${currentWeek}`
|
// 注意:尽管函数名沿用旧的 Opus 命名,但当前实现统计的是 Claude 系列模型的“周费用”。
|
||||||
const totalKey = `usage:opus:total:${keyId}`
|
const weeklyKey = `usage:claude:weekly:${keyId}:${currentWeek}`
|
||||||
|
const totalKey = `usage:claude:total:${keyId}`
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`💰 Incrementing weekly Opus cost for ${keyId}, week: ${currentWeek}, amount: $${amount}`
|
`💰 Incrementing weekly Opus cost for ${keyId}, week: ${currentWeek}, amount: $${amount}`
|
||||||
@@ -1111,6 +1117,16 @@ class RedisClient {
|
|||||||
logger.debug(`💰 Opus cost incremented successfully, new weekly total: $${results[0][1]}`)
|
logger.debug(`💰 Opus cost incremented successfully, new weekly total: $${results[0][1]}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 💰 覆盖设置本周 Claude 费用(用于启动回填/迁移)
|
||||||
|
async setWeeklyClaudeCost(keyId, amount, weekString = null) {
|
||||||
|
const currentWeek = weekString || getWeekStringInTimezone()
|
||||||
|
const weeklyKey = `usage:claude:weekly:${keyId}:${currentWeek}`
|
||||||
|
|
||||||
|
await this.client.set(weeklyKey, String(amount || 0))
|
||||||
|
// 保留 2 周,足够覆盖“当前周 + 上周”查看/回填
|
||||||
|
await this.client.expire(weeklyKey, 14 * 24 * 3600)
|
||||||
|
}
|
||||||
|
|
||||||
// 💰 计算账户的每日费用(基于模型使用)
|
// 💰 计算账户的每日费用(基于模型使用)
|
||||||
async getAccountDailyCost(accountId) {
|
async getAccountDailyCost(accountId) {
|
||||||
const CostCalculator = require('../utils/costCalculator')
|
const CostCalculator = require('../utils/costCalculator')
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { v4: uuidv4 } = require('uuid')
|
|||||||
const config = require('../../config/config')
|
const config = require('../../config/config')
|
||||||
const redis = require('../models/redis')
|
const redis = require('../models/redis')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
|
const { isClaudeFamilyModel } = require('../utils/modelHelper')
|
||||||
|
|
||||||
const ACCOUNT_TYPE_CONFIG = {
|
const ACCOUNT_TYPE_CONFIG = {
|
||||||
claude: { prefix: 'claude:account:' },
|
claude: { prefix: 'claude:account:' },
|
||||||
@@ -1029,6 +1030,9 @@ class ApiKeyService {
|
|||||||
logger.database(
|
logger.database(
|
||||||
`💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}`
|
`💰 Recorded cost for ${keyId}: $${costInfo.costs.total.toFixed(6)}, model: ${model}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 记录 Claude 周费用(如果适用)
|
||||||
|
await this.recordClaudeWeeklyCost(keyId, costInfo.costs.total, model, null)
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`)
|
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`)
|
||||||
}
|
}
|
||||||
@@ -1092,35 +1096,31 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 记录 Opus 模型费用(仅限 claude 和 claude-console 账户)
|
// 📊 记录 Claude 模型周费用(API Key 维度)
|
||||||
async recordOpusCost(keyId, cost, model, accountType) {
|
async recordClaudeWeeklyCost(keyId, cost, model, accountType) {
|
||||||
try {
|
try {
|
||||||
// 判断是否为 Opus 模型
|
// 判断是否为 Claude 系列模型(包含 Bedrock 格式等)
|
||||||
if (!model || !model.toLowerCase().includes('claude-opus')) {
|
if (!isClaudeFamilyModel(model)) {
|
||||||
return // 不是 Opus 模型,直接返回
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为 claude、claude-console 或 ccr 账户
|
// 记录 Claude 周费用
|
||||||
if (
|
|
||||||
!accountType ||
|
|
||||||
(accountType !== 'claude' && accountType !== 'claude-console' && accountType !== 'ccr')
|
|
||||||
) {
|
|
||||||
logger.debug(`⚠️ Skipping Opus cost recording for non-Claude account type: ${accountType}`)
|
|
||||||
return // 不是 claude 账户,直接返回
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录 Opus 周费用
|
|
||||||
await redis.incrementWeeklyOpusCost(keyId, cost)
|
await redis.incrementWeeklyOpusCost(keyId, cost)
|
||||||
logger.database(
|
logger.database(
|
||||||
`💰 Recorded Opus weekly cost for ${keyId}: $${cost.toFixed(
|
`💰 Recorded Claude weekly cost for ${keyId}: $${cost.toFixed(
|
||||||
6
|
6
|
||||||
)}, model: ${model}, account type: ${accountType}`
|
)}, model: ${model}${accountType ? `, account type: ${accountType}` : ''}`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ Failed to record Opus cost:', error)
|
logger.error('❌ Failed to record Claude weekly cost:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 向后兼容:旧名字是 Opus-only 口径;现在周费用统计已扩展为 Claude 全模型口径。
|
||||||
|
async recordOpusCost(keyId, cost, model, accountType) {
|
||||||
|
return this.recordClaudeWeeklyCost(keyId, cost, model, accountType)
|
||||||
|
}
|
||||||
|
|
||||||
// 📊 记录使用情况(新版本,支持详细的缓存类型)
|
// 📊 记录使用情况(新版本,支持详细的缓存类型)
|
||||||
async recordUsageWithDetails(
|
async recordUsageWithDetails(
|
||||||
keyId,
|
keyId,
|
||||||
@@ -1220,8 +1220,8 @@ class ApiKeyService {
|
|||||||
`💰 Recorded cost for ${keyId}: $${costInfo.totalCost.toFixed(6)}, model: ${model}`
|
`💰 Recorded cost for ${keyId}: $${costInfo.totalCost.toFixed(6)}, model: ${model}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 记录 Opus 周费用(如果适用)
|
// 记录 Claude 周费用(如果适用)
|
||||||
await this.recordOpusCost(keyId, costInfo.totalCost, model, accountType)
|
await this.recordClaudeWeeklyCost(keyId, costInfo.totalCost, model, accountType)
|
||||||
|
|
||||||
// 记录详细的缓存费用(如果有)
|
// 记录详细的缓存费用(如果有)
|
||||||
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
|
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
|
||||||
|
|||||||
219
src/services/weeklyClaudeCostInitService.js
Normal file
219
src/services/weeklyClaudeCostInitService.js
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
const redis = require('../models/redis')
|
||||||
|
const logger = require('../utils/logger')
|
||||||
|
const pricingService = require('./pricingService')
|
||||||
|
const { isClaudeFamilyModel } = require('../utils/modelHelper')
|
||||||
|
|
||||||
|
function pad2(n) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成配置时区下的 YYYY-MM-DD 字符串。
|
||||||
|
// 注意:入参 date 必须是 redis.getDateInTimezone() 生成的“时区偏移后”的 Date。
|
||||||
|
function formatTzDateYmd(tzDate) {
|
||||||
|
return `${tzDate.getUTCFullYear()}-${pad2(tzDate.getUTCMonth() + 1)}-${pad2(tzDate.getUTCDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
class WeeklyClaudeCostInitService {
|
||||||
|
_getCurrentWeekDatesInTimezone() {
|
||||||
|
const tzNow = redis.getDateInTimezone(new Date())
|
||||||
|
const tzToday = new Date(tzNow)
|
||||||
|
tzToday.setUTCHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
// ISO 周:周一=1 ... 周日=7
|
||||||
|
const isoDay = tzToday.getUTCDay() || 7
|
||||||
|
const tzMonday = new Date(tzToday)
|
||||||
|
tzMonday.setUTCDate(tzToday.getUTCDate() - (isoDay - 1))
|
||||||
|
|
||||||
|
const dates = []
|
||||||
|
for (let d = new Date(tzMonday); d <= tzToday; d.setUTCDate(d.getUTCDate() + 1)) {
|
||||||
|
dates.push(formatTzDateYmd(d))
|
||||||
|
}
|
||||||
|
return dates
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildWeeklyClaudeKey(keyId, weekString) {
|
||||||
|
return `usage:claude:weekly:${keyId}:${weekString}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动回填:把“本周(周一到今天)Claude 全模型”周费用从按日/按模型统计里反算出来,
|
||||||
|
* 写入 `usage:claude:weekly:*`,保证周限额在重启后不归零。
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 只回填本周,不做历史回填(符合“只要本周数据”诉求)
|
||||||
|
* - 会加分布式锁,避免多实例重复跑
|
||||||
|
* - 会写 done 标记:同一周内重启默认不重复回填(需要时可手动删掉 done key)
|
||||||
|
*/
|
||||||
|
async backfillCurrentWeekClaudeCosts() {
|
||||||
|
const client = redis.getClientSafe()
|
||||||
|
if (!client) {
|
||||||
|
logger.warn('⚠️ 本周 Claude 周费用回填跳过:Redis client 不可用')
|
||||||
|
return { success: false, reason: 'redis_unavailable' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pricingService || !pricingService.pricingData) {
|
||||||
|
logger.warn('⚠️ 本周 Claude 周费用回填跳过:pricing service 未初始化')
|
||||||
|
return { success: false, reason: 'pricing_uninitialized' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekString = redis.getWeekStringInTimezone()
|
||||||
|
const doneKey = `init:weekly_claude_cost:${weekString}:done`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const alreadyDone = await client.get(doneKey)
|
||||||
|
if (alreadyDone) {
|
||||||
|
logger.info(`ℹ️ 本周 Claude 周费用回填已完成(${weekString}),跳过`)
|
||||||
|
return { success: true, skipped: true }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 尽力而为:读取失败不阻断启动回填流程。
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockKey = `lock:init:weekly_claude_cost:${weekString}`
|
||||||
|
const lockValue = `${process.pid}:${Date.now()}`
|
||||||
|
const lockTtlMs = 15 * 60 * 1000
|
||||||
|
|
||||||
|
const lockAcquired = await redis.setAccountLock(lockKey, lockValue, lockTtlMs)
|
||||||
|
if (!lockAcquired) {
|
||||||
|
logger.info(`ℹ️ 本周 Claude 周费用回填已在运行(${weekString}),跳过`)
|
||||||
|
return { success: true, skipped: true, reason: 'locked' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = Date.now()
|
||||||
|
try {
|
||||||
|
logger.info(`💰 开始回填本周 Claude 周费用:${weekString}(仅本周)...`)
|
||||||
|
|
||||||
|
const keyIds = await redis.scanApiKeyIds()
|
||||||
|
const dates = this._getCurrentWeekDatesInTimezone()
|
||||||
|
|
||||||
|
const costByKeyId = new Map()
|
||||||
|
let scannedKeys = 0
|
||||||
|
let matchedClaudeKeys = 0
|
||||||
|
|
||||||
|
const toInt = (v) => {
|
||||||
|
const n = parseInt(v || '0', 10)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描“按日 + 按模型”的使用统计 key,并反算 Claude 系列模型的费用。
|
||||||
|
for (const dateStr of dates) {
|
||||||
|
let cursor = '0'
|
||||||
|
const pattern = `usage:*:model:daily:*:${dateStr}`
|
||||||
|
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', 1000)
|
||||||
|
cursor = nextCursor
|
||||||
|
scannedKeys += keys.length
|
||||||
|
|
||||||
|
const entries = []
|
||||||
|
for (const usageKey of keys) {
|
||||||
|
// usage:{keyId}:model:daily:{model}:{YYYY-MM-DD}
|
||||||
|
const match = usageKey.match(/^usage:([^:]+):model:daily:(.+):(\d{4}-\d{2}-\d{2})$/)
|
||||||
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const keyId = match[1]
|
||||||
|
const model = match[2]
|
||||||
|
if (!isClaudeFamilyModel(model)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matchedClaudeKeys++
|
||||||
|
entries.push({ usageKey, keyId, model })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline = client.pipeline()
|
||||||
|
for (const entry of entries) {
|
||||||
|
pipeline.hgetall(entry.usageKey)
|
||||||
|
}
|
||||||
|
const results = await pipeline.exec()
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
const entry = entries[i]
|
||||||
|
const [, data] = results[i] || []
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputTokens = toInt(data.totalInputTokens || data.inputTokens)
|
||||||
|
const outputTokens = toInt(data.totalOutputTokens || data.outputTokens)
|
||||||
|
const cacheReadTokens = toInt(data.totalCacheReadTokens || data.cacheReadTokens)
|
||||||
|
const cacheCreateTokens = toInt(data.totalCacheCreateTokens || data.cacheCreateTokens)
|
||||||
|
const ephemeral5mTokens = toInt(data.ephemeral5mTokens)
|
||||||
|
const ephemeral1hTokens = toInt(data.ephemeral1hTokens)
|
||||||
|
|
||||||
|
const cacheCreationTotal =
|
||||||
|
ephemeral5mTokens > 0 || ephemeral1hTokens > 0
|
||||||
|
? ephemeral5mTokens + ephemeral1hTokens
|
||||||
|
: cacheCreateTokens
|
||||||
|
|
||||||
|
const usage = {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: cacheCreationTotal,
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) {
|
||||||
|
usage.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens: ephemeral5mTokens,
|
||||||
|
ephemeral_1h_input_tokens: ephemeral1hTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const costInfo = pricingService.calculateCost(usage, entry.model)
|
||||||
|
const cost = costInfo && costInfo.totalCost ? costInfo.totalCost : 0
|
||||||
|
if (cost <= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
costByKeyId.set(entry.keyId, (costByKeyId.get(entry.keyId) || 0) + cost)
|
||||||
|
}
|
||||||
|
} while (cursor !== '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为所有 API Key 写入本周 claude:weekly key,避免读取时回退到旧 opus:weekly 造成口径混淆。
|
||||||
|
const ttlSeconds = 14 * 24 * 3600
|
||||||
|
const batchSize = 500
|
||||||
|
for (let i = 0; i < keyIds.length; i += batchSize) {
|
||||||
|
const batch = keyIds.slice(i, i + batchSize)
|
||||||
|
const pipeline = client.pipeline()
|
||||||
|
for (const keyId of batch) {
|
||||||
|
const weeklyKey = this._buildWeeklyClaudeKey(keyId, weekString)
|
||||||
|
const cost = costByKeyId.get(keyId) || 0
|
||||||
|
pipeline.set(weeklyKey, String(cost))
|
||||||
|
pipeline.expire(weeklyKey, ttlSeconds)
|
||||||
|
}
|
||||||
|
await pipeline.exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入 done 标记(保留略长于 1 周,避免同一周内重启重复回填)。
|
||||||
|
await client.set(doneKey, new Date().toISOString(), 'EX', 10 * 24 * 3600)
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startedAt
|
||||||
|
logger.info(
|
||||||
|
`✅ 本周 Claude 周费用回填完成(${weekString}):keys=${keyIds.length}, scanned=${scannedKeys}, matchedClaude=${matchedClaudeKeys}, filled=${costByKeyId.size}(${durationMs}ms)`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
weekString,
|
||||||
|
keyCount: keyIds.length,
|
||||||
|
scannedKeys,
|
||||||
|
matchedClaudeKeys,
|
||||||
|
filledKeys: costByKeyId.size,
|
||||||
|
durationMs
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ 本周 Claude 周费用回填失败(${weekString}):`, error)
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
} finally {
|
||||||
|
await redis.releaseAccountLock(lockKey, lockValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new WeeklyClaudeCostInitService()
|
||||||
@@ -188,10 +188,54 @@ function isOpus45OrNewer(modelName) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断某个 model 名称是否属于 Anthropic Claude 系列模型。
|
||||||
|
*
|
||||||
|
* 用于 API Key 维度的限额/统计(Claude 周费用)。这里刻意覆盖以下命名:
|
||||||
|
* - 标准 Anthropic 模型:claude-*,包括 claude-3-opus、claude-sonnet-*、claude-haiku-* 等
|
||||||
|
* - Bedrock 模型:{region}.anthropic.claude-... / anthropic.claude-...
|
||||||
|
* - 少数情况下 model 字段可能只包含家族关键词(sonnet/haiku/opus),也视为 Claude 系列
|
||||||
|
*
|
||||||
|
* 注意:会先去掉支持的 vendor 前缀(例如 "ccr,")。
|
||||||
|
*/
|
||||||
|
function isClaudeFamilyModel(modelName) {
|
||||||
|
if (!modelName || typeof modelName !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { baseModel } = parseVendorPrefixedModel(modelName)
|
||||||
|
const m = (baseModel || '').trim().toLowerCase()
|
||||||
|
if (!m) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bedrock 模型格式
|
||||||
|
if (
|
||||||
|
m.includes('.anthropic.claude-') ||
|
||||||
|
m.startsWith('anthropic.claude-') ||
|
||||||
|
m.includes('.claude-')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标准 Anthropic 模型 ID
|
||||||
|
if (m.startsWith('claude-') || m.includes('claude-')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底:某些下游链路里 model 字段可能不带 "claude-" 前缀,但仍包含家族关键词。
|
||||||
|
if (m.includes('opus') || m.includes('sonnet') || m.includes('haiku')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parseVendorPrefixedModel,
|
parseVendorPrefixedModel,
|
||||||
hasVendorPrefix,
|
hasVendorPrefix,
|
||||||
getEffectiveModel,
|
getEffectiveModel,
|
||||||
getVendorType,
|
getVendorType,
|
||||||
isOpus45OrNewer
|
isOpus45OrNewer,
|
||||||
|
isClaudeFamilyModel
|
||||||
}
|
}
|
||||||
|
|||||||
15
web/admin-spa/package-lock.json
generated
15
web/admin-spa/package-lock.json
generated
@@ -1157,7 +1157,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
@@ -1352,7 +1351,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1589,7 +1587,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001726",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.173",
|
"electron-to-chromium": "^1.5.173",
|
||||||
@@ -3063,15 +3060,13 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash-unified": {
|
"node_modules/lodash-unified": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@@ -3623,7 +3618,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3770,7 +3764,6 @@
|
|||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -4035,7 +4028,6 @@
|
|||||||
"integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==",
|
"integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -4533,7 +4525,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4924,7 +4915,6 @@
|
|||||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -5125,7 +5115,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
|
||||||
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.18",
|
"@vue/compiler-dom": "3.5.18",
|
||||||
"@vue/compiler-sfc": "3.5.18",
|
"@vue/compiler-sfc": "3.5.18",
|
||||||
|
|||||||
@@ -232,10 +232,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Opus 模型周费用限制 -->
|
<!-- Claude 模型周费用限制 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
Opus 模型周费用限制 (美元)
|
Claude 模型周费用限制 (美元)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model="form.weeklyOpusCostLimit"
|
v-model="form.weeklyOpusCostLimit"
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户
|
设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -510,7 +510,7 @@ const form = reactive({
|
|||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
dailyCostLimit: '',
|
dailyCostLimit: '',
|
||||||
totalCostLimit: '',
|
totalCostLimit: '',
|
||||||
weeklyOpusCostLimit: '', // 新增Opus周费用限制
|
weeklyOpusCostLimit: '', // 新增Claude周费用限制
|
||||||
permissions: '', // 空字符串表示不修改
|
permissions: '', // 空字符串表示不修改
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
geminiAccountId: '',
|
geminiAccountId: '',
|
||||||
|
|||||||
@@ -386,7 +386,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>Opus 模型周费用限制 (美元)</label
|
>Claude 模型周费用限制 (美元)</label
|
||||||
>
|
>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -428,7 +428,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制
|
设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效,0
|
||||||
|
或留空表示无限制
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -324,7 +324,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||||
>Opus 模型周费用限制 (美元)</label
|
>Claude 模型周费用限制 (美元)</label
|
||||||
>
|
>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -366,7 +366,8 @@
|
|||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制
|
设置 Claude 模型的周费用限制(周一到周日),仅对 Claude 模型请求生效,0
|
||||||
|
或留空表示无限制
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,11 +167,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Opus 模型周费用限制 -->
|
<!-- Claude 模型周费用限制 -->
|
||||||
<div v-if="statsData.limits.weeklyOpusCostLimit > 0">
|
<div v-if="statsData.limits.weeklyOpusCostLimit > 0">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
|
||||||
>Opus 模型周费用限制</span
|
>Claude 模型周费用限制</span
|
||||||
>
|
>
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
|
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
|
||||||
${{ statsData.limits.weeklyOpusCost.toFixed(4) }} / ${{
|
${{ statsData.limits.weeklyOpusCost.toFixed(4) }} / ${{
|
||||||
@@ -383,7 +383,7 @@ const getTotalCostProgressColor = () => {
|
|||||||
return 'bg-blue-500'
|
return 'bg-blue-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Opus周费用进度
|
// 获取Claude周费用进度
|
||||||
const getOpusWeeklyCostProgress = () => {
|
const getOpusWeeklyCostProgress = () => {
|
||||||
if (
|
if (
|
||||||
!statsData.value.limits.weeklyOpusCostLimit ||
|
!statsData.value.limits.weeklyOpusCostLimit ||
|
||||||
@@ -395,7 +395,7 @@ const getOpusWeeklyCostProgress = () => {
|
|||||||
return Math.min(percentage, 100)
|
return Math.min(percentage, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Opus周费用进度条颜色
|
// 获取Claude周费用进度条颜色
|
||||||
const getOpusWeeklyCostProgressColor = () => {
|
const getOpusWeeklyCostProgressColor = () => {
|
||||||
const progress = getOpusWeeklyCostProgress()
|
const progress = getOpusWeeklyCostProgress()
|
||||||
if (progress >= 100) return 'bg-red-500'
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
|||||||
Reference in New Issue
Block a user