mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
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:
391
src/services/bedrockRelayService.js
Normal file
391
src/services/bedrockRelayService.js
Normal file
@@ -0,0 +1,391 @@
|
||||
const { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
|
||||
const { fromEnv } = require('@aws-sdk/credential-providers');
|
||||
const logger = require('../utils/logger');
|
||||
const config = require('../../config/config');
|
||||
|
||||
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.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(); // 缓存不同区域的客户端
|
||||
}
|
||||
|
||||
// 获取或创建Bedrock客户端
|
||||
_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);
|
||||
}
|
||||
|
||||
const clientConfig = {
|
||||
region: targetRegion
|
||||
};
|
||||
|
||||
// 如果账户配置了特定的AWS凭证,使用它们
|
||||
if (bedrockAccount?.awsCredentials) {
|
||||
clientConfig.credentials = {
|
||||
accessKeyId: bedrockAccount.awsCredentials.accessKeyId,
|
||||
secretAccessKey: bedrockAccount.awsCredentials.secretAccessKey,
|
||||
sessionToken: bedrockAccount.awsCredentials.sessionToken
|
||||
};
|
||||
} else {
|
||||
// 使用默认凭证链:环境变量 -> AWS配置文件 -> IAM角色
|
||||
clientConfig.credentials = fromEnv();
|
||||
}
|
||||
|
||||
const client = new BedrockRuntimeClient(clientConfig);
|
||||
this.clients.set(clientKey, client);
|
||||
|
||||
logger.debug(`🔧 Created Bedrock client for region: ${targetRegion}, account: ${bedrockAccount?.name || 'default'}`);
|
||||
return client;
|
||||
}
|
||||
|
||||
// 处理非流式请求
|
||||
async handleNonStreamRequest(requestBody, bedrockAccount = null) {
|
||||
try {
|
||||
const modelId = this._selectModel(requestBody, bedrockAccount);
|
||||
const region = this._selectRegion(modelId, bedrockAccount);
|
||||
const client = this._getBedrockClient(region, bedrockAccount);
|
||||
|
||||
// 转换请求格式为Bedrock格式
|
||||
const bedrockPayload = this._convertToBedrockFormat(requestBody);
|
||||
|
||||
const command = new InvokeModelCommand({
|
||||
modelId: modelId,
|
||||
body: JSON.stringify(bedrockPayload),
|
||||
contentType: 'application/json',
|
||||
accept: 'application/json'
|
||||
});
|
||||
|
||||
logger.debug(`🚀 Bedrock非流式请求 - 模型: ${modelId}, 区域: ${region}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await client.send(command);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 解析响应
|
||||
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
|
||||
const claudeResponse = this._convertFromBedrockFormat(responseBody);
|
||||
|
||||
logger.info(`✅ Bedrock请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: claudeResponse,
|
||||
usage: claudeResponse.usage,
|
||||
model: modelId,
|
||||
duration
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Bedrock非流式请求失败:', error);
|
||||
throw this._handleBedrockError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理流式请求
|
||||
async handleStreamRequest(requestBody, bedrockAccount = null, res) {
|
||||
try {
|
||||
const modelId = this._selectModel(requestBody, bedrockAccount);
|
||||
const region = this._selectRegion(modelId, bedrockAccount);
|
||||
const client = this._getBedrockClient(region, bedrockAccount);
|
||||
|
||||
// 转换请求格式为Bedrock格式
|
||||
const bedrockPayload = this._convertToBedrockFormat(requestBody);
|
||||
|
||||
const command = new InvokeModelWithResponseStreamCommand({
|
||||
modelId: modelId,
|
||||
body: JSON.stringify(bedrockPayload),
|
||||
contentType: 'application/json',
|
||||
accept: 'application/json'
|
||||
});
|
||||
|
||||
logger.debug(`🌊 Bedrock流式请求 - 模型: ${modelId}, 区域: ${region}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await client.send(command);
|
||||
|
||||
// 设置SSE响应头
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
});
|
||||
|
||||
let totalUsage = null;
|
||||
let isFirstChunk = true;
|
||||
|
||||
// 处理流式响应
|
||||
for await (const chunk of response.body) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(`✅ Bedrock流式请求完成 - 模型: ${modelId}, 耗时: ${duration}ms`);
|
||||
|
||||
// 发送结束事件
|
||||
res.write('event: done\n');
|
||||
res.write('data: [DONE]\n\n');
|
||||
res.end();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
usage: totalUsage,
|
||||
model: modelId,
|
||||
duration
|
||||
};
|
||||
|
||||
} 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) {
|
||||
// 优先使用账户配置的模型
|
||||
if (bedrockAccount?.defaultModel) {
|
||||
return bedrockAccount.defaultModel;
|
||||
}
|
||||
|
||||
// 检查请求中指定的模型
|
||||
if (requestBody.model) {
|
||||
return requestBody.model;
|
||||
}
|
||||
|
||||
// 使用默认模型
|
||||
return this.defaultModel;
|
||||
}
|
||||
|
||||
// 选择使用的区域
|
||||
_selectRegion(modelId, bedrockAccount) {
|
||||
// 优先使用账户配置的区域
|
||||
if (bedrockAccount?.region) {
|
||||
return bedrockAccount.region;
|
||||
}
|
||||
|
||||
// 对于小模型,使用专门的区域配置
|
||||
if (modelId.includes('haiku')) {
|
||||
return this.smallFastModelRegion;
|
||||
}
|
||||
|
||||
return this.defaultRegion;
|
||||
}
|
||||
|
||||
// 转换Claude格式请求到Bedrock格式
|
||||
_convertToBedrockFormat(requestBody) {
|
||||
const bedrockPayload = {
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
max_tokens: Math.min(requestBody.max_tokens || this.maxOutputTokens, this.maxOutputTokens),
|
||||
messages: requestBody.messages || []
|
||||
};
|
||||
|
||||
// 添加系统提示词
|
||||
if (requestBody.system) {
|
||||
bedrockPayload.system = requestBody.system;
|
||||
}
|
||||
|
||||
// 添加其他参数
|
||||
if (requestBody.temperature !== undefined) {
|
||||
bedrockPayload.temperature = requestBody.temperature;
|
||||
}
|
||||
|
||||
if (requestBody.top_p !== undefined) {
|
||||
bedrockPayload.top_p = requestBody.top_p;
|
||||
}
|
||||
|
||||
if (requestBody.top_k !== undefined) {
|
||||
bedrockPayload.top_k = requestBody.top_k;
|
||||
}
|
||||
|
||||
if (requestBody.stop_sequences) {
|
||||
bedrockPayload.stop_sequences = requestBody.stop_sequences;
|
||||
}
|
||||
|
||||
// 工具调用支持
|
||||
if (requestBody.tools) {
|
||||
bedrockPayload.tools = requestBody.tools;
|
||||
}
|
||||
|
||||
if (requestBody.tool_choice) {
|
||||
bedrockPayload.tool_choice = requestBody.tool_choice;
|
||||
}
|
||||
|
||||
return bedrockPayload;
|
||||
}
|
||||
|
||||
// 转换Bedrock响应到Claude格式
|
||||
_convertFromBedrockFormat(bedrockResponse) {
|
||||
return {
|
||||
id: `msg_${Date.now()}_bedrock`,
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: bedrockResponse.content || [],
|
||||
model: bedrockResponse.model || this.defaultModel,
|
||||
stop_reason: bedrockResponse.stop_reason || 'end_turn',
|
||||
stop_sequence: bedrockResponse.stop_sequence || null,
|
||||
usage: bedrockResponse.usage || {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 转换Bedrock流事件到Claude SSE格式
|
||||
_convertBedrockStreamToClaudeFormat(bedrockChunk) {
|
||||
if (bedrockChunk.type === 'message_start') {
|
||||
return {
|
||||
type: 'message_start',
|
||||
data: {
|
||||
type: 'message',
|
||||
id: `msg_${Date.now()}_bedrock`,
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
model: this.defaultModel,
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: bedrockChunk.message?.usage || { input_tokens: 0, output_tokens: 0 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'content_block_delta') {
|
||||
return {
|
||||
type: 'content_block_delta',
|
||||
data: {
|
||||
index: bedrockChunk.index || 0,
|
||||
delta: bedrockChunk.delta || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'message_delta') {
|
||||
return {
|
||||
type: 'message_delta',
|
||||
data: {
|
||||
delta: bedrockChunk.delta || {},
|
||||
usage: bedrockChunk.usage || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (bedrockChunk.type === 'message_stop') {
|
||||
return {
|
||||
type: 'message_stop',
|
||||
data: {
|
||||
usage: bedrockChunk.usage || {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理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}`);
|
||||
}
|
||||
|
||||
// 获取可用模型列表
|
||||
async getAvailableModels(bedrockAccount = null) {
|
||||
try {
|
||||
const region = bedrockAccount?.region || this.defaultRegion;
|
||||
|
||||
// Bedrock暂不支持列出推理配置文件的API,返回预定义的模型列表
|
||||
const models = [
|
||||
{
|
||||
id: 'us.anthropic.claude-opus-4-1-20250805-v1:0',
|
||||
name: 'Claude Opus 4.1',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
||||
name: 'Claude 3.7 Sonnet',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-5-sonnet-20241022-v2:0',
|
||||
name: 'Claude 3.5 Sonnet v2',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
},
|
||||
{
|
||||
id: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||
name: 'Claude 3.5 Haiku',
|
||||
provider: 'anthropic',
|
||||
type: 'bedrock'
|
||||
}
|
||||
];
|
||||
|
||||
logger.debug(`📋 返回Bedrock可用模型 ${models.length} 个, 区域: ${region}`);
|
||||
return models;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ 获取Bedrock模型列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new BedrockRelayService();
|
||||
Reference in New Issue
Block a user