feat: Add test scripts for Bedrock models and model mapping functionality

This commit is contained in:
andersonby
2025-08-06 19:23:36 +08:00
parent 9a9a82c86f
commit 657b7b0a05
6 changed files with 253 additions and 76 deletions

View File

@@ -145,7 +145,7 @@ async function handleMessagesRequest(req, res) {
throw new Error('Failed to get Bedrock account details');
}
const result = await bedrockRelayService.handleStreamRequest(req.body, bedrockAccountResult.data, req.headers, res);
const result = await bedrockRelayService.handleStreamRequest(req.body, bedrockAccountResult.data, res);
// 记录Bedrock使用统计
if (result.usage) {

View File

@@ -19,7 +19,7 @@ class BedrockAccountService {
description = '',
region = process.env.AWS_REGION || 'us-east-1',
awsCredentials = null, // { accessKeyId, secretAccessKey, sessionToken }
defaultModel = 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
defaultModel = 'us.anthropic.claude-sonnet-4-20250514-v1:0',
isActive = true,
accountType = 'shared', // 'dedicated' or 'shared'
priority = 50, // 调度优先级 (1-100数字越小优先级越高)
@@ -28,7 +28,7 @@ class BedrockAccountService {
} = options;
const accountId = uuidv4();
let accountData = {
id: accountId,
name,
@@ -52,9 +52,9 @@ class BedrockAccountService {
const client = redis.getClientSafe();
await client.set(`bedrock_account:${accountId}`, JSON.stringify(accountData));
logger.info(`✅ 创建Bedrock账户成功 - ID: ${accountId}, 名称: ${name}, 区域: ${region}`);
return {
success: true,
data: {
@@ -84,14 +84,14 @@ class BedrockAccountService {
}
const account = JSON.parse(accountData);
// 解密AWS凭证用于内部使用
if (account.awsCredentials) {
account.awsCredentials = this._decryptAwsCredentials(account.awsCredentials);
}
logger.debug(`🔍 获取Bedrock账户 - ID: ${accountId}, 名称: ${account.name}`);
return {
success: true,
data: account
@@ -113,7 +113,7 @@ class BedrockAccountService {
const accountData = await client.get(key);
if (accountData) {
const account = JSON.parse(accountData);
// 返回给前端时,不包含敏感信息,只显示掩码
accounts.push({
id: account.id,
@@ -141,7 +141,7 @@ class BedrockAccountService {
});
logger.debug(`📋 获取所有Bedrock账户 - 共 ${accounts.length}`);
return {
success: true,
data: accounts
@@ -161,7 +161,7 @@ class BedrockAccountService {
}
const account = accountResult.data;
// 更新字段
if (updates.name !== undefined) account.name = updates.name;
if (updates.description !== undefined) account.description = updates.description;
@@ -186,9 +186,9 @@ class BedrockAccountService {
const client = redis.getClientSafe();
await client.set(`bedrock_account:${accountId}`, JSON.stringify(account));
logger.info(`✅ 更新Bedrock账户成功 - ID: ${accountId}, 名称: ${account.name}`);
return {
success: true,
data: {
@@ -222,9 +222,9 @@ class BedrockAccountService {
const client = redis.getClientSafe();
await client.del(`bedrock_account:${accountId}`);
logger.info(`✅ 删除Bedrock账户成功 - ID: ${accountId}`);
return { success: true };
} catch (error) {
logger.error(`❌ 删除Bedrock账户失败 - ID: ${accountId}`, error);
@@ -240,7 +240,7 @@ class BedrockAccountService {
return { success: false, error: 'Failed to get accounts' };
}
const availableAccounts = accountsResult.data.filter(account =>
const availableAccounts = accountsResult.data.filter(account =>
account.isActive && account.schedulable
);
@@ -250,7 +250,7 @@ class BedrockAccountService {
// 简单的轮询选择策略 - 选择优先级最高的账户
const selectedAccount = availableAccounts[0];
// 获取完整账户信息(包含解密的凭证)
const fullAccountResult = await this.getAccount(selectedAccount.id);
if (!fullAccountResult.success) {
@@ -258,7 +258,7 @@ class BedrockAccountService {
}
logger.debug(`🎯 选择Bedrock账户 - ID: ${selectedAccount.id}, 名称: ${selectedAccount.name}`);
return {
success: true,
data: fullAccountResult.data
@@ -278,12 +278,12 @@ class BedrockAccountService {
}
const account = accountResult.data;
logger.info(`🧪 测试Bedrock账户连接 - ID: ${accountId}, 名称: ${account.name}`);
// 尝试获取模型列表来测试连接
const models = await bedrockRelayService.getAvailableModels(account);
if (models && models.length > 0) {
logger.info(`✅ Bedrock账户测试成功 - ID: ${accountId}, 发现 ${models.length} 个模型`);
return {
@@ -313,14 +313,14 @@ class BedrockAccountService {
// 🔐 加密AWS凭证
_encryptAwsCredentials(credentials) {
try {
const key = Buffer.from(config.security.encryptionKey, 'utf8');
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.ENCRYPTION_ALGORITHM, key);
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
const credentialsString = JSON.stringify(credentials);
let encrypted = cipher.update(credentialsString, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted: encrypted,
iv: iv.toString('hex')
@@ -334,12 +334,28 @@ class BedrockAccountService {
// 🔓 解密AWS凭证
_decryptAwsCredentials(encryptedData) {
try {
const key = Buffer.from(config.security.encryptionKey, 'utf8');
const decipher = crypto.createDecipher(this.ENCRYPTION_ALGORITHM, key);
// 检查数据格式
if (!encryptedData || typeof encryptedData !== 'object') {
logger.error('❌ 无效的加密数据格式:', encryptedData);
throw new Error('Invalid encrypted data format');
}
// 检查必要字段
if (!encryptedData.encrypted || !encryptedData.iv) {
logger.error('❌ 缺少加密数据字段:', {
hasEncrypted: !!encryptedData.encrypted,
hasIv: !!encryptedData.iv
});
throw new Error('Missing encrypted data fields');
}
const key = crypto.createHash('sha256').update(config.security.encryptionKey).digest();
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
} catch (error) {
logger.error('❌ AWS凭证解密失败', error);

View File

@@ -7,16 +7,16 @@ class BedrockRelayService {
constructor() {
this.defaultRegion = process.env.AWS_REGION || config.bedrock?.defaultRegion || 'us-east-1';
this.smallFastModelRegion = process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION || this.defaultRegion;
// 默认模型配置
this.defaultModel = process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-3-7-sonnet-20250219-v1:0';
this.defaultModel = process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0';
this.defaultSmallModel = process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0';
// Token配置
this.maxOutputTokens = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096;
this.maxThinkingTokens = parseInt(process.env.MAX_THINKING_TOKENS) || 1024;
this.enablePromptCaching = process.env.DISABLE_PROMPT_CACHING !== '1';
// 创建Bedrock客户端
this.clients = new Map(); // 缓存不同区域的客户端
}
@@ -25,7 +25,7 @@ class BedrockRelayService {
_getBedrockClient(region = null, bedrockAccount = null) {
const targetRegion = region || this.defaultRegion;
const clientKey = `${targetRegion}-${bedrockAccount?.id || 'default'}`;
if (this.clients.has(clientKey)) {
return this.clients.get(clientKey);
}
@@ -42,13 +42,17 @@ class BedrockRelayService {
sessionToken: bedrockAccount.awsCredentials.sessionToken
};
} else {
// 使用默认凭证链:环境变量 -> AWS配置文件 -> IAM角色
clientConfig.credentials = fromEnv();
// 检查是否有环境变量凭证
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
clientConfig.credentials = fromEnv();
} else {
throw new Error('AWS凭证未配置。请在Bedrock账户中配置AWS访问密钥或设置环境变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY');
}
}
const client = new BedrockRuntimeClient(clientConfig);
this.clients.set(clientKey, client);
logger.debug(`🔧 Created Bedrock client for region: ${targetRegion}, account: ${bedrockAccount?.name || 'default'}`);
return client;
}
@@ -62,7 +66,7 @@ class BedrockRelayService {
// 转换请求格式为Bedrock格式
const bedrockPayload = this._convertToBedrockFormat(requestBody);
const command = new InvokeModelCommand({
modelId: modelId,
body: JSON.stringify(bedrockPayload),
@@ -71,7 +75,7 @@ class BedrockRelayService {
});
logger.debug(`🚀 Bedrock非流式请求 - 模型: ${modelId}, 区域: ${region}`);
const startTime = Date.now();
const response = await client.send(command);
const duration = Date.now() - startTime;
@@ -81,7 +85,7 @@ class BedrockRelayService {
const claudeResponse = this._convertFromBedrockFormat(responseBody);
logger.info(`✅ Bedrock请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
return {
success: true,
data: claudeResponse,
@@ -105,7 +109,7 @@ class BedrockRelayService {
// 转换请求格式为Bedrock格式
const bedrockPayload = this._convertToBedrockFormat(requestBody);
const command = new InvokeModelWithResponseStreamCommand({
modelId: modelId,
body: JSON.stringify(bedrockPayload),
@@ -114,7 +118,7 @@ class BedrockRelayService {
});
logger.debug(`🌊 Bedrock流式请求 - 模型: ${modelId}, 区域: ${region}`);
const startTime = Date.now();
const response = await client.send(command);
@@ -135,17 +139,17 @@ class BedrockRelayService {
if (chunk.chunk) {
const chunkData = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes));
const claudeEvent = this._convertBedrockStreamToClaudeFormat(chunkData, isFirstChunk);
if (claudeEvent) {
// 发送SSE事件
res.write(`event: ${claudeEvent.type}\n`);
res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`);
// 提取使用统计
if (claudeEvent.type === 'message_stop' && claudeEvent.data.usage) {
totalUsage = claudeEvent.data.usage;
}
isFirstChunk = false;
}
}
@@ -168,34 +172,97 @@ class BedrockRelayService {
} catch (error) {
logger.error('❌ Bedrock流式请求失败:', error);
// 发送错误事件
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
}
res.write('event: error\n');
res.write(`data: ${JSON.stringify({ error: this._handleBedrockError(error).message })}\n\n`);
res.end();
throw this._handleBedrockError(error);
}
}
// 选择使用的模型
_selectModel(requestBody, bedrockAccount) {
let selectedModel;
// 优先使用账户配置的模型
if (bedrockAccount?.defaultModel) {
return bedrockAccount.defaultModel;
selectedModel = bedrockAccount.defaultModel;
logger.info(`🎯 使用账户配置的模型: ${selectedModel}`, { metadata: { source: 'account', accountId: bedrockAccount.id } });
}
// 检查请求中指定的模型
if (requestBody.model) {
return requestBody.model;
else if (requestBody.model) {
selectedModel = requestBody.model;
logger.info(`🎯 使用请求指定的模型: ${selectedModel}`, { metadata: { source: 'request' } });
}
// 使用默认模型
return this.defaultModel;
else {
selectedModel = this.defaultModel;
logger.info(`🎯 使用系统默认模型: ${selectedModel}`, { metadata: { source: 'default' } });
}
// 如果是标准Claude模型名需要映射为Bedrock格式
const bedrockModel = this._mapToBedrockModel(selectedModel);
if (bedrockModel !== selectedModel) {
logger.info(`🔄 模型映射: ${selectedModel}${bedrockModel}`, { metadata: { originalModel: selectedModel, bedrockModel } });
}
return bedrockModel;
}
// 将标准Claude模型名映射为Bedrock格式
_mapToBedrockModel(modelName) {
// 标准Claude模型名到Bedrock模型名的映射表
const modelMapping = {
// Claude Sonnet 4
'claude-sonnet-4': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
'claude-sonnet-4-20250514': 'us.anthropic.claude-sonnet-4-20250514-v1:0',
// Claude Opus 4.1
'claude-opus-4': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
'claude-opus-4-1': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
'claude-opus-4-1-20250805': 'us.anthropic.claude-opus-4-1-20250805-v1:0',
// Claude 3.7 Sonnet
'claude-3-7-sonnet': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
'claude-3-7-sonnet-20250219': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
// Claude 3.5 Sonnet v2
'claude-3-5-sonnet': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
'claude-3-5-sonnet-20241022': 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
// Claude 3.5 Haiku
'claude-3-5-haiku': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
'claude-3-5-haiku-20241022': 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
// Claude 3 Sonnet
'claude-3-sonnet': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
'claude-3-sonnet-20240229': 'us.anthropic.claude-3-sonnet-20240229-v1:0',
// Claude 3 Haiku
'claude-3-haiku': 'us.anthropic.claude-3-haiku-20240307-v1:0',
'claude-3-haiku-20240307': 'us.anthropic.claude-3-haiku-20240307-v1:0'
};
// 如果已经是Bedrock格式直接返回
if (modelName.startsWith('us.anthropic.') || modelName.startsWith('anthropic.')) {
return modelName;
}
// 查找映射
const mappedModel = modelMapping[modelName];
if (mappedModel) {
return mappedModel;
}
// 如果没有找到映射,返回原始模型名(可能会导致错误,但保持向后兼容)
logger.warn(`⚠️ 未找到模型映射: ${modelName},使用原始模型名`, { metadata: { originalModel: modelName } });
return modelName;
}
// 选择使用的区域
@@ -204,12 +271,12 @@ class BedrockRelayService {
if (bedrockAccount?.region) {
return bedrockAccount.region;
}
// 对于小模型,使用专门的区域配置
if (modelId.includes('haiku')) {
return this.smallFastModelRegion;
}
return this.defaultRegion;
}
@@ -230,7 +297,7 @@ class BedrockRelayService {
if (requestBody.temperature !== undefined) {
bedrockPayload.temperature = requestBody.temperature;
}
if (requestBody.top_p !== undefined) {
bedrockPayload.top_p = requestBody.top_p;
}
@@ -289,7 +356,7 @@ class BedrockRelayService {
}
};
}
if (bedrockChunk.type === 'content_block_delta') {
return {
type: 'content_block_delta',
@@ -299,7 +366,7 @@ class BedrockRelayService {
}
};
}
if (bedrockChunk.type === 'message_delta') {
return {
type: 'message_delta',
@@ -309,7 +376,7 @@ class BedrockRelayService {
}
};
}
if (bedrockChunk.type === 'message_stop') {
return {
type: 'message_stop',
@@ -325,23 +392,23 @@ class BedrockRelayService {
// 处理Bedrock错误
_handleBedrockError(error) {
const errorMessage = error.message || 'Unknown Bedrock error';
if (error.name === 'ValidationException') {
return new Error(`Bedrock参数验证失败: ${errorMessage}`);
}
if (error.name === 'ThrottlingException') {
return new Error('Bedrock请求限流请稍后重试');
}
if (error.name === 'AccessDeniedException') {
return new Error('Bedrock访问被拒绝请检查IAM权限');
}
if (error.name === 'ModelNotReadyException') {
return new Error('Bedrock模型未就绪请稍后重试');
}
return new Error(`Bedrock服务错误: ${errorMessage}`);
}
@@ -349,9 +416,15 @@ class BedrockRelayService {
async getAvailableModels(bedrockAccount = null) {
try {
const region = bedrockAccount?.region || this.defaultRegion;
// Bedrock暂不支持列出推理配置文件的API返回预定义的模型列表
const models = [
{
id: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
name: 'Claude Sonnet 4',
provider: 'anthropic',
type: 'bedrock'
},
{
id: 'us.anthropic.claude-opus-4-1-20250805-v1:0',
name: 'Claude Opus 4.1',