merge: 合并远程 dev 分支,整合 CCR 和 OpenAI-Responses 功能

## 合并内容
- 成功合并远程 dev 分支的 CCR (Claude Connector) 功能
- 保留本地的 OpenAI-Responses 账户管理功能
- 解决所有合并冲突,保留双方功能

## UI 调整
- 将 CCR 平台归类到 Claude 分组中
- 保留新的平台分组选择器设计
- 支持所有平台类型:Claude、CCR、OpenAI、OpenAI-Responses、Gemini、Azure OpenAI、Bedrock

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-09-10 15:49:52 +08:00
11 changed files with 3145 additions and 65 deletions

View File

@@ -3,6 +3,7 @@ const apiKeyService = require('../services/apiKeyService')
const claudeAccountService = require('../services/claudeAccountService')
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService')
const bedrockAccountService = require('../services/bedrockAccountService')
const ccrAccountService = require('../services/ccrAccountService')
const geminiAccountService = require('../services/geminiAccountService')
const openaiAccountService = require('../services/openaiAccountService')
const openaiResponsesAccountService = require('../services/openaiResponsesAccountService')
@@ -2498,9 +2499,9 @@ router.post('/claude-console-accounts', authenticateAdmin, async (req, res) => {
quotaResetTime: quotaResetTime || '00:00'
})
// 如果是分组类型,将账户添加到分组
// 如果是分组类型,将账户添加到分组CCR 归属 Claude 平台分组)
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
await accountGroupService.addAccountToGroup(newAccount.id, groupId, 'claude')
}
logger.success(`🎮 Admin created Claude Console account: ${name}`)
@@ -2741,6 +2742,382 @@ router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async
}
})
// 🔧 CCR 账户管理
// 获取所有CCR账户
router.get('/ccr-accounts', authenticateAdmin, async (req, res) => {
try {
const { platform, groupId } = req.query
let accounts = await ccrAccountService.getAllAccounts()
// 根据查询参数进行筛选
if (platform && platform !== 'all' && platform !== 'ccr') {
// 如果指定了其他平台,返回空数组
accounts = []
}
// 如果指定了分组筛选
if (groupId && groupId !== 'all') {
if (groupId === 'ungrouped') {
// 筛选未分组账户
const filteredAccounts = []
for (const account of accounts) {
const groups = await accountGroupService.getAccountGroups(account.id)
if (!groups || groups.length === 0) {
filteredAccounts.push(account)
}
}
accounts = filteredAccounts
} else {
// 筛选特定分组的账户
const groupMembers = await accountGroupService.getGroupMembers(groupId)
accounts = accounts.filter((account) => groupMembers.includes(account.id))
}
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(
accounts.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id)
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
groupInfos,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
}
} catch (statsError) {
logger.warn(
`⚠️ Failed to get usage stats for CCR account ${account.id}:`,
statsError.message
)
try {
const groupInfos = await accountGroupService.getAccountGroups(account.id)
return {
...account,
// 转换schedulable为布尔值
schedulable: account.schedulable === 'true' || account.schedulable === true,
groupInfos,
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
} catch (groupError) {
logger.warn(
`⚠️ Failed to get group info for CCR account ${account.id}:`,
groupError.message
)
return {
...account,
groupInfos: [],
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
}
}
}
})
)
return res.json({ success: true, data: accountsWithStats })
} catch (error) {
logger.error('❌ Failed to get CCR accounts:', error)
return res.status(500).json({ error: 'Failed to get CCR accounts', message: error.message })
}
})
// 创建新的CCR账户
router.post('/ccr-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
apiUrl,
apiKey,
priority,
supportedModels,
userAgent,
rateLimitDuration,
proxy,
accountType,
groupId,
dailyQuota,
quotaResetTime
} = req.body
if (!name || !apiUrl || !apiKey) {
return res.status(400).json({ error: 'Name, API URL and API Key are required' })
}
// 验证priority的有效性1-100
if (priority !== undefined && (priority < 1 || priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated', 'group'].includes(accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果是分组类型验证groupId
if (accountType === 'group' && !groupId) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
}
const newAccount = await ccrAccountService.createAccount({
name,
description,
apiUrl,
apiKey,
priority: priority || 50,
supportedModels: supportedModels || [],
userAgent,
rateLimitDuration:
rateLimitDuration !== undefined && rateLimitDuration !== null ? rateLimitDuration : 60,
proxy,
accountType: accountType || 'shared',
dailyQuota: dailyQuota || 0,
quotaResetTime: quotaResetTime || '00:00'
})
// 如果是分组类型,将账户添加到分组
if (accountType === 'group' && groupId) {
await accountGroupService.addAccountToGroup(newAccount.id, groupId)
}
logger.success(`🔧 Admin created CCR account: ${name}`)
return res.json({ success: true, data: newAccount })
} catch (error) {
logger.error('❌ Failed to create CCR account:', error)
return res.status(500).json({ error: 'Failed to create CCR account', message: error.message })
}
})
// 更新CCR账户
router.put('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const updates = req.body
// 验证priority的有效性1-100
if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' })
}
// 验证accountType的有效性
if (updates.accountType && !['shared', 'dedicated', 'group'].includes(updates.accountType)) {
return res
.status(400)
.json({ error: 'Invalid account type. Must be "shared", "dedicated" or "group"' })
}
// 如果更新为分组类型验证groupId
if (updates.accountType === 'group' && !updates.groupId) {
return res.status(400).json({ error: 'Group ID is required for group type accounts' })
}
// 获取账户当前信息以处理分组变更
const currentAccount = await ccrAccountService.getAccount(accountId)
if (!currentAccount) {
return res.status(404).json({ error: 'Account not found' })
}
// 处理分组的变更
if (updates.accountType !== undefined) {
// 如果之前是分组类型,需要从所有分组中移除
if (currentAccount.accountType === 'group') {
const oldGroups = await accountGroupService.getAccountGroups(accountId)
for (const oldGroup of oldGroups) {
await accountGroupService.removeAccountFromGroup(accountId, oldGroup.id)
}
}
// 如果新类型是分组,处理多分组支持
if (updates.accountType === 'group') {
if (Object.prototype.hasOwnProperty.call(updates, 'groupIds')) {
// 如果明确提供了 groupIds 参数(包括空数组)
if (updates.groupIds && updates.groupIds.length > 0) {
// 设置新的多分组
await accountGroupService.setAccountGroups(accountId, updates.groupIds, 'claude')
} else {
// groupIds 为空数组,从所有分组中移除
await accountGroupService.removeAccountFromAllGroups(accountId)
}
} else if (updates.groupId) {
// 向后兼容:仅当没有 groupIds 但有 groupId 时使用单分组逻辑
await accountGroupService.addAccountToGroup(accountId, updates.groupId, 'claude')
}
}
}
await ccrAccountService.updateAccount(accountId, updates)
logger.success(`📝 Admin updated CCR account: ${accountId}`)
return res.json({ success: true, message: 'CCR account updated successfully' })
} catch (error) {
logger.error('❌ Failed to update CCR account:', error)
return res.status(500).json({ error: 'Failed to update CCR account', message: error.message })
}
})
// 删除CCR账户
router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
// 获取账户信息以检查是否在分组中
const account = await ccrAccountService.getAccount(accountId)
if (account && account.accountType === 'group') {
const groups = await accountGroupService.getAccountGroups(accountId)
for (const group of groups) {
await accountGroupService.removeAccountFromGroup(accountId, group.id)
}
}
await ccrAccountService.deleteAccount(accountId)
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
return res.json({ success: true, message: 'CCR account deleted successfully' })
} catch (error) {
logger.error('❌ Failed to delete CCR account:', error)
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
}
})
// 切换CCR账户状态
router.put('/ccr-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await ccrAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newStatus = !account.isActive
await ccrAccountService.updateAccount(accountId, { isActive: newStatus })
logger.success(
`🔄 Admin toggled CCR account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`
)
return res.json({ success: true, isActive: newStatus })
} catch (error) {
logger.error('❌ Failed to toggle CCR account status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle account status', message: error.message })
}
})
// 切换CCR账户调度状态
router.put('/ccr-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const account = await ccrAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({ error: 'Account not found' })
}
const newSchedulable = !account.schedulable
await ccrAccountService.updateAccount(accountId, { schedulable: newSchedulable })
// 如果账号被禁用发送webhook通知
if (!newSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: account.id,
accountName: account.name || 'CCR Account',
platform: 'ccr',
status: 'disabled',
errorCode: 'CCR_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled CCR account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`
)
return res.json({ success: true, schedulable: newSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle CCR account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
})
// 获取CCR账户的使用统计
router.get('/ccr-accounts/:accountId/usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const usageStats = await ccrAccountService.getAccountUsageStats(accountId)
if (!usageStats) {
return res.status(404).json({ error: 'Account not found' })
}
return res.json(usageStats)
} catch (error) {
logger.error('❌ Failed to get CCR account usage stats:', error)
return res.status(500).json({ error: 'Failed to get usage stats', message: error.message })
}
})
// 手动重置CCR账户的每日使用量
router.post('/ccr-accounts/:accountId/reset-usage', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
await ccrAccountService.resetDailyUsage(accountId)
logger.success(`✅ Admin manually reset daily usage for CCR account: ${accountId}`)
return res.json({ success: true, message: 'Daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset CCR account daily usage:', error)
return res.status(500).json({ error: 'Failed to reset daily usage', message: error.message })
}
})
// 重置CCR账户状态清除所有异常状态
router.post('/ccr-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await ccrAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for CCR account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset CCR account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
// 手动重置所有CCR账户的每日使用量
router.post('/ccr-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
try {
await ccrAccountService.resetAllDailyUsage()
logger.success('✅ Admin manually reset daily usage for all CCR accounts')
return res.json({ success: true, message: 'All daily usage reset successfully' })
} catch (error) {
logger.error('❌ Failed to reset all CCR accounts daily usage:', error)
return res
.status(500)
.json({ error: 'Failed to reset all daily usage', message: error.message })
}
})
// ☁️ Bedrock 账户管理
// 获取所有Bedrock账户
@@ -3566,6 +3943,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
geminiAccounts,
bedrockAccountsResult,
openaiAccounts,
ccrAccounts,
todayStats,
systemAverages,
realtimeMetrics
@@ -3576,6 +3954,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeConsoleAccountService.getAllAccounts(),
geminiAccountService.getAllAccounts(),
bedrockAccountService.getAllAccounts(),
ccrAccountService.getAllAccounts(),
redis.getAllOpenAIAccounts(),
redis.getTodayStats(),
redis.getSystemAverages(),
@@ -3747,6 +4126,29 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length
// CCR账户统计
const normalCcrAccounts = ccrAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false &&
!(acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited)
).length
const abnormalCcrAccounts = ccrAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedCcrAccounts = ccrAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length
const rateLimitedCcrAccounts = ccrAccounts.filter(
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length
const dashboard = {
overview: {
totalApiKeys: apiKeys.length,
@@ -3757,31 +4159,36 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeConsoleAccounts.length +
geminiAccounts.length +
bedrockAccounts.length +
openaiAccounts.length,
openaiAccounts.length +
ccrAccounts.length,
normalAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts +
normalOpenAIAccounts,
normalOpenAIAccounts +
normalCcrAccounts,
abnormalAccounts:
abnormalClaudeAccounts +
abnormalClaudeConsoleAccounts +
abnormalGeminiAccounts +
abnormalBedrockAccounts +
abnormalOpenAIAccounts,
abnormalOpenAIAccounts +
abnormalCcrAccounts,
pausedAccounts:
pausedClaudeAccounts +
pausedClaudeConsoleAccounts +
pausedGeminiAccounts +
pausedBedrockAccounts +
pausedOpenAIAccounts,
pausedOpenAIAccounts +
pausedCcrAccounts,
rateLimitedAccounts:
rateLimitedClaudeAccounts +
rateLimitedClaudeConsoleAccounts +
rateLimitedGeminiAccounts +
rateLimitedBedrockAccounts +
rateLimitedOpenAIAccounts,
rateLimitedOpenAIAccounts +
rateLimitedCcrAccounts,
// 各平台详细统计
accountsByPlatform: {
claude: {
@@ -3818,6 +4225,13 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
abnormal: abnormalOpenAIAccounts,
paused: pausedOpenAIAccounts,
rateLimited: rateLimitedOpenAIAccounts
},
ccr: {
total: ccrAccounts.length,
normal: normalCcrAccounts,
abnormal: abnormalCcrAccounts,
paused: pausedCcrAccounts,
rateLimited: rateLimitedCcrAccounts
}
},
// 保留旧字段以兼容
@@ -3826,7 +4240,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts +
normalOpenAIAccounts,
normalOpenAIAccounts +
normalCcrAccounts,
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,

View File

@@ -2,6 +2,7 @@ const express = require('express')
const claudeRelayService = require('../services/claudeRelayService')
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService')
const bedrockRelayService = require('../services/bedrockRelayService')
const ccrRelayService = require('../services/ccrRelayService')
const bedrockAccountService = require('../services/bedrockAccountService')
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
const apiKeyService = require('../services/apiKeyService')
@@ -9,6 +10,7 @@ const pricingService = require('../services/pricingService')
const { authenticateApiKey } = require('../middleware/auth')
const logger = require('../utils/logger')
const redis = require('../models/redis')
const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
const sessionHelper = require('../utils/sessionHelper')
const router = express.Router()
@@ -40,6 +42,23 @@ async function handleMessagesRequest(req, res) {
})
}
// 模型限制(允许列表)校验:统一在此处处理(去除供应商前缀)
if (
req.apiKey.enableModelRestriction &&
Array.isArray(req.apiKey.restrictedModels) &&
req.apiKey.restrictedModels.length > 0
) {
const effectiveModel = getEffectiveModel(req.body.model || '')
if (!req.apiKey.restrictedModels.includes(effectiveModel)) {
return res.status(403).json({
error: {
type: 'forbidden',
message: '暂无该模型访问权限'
}
})
}
}
// 检查是否为流式请求
const isStream = req.body.stream === true
@@ -354,6 +373,110 @@ async function handleMessagesRequest(req, res) {
}
return undefined
}
} else if (accountType === 'ccr') {
// CCR账号使用CCR转发服务需要传递accountId
await ccrRelayService.relayStreamRequestWithUsageCapture(
req.body,
req.apiKey,
res,
req.headers,
(usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
logger.info(
'🎯 CCR usage callback triggered with complete data:',
JSON.stringify(usageData, null, 2)
)
if (
usageData &&
usageData.input_tokens !== undefined &&
usageData.output_tokens !== undefined
) {
const inputTokens = usageData.input_tokens || 0
const outputTokens = usageData.output_tokens || 0
// 兼容处理:如果有详细的 cache_creation 对象,使用它;否则使用总的 cache_creation_input_tokens
let cacheCreateTokens = usageData.cache_creation_input_tokens || 0
let ephemeral5mTokens = 0
let ephemeral1hTokens = 0
if (usageData.cache_creation && typeof usageData.cache_creation === 'object') {
ephemeral5mTokens = usageData.cache_creation.ephemeral_5m_input_tokens || 0
ephemeral1hTokens = usageData.cache_creation.ephemeral_1h_input_tokens || 0
// 总的缓存创建 tokens 是两者之和
cacheCreateTokens = ephemeral5mTokens + ephemeral1hTokens
}
const cacheReadTokens = usageData.cache_read_input_tokens || 0
const model = usageData.model || 'unknown'
// 记录真实的token使用量包含模型信息和所有4种token以及账户ID
const usageAccountId = usageData.accountId
// 构建 usage 对象以传递给 recordUsage
const usageObject = {
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: cacheCreateTokens,
cache_read_input_tokens: cacheReadTokens
}
// 如果有详细的缓存创建数据,添加到 usage 对象中
if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) {
usageObject.cache_creation = {
ephemeral_5m_input_tokens: ephemeral5mTokens,
ephemeral_1h_input_tokens: ephemeral1hTokens
}
}
apiKeyService
.recordUsageWithDetails(req.apiKey.id, usageObject, model, usageAccountId, 'ccr')
.catch((error) => {
logger.error('❌ Failed to record CCR stream usage:', error)
})
// 更新时间窗口内的token计数和费用
if (req.rateLimitInfo) {
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
// 更新Token计数向后兼容
redis
.getClient()
.incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
.catch((error) => {
logger.error('❌ Failed to update rate limit token count:', error)
})
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
// 计算并更新费用计数(新功能)
if (req.rateLimitInfo.costCountKey) {
const costInfo = pricingService.calculateCost(usageData, model)
if (costInfo.totalCost > 0) {
redis
.getClient()
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
.catch((error) => {
logger.error('❌ Failed to update rate limit cost count:', error)
})
logger.api(
`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`
)
}
}
}
usageDataCaptured = true
logger.api(
`📊 CCR stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`
)
} else {
logger.warn(
'⚠️ CCR usage callback triggered but data is incomplete:',
JSON.stringify(usageData)
)
}
},
accountId
)
}
// 流式请求完成后 - 如果没有捕获到usage数据记录警告但不进行估算
@@ -447,6 +570,17 @@ async function handleMessagesRequest(req, res) {
accountId
}
}
} else if (accountType === 'ccr') {
// CCR账号使用CCR转发服务
logger.debug(`[DEBUG] Calling ccrRelayService.relayRequest with accountId: ${accountId}`)
response = await ccrRelayService.relayRequest(
req.body,
req.apiKey,
req,
res,
req.headers,
accountId
)
}
logger.info('📡 Claude API response received', {
@@ -483,7 +617,10 @@ async function handleMessagesRequest(req, res) {
const outputTokens = jsonData.usage.output_tokens || 0
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0
const model = jsonData.model || req.body.model || 'unknown'
// Parse the model to remove vendor prefix if present (e.g., "ccr,gemini-2.5-pro" -> "gemini-2.5-pro")
const rawModel = jsonData.model || req.body.model || 'unknown'
const { baseModel } = parseVendorPrefixedModel(rawModel)
const model = baseModel || rawModel
// 记录真实的token使用量包含模型信息和所有4种token以及账户ID
const { accountId: responseAccountId } = response
@@ -762,6 +899,23 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
logger.info(`🔢 Processing token count request for key: ${req.apiKey.name}`)
// 模型限制(允许列表)校验:统一在此处处理(去除供应商前缀)
if (
req.apiKey.enableModelRestriction &&
Array.isArray(req.apiKey.restrictedModels) &&
req.apiKey.restrictedModels.length > 0
) {
const effectiveModel = getEffectiveModel(req.body.model || '')
if (!req.apiKey.restrictedModels.includes(effectiveModel)) {
return res.status(403).json({
error: {
type: 'forbidden',
message: '暂无该模型访问权限'
}
})
}
}
// 生成会话哈希用于sticky会话
const sessionHash = sessionHelper.generateSessionHash(req.body)
@@ -801,6 +955,14 @@ router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) =>
customPath: '/v1/messages/count_tokens' // 指定count_tokens路径
}
)
} else if (accountType === 'ccr') {
// CCR不支持count_tokens
return res.status(501).json({
error: {
type: 'not_supported',
message: 'Token counting is not supported for CCR accounts'
}
})
} else {
// Bedrock不支持count_tokens
return res.status(501).json({