route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
455 lines • 18.9 kB
JavaScript
;
/**
* Gemini API Client - Pure API Client
* Google Gemini API integration via Generative Language API
* Project owner: Jason Zhang
*
* Architecture: Pure API client following transformer architecture
* - Zero hardcoding, zero fallback principles
* - Provider only handles API calls, no format conversion
* - All transformations handled by transformer layer
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeminiClient = void 0;
const logger_1 = require("../../utils/logger");
const enhanced_rate_limit_manager_1 = require("./enhanced-rate-limit-manager");
const genai_1 = require("@google/genai");
// Using interfaces from types.ts to avoid conflicts
class GeminiClient {
config;
name;
type = 'gemini';
apiKey;
baseUrl;
enhancedRateLimitManager;
apiKeys = [];
maxRetries;
retryDelay;
requestTimeout;
genAIClients = [];
constructor(config, providerId) {
this.config = config;
if (!providerId) {
throw new Error('GeminiClient: providerId is required - no default fallback allowed');
}
this.name = providerId;
// Zero Hardcode Principle: All configuration from config
// @ts-ignore - TODO: Add proper types for timeout and retry
if (!config.timeout) {
throw new Error('GeminiClient: config.timeout is required');
}
// @ts-ignore - TODO: Add proper types for timeout and retry
if (!config.retry) {
throw new Error('GeminiClient: config.retry is required with maxRetries and delayMs properties');
}
// @ts-ignore - TODO: Add proper types for timeout and retry
this.maxRetries = config.retry.maxRetries;
// @ts-ignore - TODO: Add proper types for timeout and retry
this.retryDelay = config.retry.delayMs;
// @ts-ignore - TODO: Add proper types for timeout and retry
this.requestTimeout = config.timeout;
// Handle API key configuration - support both single and multiple keys
const credentials = config.authentication.credentials;
const apiKey = credentials ? (credentials.apiKey ?? credentials.api_key) : undefined;
if (Array.isArray(apiKey) && apiKey.length > 1) {
this.apiKeys = apiKey;
// Initialize rate limit manager without fallback (Zero Fallback Principle)
this.enhancedRateLimitManager = new enhanced_rate_limit_manager_1.EnhancedRateLimitManager(apiKey, this.name, this.config);
this.apiKey = '';
this.genAIClients = apiKey.map(key => new genai_1.GoogleGenAI({ apiKey: key }));
logger_1.logger.info('Initialized Enhanced Gemini Rate Limit Manager', {
providerId: this.name,
keyCount: apiKey.length,
fallbackDisabled: 'Zero Fallback Principle enforced'
});
}
else {
if (!apiKey) {
throw new Error('GeminiClient: API key is required');
}
this.apiKey = Array.isArray(apiKey) ? apiKey[0] : apiKey;
this.apiKeys = [this.apiKey];
if (!this.apiKey) {
throw new Error('Gemini API key is required');
}
this.genAIClients = [new genai_1.GoogleGenAI({ apiKey: this.apiKey })];
}
if (!config.endpoint) {
throw new Error('GeminiClient: config.endpoint is required - no default endpoint allowed (Zero Hardcode Principle)');
}
this.baseUrl = config.endpoint;
logger_1.logger.info('Gemini client initialized with minimal architecture', {
endpoint: this.baseUrl,
hasApiKey: !!(this.apiKey ?? this.enhancedRateLimitManager),
keyRotationEnabled: !!this.enhancedRateLimitManager
});
}
async healthCheck() {
try {
const genAI = this.genAIClients[0];
if (!genAI) {
logger_1.logger.error('No GenAI client available for health check');
return false;
}
// 获取健康检查用的模型名 - 零硬编码原则
const healthCheckModel = this.config.healthCheck?.model;
if (!healthCheckModel) {
throw new Error('Health check model not configured. Required: config.healthCheck.model');
}
// 使用配置化的健康检查请求
const testResponse = await genAI.models.generateContent({
model: healthCheckModel,
contents: [{
role: 'user',
parts: [{ text: 'Hi' }]
}]
});
const success = !!(testResponse && testResponse.candidates && testResponse.candidates.length > 0);
if (success) {
logger_1.logger.debug('Gemini health check succeeded', { model: healthCheckModel });
}
else {
logger_1.logger.warn('Gemini health check returned no response', { model: healthCheckModel });
}
return success;
}
catch (error) {
logger_1.logger.error('Gemini health check failed', {
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
async createCompletion(request) {
if (!request.metadata?.requestId) {
throw new Error('GeminiClient: request.metadata.requestId is required');
}
const requestId = request.metadata.requestId;
logger_1.logger.info('Processing pure Gemini API request', {
model: request.model,
messageCount: request.messages?.length ?? 0,
hasTools: !!request.tools,
maxTokens: request.max_tokens
}, requestId, 'gemini-provider');
// 期望接收已转换的Gemini格式请求
if (!this.isValidGeminiRequest(request)) {
throw new Error('GeminiClient: Invalid Gemini request format. Request must be pre-transformed.');
}
// 构建Gemini API请求
const geminiRequest = this.buildGeminiApiRequest(request);
// 执行API调用
const geminiResponse = await this.executeWithRetry(async (genAI, model) => {
const apiRequest = {
...geminiRequest,
model: model
};
return Promise.race([
genAI.models.generateContent(apiRequest),
new Promise((_, reject) => setTimeout(() => reject(new Error(`Gemini SDK timeout after ${this.requestTimeout}ms`)), this.requestTimeout))
]);
}, request.model, 'createCompletion', requestId);
// 构建原始Gemini响应
const response = this.buildGeminiResponse(geminiResponse, request.model, requestId);
logger_1.logger.info('Pure Gemini API request completed successfully', {
originalModel: request.model,
responseType: 'raw_gemini_response',
candidatesCount: geminiResponse.candidates?.length ?? 0
}, requestId, 'gemini-provider');
return response;
}
async *streamCompletion(request) {
if (!request.metadata?.requestId) {
throw new Error('GeminiClient: request.metadata.requestId is required');
}
const requestId = request.metadata.requestId;
logger_1.logger.info('Starting pure Gemini API streaming', {
model: request.model,
messageCount: request.messages?.length ?? 0
}, requestId, 'gemini-provider');
// For now, use non-streaming and return raw response
const response = await this.createCompletion(request);
// 返回原始Gemini响应,不进行Anthropic格式转换
yield response;
logger_1.logger.info('Pure Gemini API streaming completed', {
responseType: 'raw_gemini_response'
}, requestId, 'gemini-provider');
}
/**
* Execute request with retry logic and API key rotation
*/
async executeWithRetry(requestFn, modelName, operation, requestId) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
let genAIClient;
let keyIndex = 0;
let currentModel = modelName;
try {
if (this.enhancedRateLimitManager) {
const keyAndModel = this.enhancedRateLimitManager.getAvailableKeyAndModel(modelName, requestId);
genAIClient = this.genAIClients[keyAndModel.keyIndex];
keyIndex = keyAndModel.keyIndex;
currentModel = keyAndModel.model;
// No fallback logging - Zero Fallback Principle enforced
// All model routing must be handled at routing layer, not provider layer
}
else {
keyIndex = attempt % this.genAIClients.length;
genAIClient = this.genAIClients[keyIndex];
}
}
catch (keyError) {
logger_1.logger.error(`No available keys for ${operation}`, keyError, requestId, 'gemini-provider');
throw keyError;
}
try {
const result = await requestFn(genAIClient, currentModel);
logger_1.logger.debug(`${operation} succeeded`, {
keyIndex,
model: currentModel,
attempt: attempt + 1
}, requestId, 'gemini-provider');
return result;
}
catch (error) {
lastError = error;
const isRateLimited = (error?.message?.includes('quota') ||
error?.message?.includes('rate') ||
error?.status === 429);
logger_1.logger.warn(`${operation} failed`, {
keyIndex,
model: currentModel,
attempt: attempt + 1,
maxRetries: this.maxRetries,
isRateLimited,
error: error instanceof Error ? error.message : String(error)
}, requestId, 'gemini-provider');
// Report error to rate limit manager
if (this.enhancedRateLimitManager && isRateLimited) {
this.enhancedRateLimitManager.report429Error(keyIndex, currentModel, requestId);
}
if (!this.isRetryableError(error)) {
break;
}
if (attempt < this.maxRetries - 1) {
await this.waitForRetry(attempt, isRateLimited);
}
}
}
throw lastError;
}
/**
* Check if error is retryable
*/
isRetryableError(error) {
const status = error?.status ?? error?.response?.status;
const message = error?.message ?? '';
// HTTP status code based retries
if (status) {
const retryableStatuses = [429, 502, 503, 504];
if (retryableStatuses.includes(status)) {
return true;
}
}
// Gemini specific error patterns
const retryablePatterns = [
'quota',
'rate',
'RESOURCE_EXHAUSTED',
'Too Many Requests',
'temporarily unavailable',
'service unavailable'
];
return retryablePatterns.some(pattern => message.toLowerCase().includes(pattern.toLowerCase()));
}
/**
* Wait for retry delay
*/
async waitForRetry(attempt, isRateLimited = false) {
let delay;
if (isRateLimited) {
// Use configuration for retry delays
const retryDelays = this.config.retryDelays;
if (!retryDelays || retryDelays.length < 3) {
throw new Error('Gemini client requires retryDelays configuration with at least 3 values');
}
if (attempt === 0)
delay = retryDelays[0]; // 1st retry
else if (attempt === 1)
delay = retryDelays[1]; // 2nd retry
else
delay = retryDelays[2]; // 3rd retry
}
else {
delay = this.retryDelay * Math.pow(2, attempt);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
/**
* 验证请求是否为有效的Gemini格式
*/
isValidGeminiRequest(request) {
// 暂时接受所有请求,内部进行转换
// 检查是否已经是Gemini格式,或者是需要转换的Anthropic/OpenAI格式
return !!request.metadata?.geminiFormat || this.isTransformableRequest(request);
}
isTransformableRequest(request) {
// 检查是否是可以转换的请求格式
return !!(request.messages && Array.isArray(request.messages) && request.messages.length > 0);
}
/**
* 构建Gemini API请求格式
*/
buildGeminiApiRequest(request) {
// 如果已经是预处理的Gemini格式,直接使用
if (request.metadata?.geminiFormat && request.metadata?.geminiRequest) {
return {
model: request.model,
...request.metadata.geminiRequest
};
}
// 否则进行内部转换 (临时解决方案)
return this.transformRequestToGemini(request);
}
transformRequestToGemini(request) {
// 简化的内部转换逻辑
const contents = [];
// 添加系统消息
if (request.metadata?.system) {
const systemText = Array.isArray(request.metadata.system)
? request.metadata.system.map(s => s.text || JSON.stringify(s)).join('\n')
: request.metadata.system;
contents.push({
role: 'user',
parts: [{ text: `System: ${systemText}` }]
});
}
// 转换消息
for (const message of request.messages) {
const role = message.role === 'assistant' ? 'model' : 'user';
const text = typeof message.content === 'string'
? message.content
: JSON.stringify(message.content);
contents.push({
role: role,
parts: [{ text }]
});
}
const geminiRequest = {
model: request.model,
contents,
generationConfig: {
temperature: request.temperature,
maxOutputTokens: request.max_tokens || 131072
}
};
// 处理工具
if (request.tools && request.tools.length > 0) {
geminiRequest.tools = [{
functionDeclarations: request.tools.map(tool => ({
name: tool.name || tool.function?.name,
description: tool.description || tool.function?.description,
parameters: tool.input_schema || tool.parameters || tool.function?.parameters || {}
}))
}];
geminiRequest.toolConfig = {
functionCallingConfig: {
mode: 'AUTO',
allowedFunctionNames: geminiRequest.tools[0].functionDeclarations.map(f => f.name)
}
};
}
// 添加安全设置
geminiRequest.safetySettings = [
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' },
{ category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' },
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_NONE' },
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_NONE' }
];
return geminiRequest;
}
/**
* 构建原Gemini响应格式
*/
buildGeminiResponse(geminiResponse, originalModel, requestId) {
// 进行内部转换 (临时解决方案)
return this.transformGeminiResponseToAnthropic(geminiResponse, originalModel, requestId);
}
transformGeminiResponseToAnthropic(geminiResponse, originalModel, requestId) {
if (!geminiResponse.candidates || geminiResponse.candidates.length === 0) {
throw new Error('No candidates in Gemini response');
}
const candidate = geminiResponse.candidates[0];
const content = [];
// 转换响应内容
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.text) {
content.push({
type: 'text',
text: part.text
});
}
else if (part.functionCall) {
content.push({
type: 'tool_use',
id: `tool_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: part.functionCall.name,
input: part.functionCall.args || {}
});
}
}
}
// 如果没有内容,添加空文本
if (content.length === 0) {
content.push({
type: 'text',
text: ''
});
}
// 确定stop_reason
let stopReason = 'end_turn';
const hasToolUse = content.some(block => block.type === 'tool_use');
if (hasToolUse) {
stopReason = 'tool_use';
}
else if (candidate.finishReason === 'MAX_TOKENS') {
stopReason = 'max_tokens';
}
return {
id: requestId,
type: 'message',
role: 'assistant',
content: content,
model: originalModel,
stop_reason: stopReason,
usage: geminiResponse.usageMetadata ? {
input_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
output_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0
} : undefined
};
}
/**
* 生成唯一消息ID
*/
generateMessageId() {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `msg_gemini_${timestamp}_${random}`;
}
/**
* Get API key rotation statistics
*/
getRotationStats() {
if (this.enhancedRateLimitManager) {
return this.enhancedRateLimitManager.getStatus();
}
return {
providerId: this.name,
totalKeys: 1,
activeKeys: 1,
rotationEnabled: false
};
}
}
exports.GeminiClient = GeminiClient;
//# sourceMappingURL=client.js.map