From 657b7b0a05020116b9dcdf453e90d6836c8b7f29 Mon Sep 17 00:00:00 2001 From: andersonby Date: Wed, 6 Aug 2025 19:23:36 +0800 Subject: [PATCH] feat: Add test scripts for Bedrock models and model mapping functionality --- scripts/test-bedrock-models.js | 35 +++++ scripts/test-model-mapping.js | 47 ++++++ src/routes/api.js | 2 +- src/services/bedrockAccountService.js | 68 ++++---- src/services/bedrockRelayService.js | 147 +++++++++++++----- .../src/components/accounts/AccountForm.vue | 30 ++-- 6 files changed, 253 insertions(+), 76 deletions(-) create mode 100644 scripts/test-bedrock-models.js create mode 100644 scripts/test-model-mapping.js diff --git a/scripts/test-bedrock-models.js b/scripts/test-bedrock-models.js new file mode 100644 index 00000000..3d911746 --- /dev/null +++ b/scripts/test-bedrock-models.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node + +const bedrockAccountService = require('../src/services/bedrockAccountService'); +const bedrockRelayService = require('../src/services/bedrockRelayService'); +const logger = require('../src/utils/logger'); + +async function testBedrockModels() { + try { + console.log('🧪 测试Bedrock模型配置...'); + + // 测试可用模型列表 + const models = await bedrockRelayService.getAvailableModels(); + console.log(`📋 找到 ${models.length} 个可用模型:`); + models.forEach(model => { + console.log(` - ${model.id} (${model.name})`); + }); + + // 测试默认模型 + console.log(`\n🎯 系统默认模型: ${bedrockRelayService.defaultModel}`); + console.log(`🎯 系统默认小模型: ${bedrockRelayService.defaultSmallModel}`); + + console.log('\n✅ Bedrock模型配置测试完成'); + process.exit(0); + } catch (error) { + console.error('❌ Bedrock模型测试失败:', error); + process.exit(1); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + testBedrockModels(); +} + +module.exports = { testBedrockModels }; \ No newline at end of file diff --git a/scripts/test-model-mapping.js b/scripts/test-model-mapping.js new file mode 100644 index 00000000..f6e33905 --- /dev/null +++ b/scripts/test-model-mapping.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +const bedrockRelayService = require('../src/services/bedrockRelayService'); + +function testModelMapping() { + console.log('🧪 测试模型映射功能...'); + + // 测试用例 + const testCases = [ + // 标准Claude模型名 + 'claude-3-5-haiku-20241022', + 'claude-3-5-sonnet-20241022', + 'claude-3-5-sonnet', + 'claude-3-5-haiku', + 'claude-sonnet-4', + 'claude-opus-4-1', + 'claude-3-7-sonnet', + + // 已经是Bedrock格式的 + 'us.anthropic.claude-sonnet-4-20250514-v1:0', + 'anthropic.claude-3-5-haiku-20241022-v1:0', + + // 未知模型 + 'unknown-model' + ]; + + console.log('\n📋 模型映射测试结果:'); + testCases.forEach(testModel => { + const mappedModel = bedrockRelayService._mapToBedrockModel(testModel); + const isChanged = mappedModel !== testModel; + const status = isChanged ? '🔄' : '✅'; + + console.log(`${status} ${testModel}`); + if (isChanged) { + console.log(` → ${mappedModel}`); + } + }); + + console.log('\n✅ 模型映射测试完成'); +} + +// 如果直接运行此脚本 +if (require.main === module) { + testModelMapping(); +} + +module.exports = { testModelMapping }; \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index 95a6d380..b300717e 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -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) { diff --git a/src/services/bedrockAccountService.js b/src/services/bedrockAccountService.js index cac4b36d..587bd637 100644 --- a/src/services/bedrockAccountService.js +++ b/src/services/bedrockAccountService.js @@ -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); diff --git a/src/services/bedrockRelayService.js b/src/services/bedrockRelayService.js index 058d7931..99251423 100644 --- a/src/services/bedrockRelayService.js +++ b/src/services/bedrockRelayService.js @@ -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', diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index d1decea5..fee143f8 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -1377,11 +1377,13 @@ const createAccount = async () => { data.userAgent = form.value.userAgent || null data.rateLimitDuration = form.value.rateLimitDuration || 60 } else if (form.value.platform === 'bedrock') { - // Bedrock 账户特定数据 - data.accessKeyId = form.value.accessKeyId - data.secretAccessKey = form.value.secretAccessKey + // Bedrock 账户特定数据 - 构造 awsCredentials 对象 + data.awsCredentials = { + accessKeyId: form.value.accessKeyId, + secretAccessKey: form.value.secretAccessKey, + sessionToken: form.value.sessionToken || null + } data.region = form.value.region - data.sessionToken = form.value.sessionToken || null data.defaultModel = form.value.defaultModel || null data.smallFastModel = form.value.smallFastModel || null data.priority = form.value.priority || 50 @@ -1511,18 +1513,22 @@ const updateAccount = async () => { // Bedrock 特定更新 if (props.account.platform === 'bedrock') { - if (form.value.accessKeyId) { - data.accessKeyId = form.value.accessKeyId - } - if (form.value.secretAccessKey) { - data.secretAccessKey = form.value.secretAccessKey + // 只有当有凭证变更时才构造 awsCredentials 对象 + if (form.value.accessKeyId || form.value.secretAccessKey || form.value.sessionToken) { + data.awsCredentials = {} + if (form.value.accessKeyId) { + data.awsCredentials.accessKeyId = form.value.accessKeyId + } + if (form.value.secretAccessKey) { + data.awsCredentials.secretAccessKey = form.value.secretAccessKey + } + if (form.value.sessionToken !== undefined) { + data.awsCredentials.sessionToken = form.value.sessionToken || null + } } if (form.value.region) { data.region = form.value.region } - if (form.value.sessionToken) { - data.sessionToken = form.value.sessionToken - } // 模型配置(支持设置为空来使用系统默认) data.defaultModel = form.value.defaultModel || null data.smallFastModel = form.value.smallFastModel || null