mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
feat: 添加API Key时间窗口限流功能并移除累计总量限制
- 新增时间窗口限流功能,支持按分钟设置时间窗口 - 支持在时间窗口内限制请求次数和Token使用量 - 移除原有的累计总量限制,只保留时间窗口限制 - Token统计包含所有4种类型:输入、输出、缓存创建、缓存读取 - 前端UI优化,明确显示限流参数的作用范围 - 限流触发时提供友好的错误提示和重置时间 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,95 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查时间窗口限流
|
||||||
|
const rateLimitWindow = validation.keyData.rateLimitWindow || 0;
|
||||||
|
const rateLimitRequests = validation.keyData.rateLimitRequests || 0;
|
||||||
|
|
||||||
|
if (rateLimitWindow > 0 && (rateLimitRequests > 0 || validation.keyData.tokenLimit > 0)) {
|
||||||
|
const windowStartKey = `rate_limit:window_start:${validation.keyData.id}`;
|
||||||
|
const requestCountKey = `rate_limit:requests:${validation.keyData.id}`;
|
||||||
|
const tokenCountKey = `rate_limit:tokens:${validation.keyData.id}`;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const windowDuration = rateLimitWindow * 60 * 1000; // 转换为毫秒
|
||||||
|
|
||||||
|
// 获取窗口开始时间
|
||||||
|
let windowStart = await redis.getClient().get(windowStartKey);
|
||||||
|
|
||||||
|
if (!windowStart) {
|
||||||
|
// 第一次请求,设置窗口开始时间
|
||||||
|
await redis.getClient().set(windowStartKey, now, 'PX', windowDuration);
|
||||||
|
await redis.getClient().set(requestCountKey, 0, 'PX', windowDuration);
|
||||||
|
await redis.getClient().set(tokenCountKey, 0, 'PX', windowDuration);
|
||||||
|
windowStart = now;
|
||||||
|
} else {
|
||||||
|
windowStart = parseInt(windowStart);
|
||||||
|
|
||||||
|
// 检查窗口是否已过期
|
||||||
|
if (now - windowStart >= windowDuration) {
|
||||||
|
// 窗口已过期,重置
|
||||||
|
await redis.getClient().set(windowStartKey, now, 'PX', windowDuration);
|
||||||
|
await redis.getClient().set(requestCountKey, 0, 'PX', windowDuration);
|
||||||
|
await redis.getClient().set(tokenCountKey, 0, 'PX', windowDuration);
|
||||||
|
windowStart = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前计数
|
||||||
|
const currentRequests = parseInt(await redis.getClient().get(requestCountKey) || '0');
|
||||||
|
const currentTokens = parseInt(await redis.getClient().get(tokenCountKey) || '0');
|
||||||
|
|
||||||
|
// 检查请求次数限制
|
||||||
|
if (rateLimitRequests > 0 && currentRequests >= rateLimitRequests) {
|
||||||
|
const resetTime = new Date(windowStart + windowDuration);
|
||||||
|
const remainingMinutes = Math.ceil((resetTime - now) / 60000);
|
||||||
|
|
||||||
|
logger.security(`🚦 Rate limit exceeded (requests) for key: ${validation.keyData.id} (${validation.keyData.name}), requests: ${currentRequests}/${rateLimitRequests}`);
|
||||||
|
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'Rate limit exceeded',
|
||||||
|
message: `已达到请求次数限制 (${rateLimitRequests} 次),将在 ${remainingMinutes} 分钟后重置`,
|
||||||
|
currentRequests,
|
||||||
|
requestLimit: rateLimitRequests,
|
||||||
|
resetAt: resetTime.toISOString(),
|
||||||
|
remainingMinutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Token使用量限制
|
||||||
|
const tokenLimit = parseInt(validation.keyData.tokenLimit);
|
||||||
|
if (tokenLimit > 0 && currentTokens >= tokenLimit) {
|
||||||
|
const resetTime = new Date(windowStart + windowDuration);
|
||||||
|
const remainingMinutes = Math.ceil((resetTime - now) / 60000);
|
||||||
|
|
||||||
|
logger.security(`🚦 Rate limit exceeded (tokens) for key: ${validation.keyData.id} (${validation.keyData.name}), tokens: ${currentTokens}/${tokenLimit}`);
|
||||||
|
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'Rate limit exceeded',
|
||||||
|
message: `已达到 Token 使用限制 (${tokenLimit} tokens),将在 ${remainingMinutes} 分钟后重置`,
|
||||||
|
currentTokens,
|
||||||
|
tokenLimit,
|
||||||
|
resetAt: resetTime.toISOString(),
|
||||||
|
remainingMinutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加请求计数
|
||||||
|
await redis.getClient().incr(requestCountKey);
|
||||||
|
|
||||||
|
// 存储限流信息到请求对象
|
||||||
|
req.rateLimitInfo = {
|
||||||
|
windowStart,
|
||||||
|
windowDuration,
|
||||||
|
requestCountKey,
|
||||||
|
tokenCountKey,
|
||||||
|
currentRequests: currentRequests + 1,
|
||||||
|
currentTokens,
|
||||||
|
rateLimitRequests,
|
||||||
|
tokenLimit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 将验证信息添加到请求对象(只包含必要信息)
|
// 将验证信息添加到请求对象(只包含必要信息)
|
||||||
req.apiKey = {
|
req.apiKey = {
|
||||||
id: validation.keyData.id,
|
id: validation.keyData.id,
|
||||||
@@ -110,6 +199,8 @@ const authenticateApiKey = async (req, res, next) => {
|
|||||||
tokenLimit: validation.keyData.tokenLimit,
|
tokenLimit: validation.keyData.tokenLimit,
|
||||||
claudeAccountId: validation.keyData.claudeAccountId,
|
claudeAccountId: validation.keyData.claudeAccountId,
|
||||||
concurrencyLimit: validation.keyData.concurrencyLimit,
|
concurrencyLimit: validation.keyData.concurrencyLimit,
|
||||||
|
rateLimitWindow: validation.keyData.rateLimitWindow,
|
||||||
|
rateLimitRequests: validation.keyData.rateLimitRequests,
|
||||||
enableModelRestriction: validation.keyData.enableModelRestriction,
|
enableModelRestriction: validation.keyData.enableModelRestriction,
|
||||||
restrictedModels: validation.keyData.restrictedModels
|
restrictedModels: validation.keyData.restrictedModels
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
claudeAccountId,
|
claudeAccountId,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
|
rateLimitWindow,
|
||||||
|
rateLimitRequests,
|
||||||
enableModelRestriction,
|
enableModelRestriction,
|
||||||
restrictedModels
|
restrictedModels
|
||||||
} = req.body;
|
} = req.body;
|
||||||
@@ -59,6 +61,14 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' });
|
return res.status(400).json({ error: 'Concurrency limit must be a non-negative integer' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rateLimitWindow !== undefined && rateLimitWindow !== null && rateLimitWindow !== '' && (!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 1)) {
|
||||||
|
return res.status(400).json({ error: 'Rate limit window must be a positive integer (minutes)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimitRequests !== undefined && rateLimitRequests !== null && rateLimitRequests !== '' && (!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 1)) {
|
||||||
|
return res.status(400).json({ error: 'Rate limit requests must be a positive integer' });
|
||||||
|
}
|
||||||
|
|
||||||
// 验证模型限制字段
|
// 验证模型限制字段
|
||||||
if (enableModelRestriction !== undefined && typeof enableModelRestriction !== 'boolean') {
|
if (enableModelRestriction !== undefined && typeof enableModelRestriction !== 'boolean') {
|
||||||
return res.status(400).json({ error: 'Enable model restriction must be a boolean' });
|
return res.status(400).json({ error: 'Enable model restriction must be a boolean' });
|
||||||
@@ -75,6 +85,8 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
claudeAccountId,
|
claudeAccountId,
|
||||||
concurrencyLimit,
|
concurrencyLimit,
|
||||||
|
rateLimitWindow,
|
||||||
|
rateLimitRequests,
|
||||||
enableModelRestriction,
|
enableModelRestriction,
|
||||||
restrictedModels
|
restrictedModels
|
||||||
});
|
});
|
||||||
@@ -91,7 +103,7 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { keyId } = req.params;
|
const { keyId } = req.params;
|
||||||
const { tokenLimit, concurrencyLimit, claudeAccountId, enableModelRestriction, restrictedModels } = req.body;
|
const { tokenLimit, concurrencyLimit, rateLimitWindow, rateLimitRequests, claudeAccountId, enableModelRestriction, restrictedModels } = req.body;
|
||||||
|
|
||||||
// 只允许更新指定字段
|
// 只允许更新指定字段
|
||||||
const updates = {};
|
const updates = {};
|
||||||
@@ -110,6 +122,20 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
|||||||
updates.concurrencyLimit = Number(concurrencyLimit);
|
updates.concurrencyLimit = Number(concurrencyLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rateLimitWindow !== undefined && rateLimitWindow !== null && rateLimitWindow !== '') {
|
||||||
|
if (!Number.isInteger(Number(rateLimitWindow)) || Number(rateLimitWindow) < 0) {
|
||||||
|
return res.status(400).json({ error: 'Rate limit window must be a non-negative integer (minutes)' });
|
||||||
|
}
|
||||||
|
updates.rateLimitWindow = Number(rateLimitWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimitRequests !== undefined && rateLimitRequests !== null && rateLimitRequests !== '') {
|
||||||
|
if (!Number.isInteger(Number(rateLimitRequests)) || Number(rateLimitRequests) < 0) {
|
||||||
|
return res.status(400).json({ error: 'Rate limit requests must be a non-negative integer' });
|
||||||
|
}
|
||||||
|
updates.rateLimitRequests = Number(rateLimitRequests);
|
||||||
|
}
|
||||||
|
|
||||||
if (claudeAccountId !== undefined) {
|
if (claudeAccountId !== undefined) {
|
||||||
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
// 空字符串表示解绑,null或空字符串都设置为空字符串
|
||||||
updates.claudeAccountId = claudeAccountId || '';
|
updates.claudeAccountId = claudeAccountId || '';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const claudeRelayService = require('../services/claudeRelayService');
|
|||||||
const apiKeyService = require('../services/apiKeyService');
|
const apiKeyService = require('../services/apiKeyService');
|
||||||
const { authenticateApiKey } = require('../middleware/auth');
|
const { authenticateApiKey } = require('../middleware/auth');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
const redis = require('../models/redis');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -66,6 +67,15 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => {
|
|||||||
logger.error('❌ Failed to record stream usage:', error);
|
logger.error('❌ Failed to record stream usage:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新时间窗口内的token计数
|
||||||
|
if (req.rateLimitInfo) {
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
usageDataCaptured = true;
|
usageDataCaptured = true;
|
||||||
logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
|
logger.api(`📊 Stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
|
||||||
} else {
|
} else {
|
||||||
@@ -122,6 +132,13 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => {
|
|||||||
// 记录真实的token使用量(包含模型信息和所有4种token)
|
// 记录真实的token使用量(包含模型信息和所有4种token)
|
||||||
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
|
await apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, cacheCreateTokens, cacheReadTokens, model);
|
||||||
|
|
||||||
|
// 更新时间窗口内的token计数
|
||||||
|
if (req.rateLimitInfo) {
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
await redis.getClient().incrby(req.rateLimitInfo.tokenCountKey, totalTokens);
|
||||||
|
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`);
|
||||||
|
}
|
||||||
|
|
||||||
usageRecorded = true;
|
usageRecorded = true;
|
||||||
logger.api(`📊 Non-stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
|
logger.api(`📊 Non-stream usage recorded (real) - Model: ${model}, Input: ${inputTokens}, Output: ${outputTokens}, Cache Create: ${cacheCreateTokens}, Cache Read: ${cacheReadTokens}, Total: ${inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens} tokens`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class ApiKeyService {
|
|||||||
claudeAccountId = null,
|
claudeAccountId = null,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
concurrencyLimit = 0,
|
concurrencyLimit = 0,
|
||||||
|
rateLimitWindow = null,
|
||||||
|
rateLimitRequests = null,
|
||||||
enableModelRestriction = false,
|
enableModelRestriction = false,
|
||||||
restrictedModels = []
|
restrictedModels = []
|
||||||
} = options;
|
} = options;
|
||||||
@@ -35,6 +37,8 @@ class ApiKeyService {
|
|||||||
apiKey: hashedKey,
|
apiKey: hashedKey,
|
||||||
tokenLimit: String(tokenLimit ?? 0),
|
tokenLimit: String(tokenLimit ?? 0),
|
||||||
concurrencyLimit: String(concurrencyLimit ?? 0),
|
concurrencyLimit: String(concurrencyLimit ?? 0),
|
||||||
|
rateLimitWindow: String(rateLimitWindow ?? 0),
|
||||||
|
rateLimitRequests: String(rateLimitRequests ?? 0),
|
||||||
isActive: String(isActive),
|
isActive: String(isActive),
|
||||||
claudeAccountId: claudeAccountId || '',
|
claudeAccountId: claudeAccountId || '',
|
||||||
enableModelRestriction: String(enableModelRestriction),
|
enableModelRestriction: String(enableModelRestriction),
|
||||||
@@ -57,6 +61,8 @@ class ApiKeyService {
|
|||||||
description: keyData.description,
|
description: keyData.description,
|
||||||
tokenLimit: parseInt(keyData.tokenLimit),
|
tokenLimit: parseInt(keyData.tokenLimit),
|
||||||
concurrencyLimit: parseInt(keyData.concurrencyLimit),
|
concurrencyLimit: parseInt(keyData.concurrencyLimit),
|
||||||
|
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
|
||||||
|
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
|
||||||
isActive: keyData.isActive === 'true',
|
isActive: keyData.isActive === 'true',
|
||||||
claudeAccountId: keyData.claudeAccountId,
|
claudeAccountId: keyData.claudeAccountId,
|
||||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||||
@@ -94,14 +100,8 @@ class ApiKeyService {
|
|||||||
return { valid: false, error: 'API key has expired' };
|
return { valid: false, error: 'API key has expired' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查使用限制
|
// 获取使用统计(供返回数据使用)
|
||||||
const usage = await redis.getUsageStats(keyData.id);
|
const usage = await redis.getUsageStats(keyData.id);
|
||||||
const tokenLimit = parseInt(keyData.tokenLimit);
|
|
||||||
|
|
||||||
if (tokenLimit > 0 && usage.total.tokens >= tokenLimit) {
|
|
||||||
return { valid: false, error: 'Token limit exceeded' };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
|
// 更新最后使用时间(优化:只在实际API调用时更新,而不是验证时)
|
||||||
// 注意:lastUsedAt的更新已移至recordUsage方法中
|
// 注意:lastUsedAt的更新已移至recordUsage方法中
|
||||||
@@ -124,6 +124,8 @@ class ApiKeyService {
|
|||||||
claudeAccountId: keyData.claudeAccountId,
|
claudeAccountId: keyData.claudeAccountId,
|
||||||
tokenLimit: parseInt(keyData.tokenLimit),
|
tokenLimit: parseInt(keyData.tokenLimit),
|
||||||
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
concurrencyLimit: parseInt(keyData.concurrencyLimit || 0),
|
||||||
|
rateLimitWindow: parseInt(keyData.rateLimitWindow || 0),
|
||||||
|
rateLimitRequests: parseInt(keyData.rateLimitRequests || 0),
|
||||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||||
restrictedModels: restrictedModels,
|
restrictedModels: restrictedModels,
|
||||||
usage
|
usage
|
||||||
@@ -145,6 +147,8 @@ class ApiKeyService {
|
|||||||
key.usage = await redis.getUsageStats(key.id);
|
key.usage = await redis.getUsageStats(key.id);
|
||||||
key.tokenLimit = parseInt(key.tokenLimit);
|
key.tokenLimit = parseInt(key.tokenLimit);
|
||||||
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
|
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
|
||||||
|
key.rateLimitWindow = parseInt(key.rateLimitWindow || 0);
|
||||||
|
key.rateLimitRequests = parseInt(key.rateLimitRequests || 0);
|
||||||
key.currentConcurrency = await redis.getConcurrency(key.id);
|
key.currentConcurrency = await redis.getConcurrency(key.id);
|
||||||
key.isActive = key.isActive === 'true';
|
key.isActive = key.isActive === 'true';
|
||||||
key.enableModelRestriction = key.enableModelRestriction === 'true';
|
key.enableModelRestriction = key.enableModelRestriction === 'true';
|
||||||
@@ -172,7 +176,7 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 允许更新的字段
|
// 允许更新的字段
|
||||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
|
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'rateLimitWindow', 'rateLimitRequests', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
|
||||||
const updatedData = { ...keyData };
|
const updatedData = { ...keyData };
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(updates)) {
|
for (const [field, value] of Object.entries(updates)) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 调试日志:查看API Key数据
|
// 调试日志:查看API Key数据
|
||||||
logger.info(`🔍 API Key data received:`, {
|
logger.info('🔍 API Key data received:', {
|
||||||
apiKeyName: apiKeyData.name,
|
apiKeyName: apiKeyData.name,
|
||||||
enableModelRestriction: apiKeyData.enableModelRestriction,
|
enableModelRestriction: apiKeyData.enableModelRestriction,
|
||||||
restrictedModels: apiKeyData.restrictedModels,
|
restrictedModels: apiKeyData.restrictedModels,
|
||||||
@@ -448,7 +448,7 @@ class ClaudeRelayService {
|
|||||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
||||||
try {
|
try {
|
||||||
// 调试日志:查看API Key数据(流式请求)
|
// 调试日志:查看API Key数据(流式请求)
|
||||||
logger.info(`🔍 [Stream] API Key data received:`, {
|
logger.info('🔍 [Stream] API Key data received:', {
|
||||||
apiKeyName: apiKeyData.name,
|
apiKeyName: apiKeyData.name,
|
||||||
enableModelRestriction: apiKeyData.enableModelRestriction,
|
enableModelRestriction: apiKeyData.enableModelRestriction,
|
||||||
restrictedModels: apiKeyData.restrictedModels,
|
restrictedModels: apiKeyData.restrictedModels,
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ const app = createApp({
|
|||||||
tokenLimit: '',
|
tokenLimit: '',
|
||||||
description: '',
|
description: '',
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
|
rateLimitWindow: '',
|
||||||
|
rateLimitRequests: '',
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
enableModelRestriction: false,
|
enableModelRestriction: false,
|
||||||
restrictedModels: [],
|
restrictedModels: [],
|
||||||
@@ -158,6 +160,8 @@ const app = createApp({
|
|||||||
name: '',
|
name: '',
|
||||||
tokenLimit: '',
|
tokenLimit: '',
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
|
rateLimitWindow: '',
|
||||||
|
rateLimitRequests: '',
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
enableModelRestriction: false,
|
enableModelRestriction: false,
|
||||||
restrictedModels: [],
|
restrictedModels: [],
|
||||||
@@ -1211,6 +1215,8 @@ const app = createApp({
|
|||||||
tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null,
|
tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.trim() ? parseInt(this.apiKeyForm.tokenLimit) : null,
|
||||||
description: this.apiKeyForm.description || '',
|
description: this.apiKeyForm.description || '',
|
||||||
concurrencyLimit: this.apiKeyForm.concurrencyLimit && this.apiKeyForm.concurrencyLimit.trim() ? parseInt(this.apiKeyForm.concurrencyLimit) : 0,
|
concurrencyLimit: this.apiKeyForm.concurrencyLimit && this.apiKeyForm.concurrencyLimit.trim() ? parseInt(this.apiKeyForm.concurrencyLimit) : 0,
|
||||||
|
rateLimitWindow: this.apiKeyForm.rateLimitWindow && this.apiKeyForm.rateLimitWindow.trim() ? parseInt(this.apiKeyForm.rateLimitWindow) : null,
|
||||||
|
rateLimitRequests: this.apiKeyForm.rateLimitRequests && this.apiKeyForm.rateLimitRequests.trim() ? parseInt(this.apiKeyForm.rateLimitRequests) : null,
|
||||||
claudeAccountId: this.apiKeyForm.claudeAccountId || null,
|
claudeAccountId: this.apiKeyForm.claudeAccountId || null,
|
||||||
enableModelRestriction: this.apiKeyForm.enableModelRestriction,
|
enableModelRestriction: this.apiKeyForm.enableModelRestriction,
|
||||||
restrictedModels: this.apiKeyForm.restrictedModels
|
restrictedModels: this.apiKeyForm.restrictedModels
|
||||||
@@ -1231,7 +1237,7 @@ const app = createApp({
|
|||||||
|
|
||||||
// 关闭创建弹窗并清理表单
|
// 关闭创建弹窗并清理表单
|
||||||
this.showCreateApiKeyModal = false;
|
this.showCreateApiKeyModal = false;
|
||||||
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', claudeAccountId: '', enableModelRestriction: false, restrictedModels: [], modelInput: '' };
|
this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', enableModelRestriction: false, restrictedModels: [], modelInput: '' };
|
||||||
|
|
||||||
// 重新加载API Keys列表
|
// 重新加载API Keys列表
|
||||||
await this.loadApiKeys();
|
await this.loadApiKeys();
|
||||||
@@ -1275,6 +1281,8 @@ const app = createApp({
|
|||||||
name: key.name,
|
name: key.name,
|
||||||
tokenLimit: key.tokenLimit || '',
|
tokenLimit: key.tokenLimit || '',
|
||||||
concurrencyLimit: key.concurrencyLimit || '',
|
concurrencyLimit: key.concurrencyLimit || '',
|
||||||
|
rateLimitWindow: key.rateLimitWindow || '',
|
||||||
|
rateLimitRequests: key.rateLimitRequests || '',
|
||||||
claudeAccountId: key.claudeAccountId || '',
|
claudeAccountId: key.claudeAccountId || '',
|
||||||
enableModelRestriction: key.enableModelRestriction || false,
|
enableModelRestriction: key.enableModelRestriction || false,
|
||||||
restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [],
|
restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [],
|
||||||
@@ -1290,6 +1298,8 @@ const app = createApp({
|
|||||||
name: '',
|
name: '',
|
||||||
tokenLimit: '',
|
tokenLimit: '',
|
||||||
concurrencyLimit: '',
|
concurrencyLimit: '',
|
||||||
|
rateLimitWindow: '',
|
||||||
|
rateLimitRequests: '',
|
||||||
claudeAccountId: '',
|
claudeAccountId: '',
|
||||||
enableModelRestriction: false,
|
enableModelRestriction: false,
|
||||||
restrictedModels: [],
|
restrictedModels: [],
|
||||||
@@ -1309,6 +1319,8 @@ const app = createApp({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0,
|
tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0,
|
||||||
concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0,
|
concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0,
|
||||||
|
rateLimitWindow: this.editApiKeyForm.rateLimitWindow && this.editApiKeyForm.rateLimitWindow.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitWindow) : 0,
|
||||||
|
rateLimitRequests: this.editApiKeyForm.rateLimitRequests && this.editApiKeyForm.rateLimitRequests.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitRequests) : 0,
|
||||||
claudeAccountId: this.editApiKeyForm.claudeAccountId || null,
|
claudeAccountId: this.editApiKeyForm.claudeAccountId || null,
|
||||||
enableModelRestriction: this.editApiKeyForm.enableModelRestriction,
|
enableModelRestriction: this.editApiKeyForm.enableModelRestriction,
|
||||||
restrictedModels: this.editApiKeyForm.restrictedModels
|
restrictedModels: this.editApiKeyForm.restrictedModels
|
||||||
|
|||||||
@@ -534,6 +534,16 @@
|
|||||||
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
|
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 时间窗口限流 -->
|
||||||
|
<div v-if="key.rateLimitWindow > 0" class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600">时间窗口:</span>
|
||||||
|
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
|
||||||
|
</div>
|
||||||
|
<!-- 请求次数限制 -->
|
||||||
|
<div v-if="key.rateLimitRequests > 0" class="flex justify-between text-sm">
|
||||||
|
<span class="text-gray-600">请求限制:</span>
|
||||||
|
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
|
||||||
|
</div>
|
||||||
<!-- 输入/输出Token -->
|
<!-- 输入/输出Token -->
|
||||||
<div class="flex justify-between text-xs text-gray-500">
|
<div class="flex justify-between text-xs text-gray-500">
|
||||||
<span>输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
|
<span>输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
|
||||||
@@ -1792,14 +1802,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Token 限制 (可选)</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口 (可选)</label>
|
||||||
|
<input
|
||||||
|
v-model="apiKeyForm.rateLimitWindow"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="留空表示无限制"
|
||||||
|
class="form-input w-full"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">设置时间窗口(分钟),在此时间内限制请求次数或Token使用量</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-3">请求次数限制 (可选)</label>
|
||||||
|
<input
|
||||||
|
v-model="apiKeyForm.rateLimitRequests"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="留空表示无限制"
|
||||||
|
class="form-input w-full"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内 Token 限制 (可选)</label>
|
||||||
<input
|
<input
|
||||||
v-model="apiKeyForm.tokenLimit"
|
v-model="apiKeyForm.tokenLimit"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="留空表示无限制"
|
placeholder="留空表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 的最大 token 使用量</p>
|
<p class="text-xs text-gray-500 mt-2">设置在时间窗口内的最大 Token 使用量(需同时设置时间窗口)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -1951,7 +1985,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Token 限制</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口</label>
|
||||||
|
<input
|
||||||
|
v-model="editApiKeyForm.rateLimitWindow"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="留空表示无限制"
|
||||||
|
class="form-input w-full"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">设置时间窗口(分钟),在此时间内限制请求次数或Token使用量</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-3">请求次数限制</label>
|
||||||
|
<input
|
||||||
|
v-model="editApiKeyForm.rateLimitRequests"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="留空表示无限制"
|
||||||
|
class="form-input w-full"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">在时间窗口内允许的最大请求次数</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-3">时间窗口内 Token 限制</label>
|
||||||
<input
|
<input
|
||||||
v-model="editApiKeyForm.tokenLimit"
|
v-model="editApiKeyForm.tokenLimit"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -1959,7 +2017,7 @@
|
|||||||
placeholder="0 表示无限制"
|
placeholder="0 表示无限制"
|
||||||
class="form-input w-full"
|
class="form-input w-full"
|
||||||
>
|
>
|
||||||
<p class="text-xs text-gray-500 mt-2">设置此 API Key 的最大 token 使用量,0 或留空表示无限制</p>
|
<p class="text-xs text-gray-500 mt-2">设置在时间窗口内的最大 Token 使用量(需同时设置时间窗口),0 或留空表示无限制</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user