feat: Add comprehensive Amazon Bedrock integration support

Add complete Amazon Bedrock integration to Claude Relay Service with:

## Core Features
-  Bedrock account management with encrypted AWS credential storage
-  Full request routing to AWS Bedrock with streaming support
-  Integration with unified Claude scheduler system
-  Support for Inference Profiles and Application Inference Profiles
-  Configurable default and small-fast model settings

## Backend Services
- Add bedrockAccountService.js for account management
- Add bedrockRelayService.js for request forwarding
- Integrate Bedrock accounts into unifiedClaudeScheduler.js
- Update admin and API routes to support Bedrock endpoints
- Add comprehensive configuration options to config.example.js

## Frontend Integration
- Complete Vue.js Web UI for Bedrock account management
- Account creation form with AWS credentials and model configuration
- Real-time account status monitoring and statistics
- Edit/update capabilities for existing accounts

## CLI Support
- Interactive CLI commands for Bedrock account operations
- Account creation, listing, updating, and testing
- Status monitoring and connection validation

## Security & Performance
- AES encrypted storage of AWS credentials in Redis
- Support for temporary credentials (session tokens)
- Region-specific configuration support
- Rate limiting and error handling

This integration enables the relay service to support three AI platforms:
1. Claude (OAuth) - Original Claude.ai integration
2. Gemini - Google AI integration
3. Amazon Bedrock - New AWS Bedrock integration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andersonby
2025-08-06 17:41:16 +08:00
parent d6ba97381d
commit 9a9a82c86f
14 changed files with 3493 additions and 23 deletions

View File

@@ -2,6 +2,7 @@ const express = require('express');
const apiKeyService = require('../services/apiKeyService');
const claudeAccountService = require('../services/claudeAccountService');
const claudeConsoleAccountService = require('../services/claudeConsoleAccountService');
const bedrockAccountService = require('../services/bedrockAccountService');
const geminiAccountService = require('../services/geminiAccountService');
const accountGroupService = require('../services/accountGroupService');
const redis = require('../models/redis');
@@ -1361,6 +1362,227 @@ router.put('/claude-console-accounts/:accountId/toggle-schedulable', authenticat
}
});
// ☁️ Bedrock 账户管理
// 获取所有Bedrock账户
router.get('/bedrock-accounts', authenticateAdmin, async (req, res) => {
try {
const result = await bedrockAccountService.getAllAccounts();
if (!result.success) {
return res.status(500).json({ error: 'Failed to get Bedrock accounts', message: result.error });
}
// 为每个账户添加使用统计信息
const accountsWithStats = await Promise.all(result.data.map(async (account) => {
try {
const usageStats = await redis.getAccountUsageStats(account.id);
return {
...account,
usage: {
daily: usageStats.daily,
total: usageStats.total,
averages: usageStats.averages
}
};
} catch (statsError) {
logger.warn(`⚠️ Failed to get usage stats for Bedrock account ${account.id}:`, statsError.message);
return {
...account,
usage: {
daily: { tokens: 0, requests: 0, allTokens: 0 },
total: { tokens: 0, requests: 0, allTokens: 0 },
averages: { rpm: 0, tpm: 0 }
}
};
}
}));
res.json({ success: true, data: accountsWithStats });
} catch (error) {
logger.error('❌ Failed to get Bedrock accounts:', error);
res.status(500).json({ error: 'Failed to get Bedrock accounts', message: error.message });
}
});
// 创建新的Bedrock账户
router.post('/bedrock-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
region,
awsCredentials,
defaultModel,
priority,
accountType,
credentialType
} = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
// 验证priority的有效性1-100
if (priority !== undefined && (priority < 1 || priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
}
// 验证accountType的有效性
if (accountType && !['shared', 'dedicated'].includes(accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
}
// 验证credentialType的有效性
if (credentialType && !['default', 'access_key', 'bearer_token'].includes(credentialType)) {
return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' });
}
const result = await bedrockAccountService.createAccount({
name,
description: description || '',
region: region || 'us-east-1',
awsCredentials,
defaultModel,
priority: priority || 50,
accountType: accountType || 'shared',
credentialType: credentialType || 'default'
});
if (!result.success) {
return res.status(500).json({ error: 'Failed to create Bedrock account', message: result.error });
}
logger.success(`☁️ Admin created Bedrock account: ${name}`);
res.json({ success: true, data: result.data });
} catch (error) {
logger.error('❌ Failed to create Bedrock account:', error);
res.status(500).json({ error: 'Failed to create Bedrock account', message: error.message });
}
});
// 更新Bedrock账户
router.put('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const updates = req.body;
// 验证priority的有效性1-100
if (updates.priority !== undefined && (updates.priority < 1 || updates.priority > 100)) {
return res.status(400).json({ error: 'Priority must be between 1 and 100' });
}
// 验证accountType的有效性
if (updates.accountType && !['shared', 'dedicated'].includes(updates.accountType)) {
return res.status(400).json({ error: 'Invalid account type. Must be "shared" or "dedicated"' });
}
// 验证credentialType的有效性
if (updates.credentialType && !['default', 'access_key', 'bearer_token'].includes(updates.credentialType)) {
return res.status(400).json({ error: 'Invalid credential type. Must be "default", "access_key", or "bearer_token"' });
}
const result = await bedrockAccountService.updateAccount(accountId, updates);
if (!result.success) {
return res.status(500).json({ error: 'Failed to update Bedrock account', message: result.error });
}
logger.success(`📝 Admin updated Bedrock account: ${accountId}`);
res.json({ success: true, message: 'Bedrock account updated successfully' });
} catch (error) {
logger.error('❌ Failed to update Bedrock account:', error);
res.status(500).json({ error: 'Failed to update Bedrock account', message: error.message });
}
});
// 删除Bedrock账户
router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const result = await bedrockAccountService.deleteAccount(accountId);
if (!result.success) {
return res.status(500).json({ error: 'Failed to delete Bedrock account', message: result.error });
}
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}`);
res.json({ success: true, message: 'Bedrock account deleted successfully' });
} catch (error) {
logger.error('❌ Failed to delete Bedrock account:', error);
res.status(500).json({ error: 'Failed to delete Bedrock account', message: error.message });
}
});
// 切换Bedrock账户状态
router.put('/bedrock-accounts/:accountId/toggle', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const accountResult = await bedrockAccountService.getAccount(accountId);
if (!accountResult.success) {
return res.status(404).json({ error: 'Account not found' });
}
const newStatus = !accountResult.data.isActive;
const updateResult = await bedrockAccountService.updateAccount(accountId, { isActive: newStatus });
if (!updateResult.success) {
return res.status(500).json({ error: 'Failed to toggle account status', message: updateResult.error });
}
logger.success(`🔄 Admin toggled Bedrock account status: ${accountId} -> ${newStatus ? 'active' : 'inactive'}`);
res.json({ success: true, isActive: newStatus });
} catch (error) {
logger.error('❌ Failed to toggle Bedrock account status:', error);
res.status(500).json({ error: 'Failed to toggle account status', message: error.message });
}
});
// 切换Bedrock账户调度状态
router.put('/bedrock-accounts/:accountId/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const accountResult = await bedrockAccountService.getAccount(accountId);
if (!accountResult.success) {
return res.status(404).json({ error: 'Account not found' });
}
const newSchedulable = !accountResult.data.schedulable;
const updateResult = await bedrockAccountService.updateAccount(accountId, { schedulable: newSchedulable });
if (!updateResult.success) {
return res.status(500).json({ error: 'Failed to toggle schedulable status', message: updateResult.error });
}
logger.success(`🔄 Admin toggled Bedrock account schedulable status: ${accountId} -> ${newSchedulable ? 'schedulable' : 'not schedulable'}`);
res.json({ success: true, schedulable: newSchedulable });
} catch (error) {
logger.error('❌ Failed to toggle Bedrock account schedulable status:', error);
res.status(500).json({ error: 'Failed to toggle schedulable status', message: error.message });
}
});
// 测试Bedrock账户连接
router.post('/bedrock-accounts/:accountId/test', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params;
const result = await bedrockAccountService.testAccount(accountId);
if (!result.success) {
return res.status(500).json({ error: 'Account test failed', message: result.error });
}
logger.success(`🧪 Admin tested Bedrock account: ${accountId} - ${result.data.status}`);
res.json({ success: true, data: result.data });
} catch (error) {
logger.error('❌ Failed to test Bedrock account:', error);
res.status(500).json({ error: 'Failed to test Bedrock account', message: error.message });
}
});
// 🤖 Gemini 账户管理
// 生成 Gemini OAuth 授权 URL

View File

@@ -1,6 +1,8 @@
const express = require('express');
const claudeRelayService = require('../services/claudeRelayService');
const claudeConsoleRelayService = require('../services/claudeConsoleRelayService');
const bedrockRelayService = require('../services/bedrockRelayService');
const bedrockAccountService = require('../services/bedrockAccountService');
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler');
const apiKeyService = require('../services/apiKeyService');
const { authenticateApiKey } = require('../middleware/auth');
@@ -101,7 +103,7 @@ async function handleMessagesRequest(req, res) {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
});
} else {
} else if (accountType === 'claude-console') {
// Claude Console账号使用Console转发服务需要传递accountId
await claudeConsoleRelayService.relayStreamRequestWithUsageCapture(req.body, req.apiKey, res, req.headers, (usageData) => {
// 回调函数当检测到完整usage数据时记录真实token使用量
@@ -135,6 +137,44 @@ async function handleMessagesRequest(req, res) {
logger.warn('⚠️ Usage callback triggered but data is incomplete:', JSON.stringify(usageData));
}
}, accountId);
} else if (accountType === 'bedrock') {
// Bedrock账号使用Bedrock转发服务
try {
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId);
if (!bedrockAccountResult.success) {
throw new Error('Failed to get Bedrock account details');
}
const result = await bedrockRelayService.handleStreamRequest(req.body, bedrockAccountResult.data, req.headers, res);
// 记录Bedrock使用统计
if (result.usage) {
const inputTokens = result.usage.input_tokens || 0;
const outputTokens = result.usage.output_tokens || 0;
apiKeyService.recordUsage(req.apiKey.id, inputTokens, outputTokens, 0, 0, result.model, accountId).catch(error => {
logger.error('❌ Failed to record Bedrock stream usage:', error);
});
// 更新时间窗口内的token计数
if (req.rateLimitInfo) {
const totalTokens = inputTokens + outputTokens;
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;
logger.api(`📊 Bedrock stream usage recorded - Model: ${result.model}, Input: ${inputTokens}, Output: ${outputTokens}, Total: ${inputTokens + outputTokens} tokens`);
}
} catch (error) {
logger.error('❌ Bedrock stream request failed:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Bedrock service error', message: error.message });
}
return;
}
}
// 流式请求完成后 - 如果没有捕获到usage数据记录警告但不进行估算
@@ -166,10 +206,43 @@ async function handleMessagesRequest(req, res) {
if (accountType === 'claude-official') {
// 官方Claude账号使用原有的转发服务
response = await claudeRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers);
} else {
} else if (accountType === 'claude-console') {
// Claude Console账号使用Console转发服务
logger.debug(`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`);
response = await claudeConsoleRelayService.relayRequest(req.body, req.apiKey, req, res, req.headers, accountId);
} else if (accountType === 'bedrock') {
// Bedrock账号使用Bedrock转发服务
try {
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId);
if (!bedrockAccountResult.success) {
throw new Error('Failed to get Bedrock account details');
}
const result = await bedrockRelayService.handleNonStreamRequest(req.body, bedrockAccountResult.data, req.headers);
// 构建标准响应格式
response = {
statusCode: result.success ? 200 : 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.success ? result.data : { error: result.error }),
accountId: accountId
};
// 如果成功,添加使用统计到响应数据中
if (result.success && result.usage) {
const responseData = JSON.parse(response.body);
responseData.usage = result.usage;
response.body = JSON.stringify(responseData);
}
} catch (error) {
logger.error('❌ Bedrock non-stream request failed:', error);
response = {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Bedrock service error', message: error.message }),
accountId: accountId
};
}
}
logger.info('📡 Claude API response received', {