mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 21:17:30 +00:00
feat: 为API Key添加模型限制功能
- 前端:在API Key创建和编辑表单中添加模型限制开关和标签输入 - 前端:支持动态添加/删除限制的模型列表 - 后端:更新API Key数据结构,新增enableModelRestriction和restrictedModels字段 - 后端:在中转请求时检查模型访问权限 - 修复:Enter键提交表单问题,使用@keydown.enter.prevent - 优化:限制模型数据持久化,关闭开关时不清空数据 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,9 @@ class ApiKeyService {
|
||||
expiresAt = null,
|
||||
claudeAccountId = null,
|
||||
isActive = true,
|
||||
concurrencyLimit = 0
|
||||
concurrencyLimit = 0,
|
||||
enableModelRestriction = false,
|
||||
restrictedModels = []
|
||||
} = options;
|
||||
|
||||
// 生成简单的API Key (64字符十六进制)
|
||||
@@ -35,6 +37,8 @@ class ApiKeyService {
|
||||
concurrencyLimit: String(concurrencyLimit ?? 0),
|
||||
isActive: String(isActive),
|
||||
claudeAccountId: claudeAccountId || '',
|
||||
enableModelRestriction: String(enableModelRestriction),
|
||||
restrictedModels: JSON.stringify(restrictedModels || []),
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: '',
|
||||
expiresAt: expiresAt || '',
|
||||
@@ -55,6 +59,8 @@ class ApiKeyService {
|
||||
concurrencyLimit: parseInt(keyData.concurrencyLimit),
|
||||
isActive: keyData.isActive === 'true',
|
||||
claudeAccountId: keyData.claudeAccountId,
|
||||
enableModelRestriction: keyData.enableModelRestriction === 'true',
|
||||
restrictedModels: JSON.parse(keyData.restrictedModels),
|
||||
createdAt: keyData.createdAt,
|
||||
expiresAt: keyData.expiresAt,
|
||||
createdBy: keyData.createdBy
|
||||
@@ -131,6 +137,12 @@ class ApiKeyService {
|
||||
key.concurrencyLimit = parseInt(key.concurrencyLimit || 0);
|
||||
key.currentConcurrency = await redis.getConcurrency(key.id);
|
||||
key.isActive = key.isActive === 'true';
|
||||
key.enableModelRestriction = key.enableModelRestriction === 'true';
|
||||
try {
|
||||
key.restrictedModels = key.restrictedModels ? JSON.parse(key.restrictedModels) : [];
|
||||
} catch (e) {
|
||||
key.restrictedModels = [];
|
||||
}
|
||||
delete key.apiKey; // 不返回哈希后的key
|
||||
}
|
||||
|
||||
@@ -150,12 +162,20 @@ class ApiKeyService {
|
||||
}
|
||||
|
||||
// 允许更新的字段
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt'];
|
||||
const allowedUpdates = ['name', 'description', 'tokenLimit', 'concurrencyLimit', 'isActive', 'claudeAccountId', 'expiresAt', 'enableModelRestriction', 'restrictedModels'];
|
||||
const updatedData = { ...keyData };
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
if (allowedUpdates.includes(field)) {
|
||||
updatedData[field] = (value != null ? value : '').toString();
|
||||
if (field === 'restrictedModels') {
|
||||
// 特殊处理 restrictedModels 数组
|
||||
updatedData[field] = JSON.stringify(value || []);
|
||||
} else if (field === 'enableModelRestriction') {
|
||||
// 布尔值转字符串
|
||||
updatedData[field] = String(value);
|
||||
} else {
|
||||
updatedData[field] = (value != null ? value : '').toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,24 @@ class ClaudeRelayService {
|
||||
let upstreamRequest = null;
|
||||
|
||||
try {
|
||||
// 检查模型限制
|
||||
if (apiKeyData.enableModelRestriction && apiKeyData.restrictedModels && apiKeyData.restrictedModels.length > 0) {
|
||||
const requestedModel = requestBody.model;
|
||||
if (requestedModel && apiKeyData.restrictedModels.includes(requestedModel)) {
|
||||
logger.warn(`🚫 Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${requestedModel}`);
|
||||
return {
|
||||
statusCode: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
message: '暂无该模型访问权限'
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
@@ -419,6 +437,26 @@ class ClaudeRelayService {
|
||||
// 🌊 处理流式响应(带usage数据捕获)
|
||||
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback) {
|
||||
try {
|
||||
// 检查模型限制
|
||||
if (apiKeyData.enableModelRestriction && apiKeyData.restrictedModels && apiKeyData.restrictedModels.length > 0) {
|
||||
const requestedModel = requestBody.model;
|
||||
if (requestedModel && apiKeyData.restrictedModels.includes(requestedModel)) {
|
||||
logger.warn(`🚫 Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${requestedModel}`);
|
||||
|
||||
// 对于流式响应,需要写入错误并结束流
|
||||
const errorResponse = JSON.stringify({
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
message: '暂无该模型访问权限'
|
||||
}
|
||||
});
|
||||
|
||||
responseStream.writeHead(403, { 'Content-Type': 'application/json' });
|
||||
responseStream.end(errorResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(requestBody);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user