diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 1b446093..df4ccaa3 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1313,6 +1313,7 @@ const authenticateApiKey = async (req, res, next) => { restrictedModels: validation.keyData.restrictedModels, enableClientRestriction: validation.keyData.enableClientRestriction, allowedClients: validation.keyData.allowedClients, + allow1mContext: validation.keyData.allow1mContext, dailyCostLimit: validation.keyData.dailyCostLimit, dailyCost: validation.keyData.dailyCost, totalCostLimit: validation.keyData.totalCostLimit, diff --git a/src/models/redis.js b/src/models/redis.js index 10323af6..122011df 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -773,7 +773,7 @@ class RedisClient { const parsed = { ...data } // 布尔字段 - const boolFields = ['isActive', 'enableModelRestriction', 'isDeleted'] + const boolFields = ['isActive', 'enableModelRestriction', 'isDeleted', 'allow1mContext'] for (const field of boolFields) { if (parsed[field] !== undefined) { parsed[field] = parsed[field] === 'true' diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js index cff0fb65..a70776c3 100644 --- a/src/routes/admin/apiKeys.js +++ b/src/routes/admin/apiKeys.js @@ -1483,6 +1483,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, + allow1mContext, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, @@ -1562,6 +1563,10 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { return res.status(400).json({ error: 'Allowed clients must be an array' }) } + if (allow1mContext !== undefined && typeof allow1mContext !== 'boolean') { + return res.status(400).json({ error: 'allow1mContext must be a boolean' }) + } + // 验证标签字段 if (tags !== undefined && !Array.isArray(tags)) { return res.status(400).json({ error: 'Tags must be an array' }) @@ -1662,6 +1667,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, + allow1mContext, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, @@ -1713,6 +1719,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, + allow1mContext, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, @@ -1778,6 +1785,7 @@ router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, + allow1mContext, dailyCostLimit, totalCostLimit, weeklyOpusCostLimit, @@ -1939,6 +1947,9 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => { if (updates.serviceRates !== undefined) { finalUpdates.serviceRates = updates.serviceRates } + if (updates.allow1mContext !== undefined) { + finalUpdates.allow1mContext = updates.allow1mContext + } if (updates.weeklyResetDay !== undefined) { const day = Number(updates.weeklyResetDay) if (Number.isInteger(day) && day >= 1 && day <= 7) { @@ -2079,6 +2090,7 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { restrictedModels, enableClientRestriction, allowedClients, + allow1mContext, expiresAt, dailyCostLimit, totalCostLimit, @@ -2212,6 +2224,13 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { updates.allowedClients = allowedClients } + if (allow1mContext !== undefined) { + if (typeof allow1mContext !== 'boolean') { + return res.status(400).json({ error: 'allow1mContext must be a boolean' }) + } + updates.allow1mContext = allow1mContext + } + // 处理过期时间字段 if (expiresAt !== undefined) { if (expiresAt === null) { diff --git a/src/routes/api.js b/src/routes/api.js index 375c04cb..6dc16048 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -197,13 +197,14 @@ async function handleMessagesRequest(req, res) { } } - // 拦截 1M 上下文窗口请求(anthropic-beta 包含 context-1m) + // 检测 1M 上下文窗口请求(anthropic-beta 包含 context-1m) const betaHeader = (req.headers['anthropic-beta'] || '').toLowerCase() - if (betaHeader.includes('context-1m')) { + const is1mContextRequest = betaHeader.includes('context-1m') + if (is1mContextRequest && !req.apiKey.allow1mContext) { return res.status(403).json({ error: { type: 'forbidden', - message: '暂不支持 1M 上下文窗口,请切换为非 [1m] 模型' + message: '该 API Key 未启用 1M 上下文窗口,请联系管理员开启或切换为非 [1m] 模型' } }) } @@ -388,6 +389,16 @@ async function handleMessagesRequest(req, res) { throw error } + // 1M 上下文窗口:检查调度到的账户类型是否支持 + if (is1mContextRequest && accountType !== 'bedrock') { + return res.status(403).json({ + error: { + type: 'forbidden', + message: `1M 上下文窗口仅支持 Bedrock 账户类型,当前调度到 ${accountType},请绑定 Bedrock 账户` + } + }) + } + // 🔗 在成功调度后建立会话绑定(仅 claude-official 类型) // claude-official 只接受:1) 新会话 2) 已绑定的会话 if ( diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 53f873a7..2ba0b2b6 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -153,6 +153,7 @@ class ApiKeyService { restrictedModels = [], enableClientRestriction = false, allowedClients = [], + allow1mContext = false, dailyCostLimit = 0, totalCostLimit = 0, weeklyOpusCostLimit = 0, @@ -197,6 +198,7 @@ class ApiKeyService { restrictedModels: JSON.stringify(restrictedModels || []), enableClientRestriction: String(enableClientRestriction || false), allowedClients: JSON.stringify(allowedClients || []), + allow1mContext: String(allow1mContext || false), dailyCostLimit: String(dailyCostLimit || 0), totalCostLimit: String(totalCostLimit || 0), weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0), diff --git a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue index 18d6b8c8..f6d629a6 100644 --- a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue @@ -322,6 +322,32 @@ + +
+ 启用后允许使用 [1m] 模型(需要 Bedrock 账户支持) +
++ 启用后允许使用 [1m] 模型(需要 Bedrock 账户支持) +
++ 启用后允许使用 [1m] 模型(需要 Bedrock 账户支持) +
+