"use strict"; // Node 20 includes native fetch support var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Bot = void 0; // AI Client for Groq with Failover Support + Circuit Breaker // Fallback shims for Probot runtime /* eslint-disable no-console */ const chatgpt_1 = require("chatgpt"); const p_retry_1 = __importDefault(require("p-retry")); const opossum_1 = __importDefault(require("opossum")); const options_1 = require("./options"); const limits_1 = require("./limits"); const pino_1 = __importDefault(require("pino")); const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' }); // Circuit breaker configuration const CIRCUIT_BREAKER_OPTIONS = { timeout: 60000, // 60 seconds errorThresholdPercentage: 50, resetTimeout: 30000, volumeThreshold: 5, }; const logInfo = (msg) => logger.info(msg); const logWarning = (msg) => logger.warn(msg); const logError = (msg) => logger.error(msg); const setFailed = (msg) => { logger.error(msg); process.exit(1); }; class Bot { api = null; options; aiOptions; currentModel; fallbackModels; circuitBreaker = null; constructor(options, aiOptions) { this.options = options; this.aiOptions = aiOptions; this.currentModel = aiOptions.model; // Setup fallback chain based on tier if (options_1.Options.HEAVY_MODELS.includes(this.currentModel)) { this.fallbackModels = [...options_1.Options.HEAVY_MODELS]; } else { this.fallbackModels = [...options_1.Options.LIGHT_MODELS]; } // Remove current model from fallbacks to avoid immediate repeat this.fallbackModels = this.fallbackModels.filter(m => m !== this.currentModel); this.initClient(); this.initCircuitBreaker(); } initCircuitBreaker() { if (!this.api) return; // Wrap the sendMessage method with circuit breaker this.circuitBreaker = new opossum_1.default(async ({ message, opts }) => { return this.api.sendMessage(message, opts); }, CIRCUIT_BREAKER_OPTIONS); // Circuit breaker event handlers this.circuitBreaker.on('open', () => { logWarning(`Circuit breaker opened for model: ${this.currentModel}`); }); this.circuitBreaker.on('halfOpen', () => { logInfo(`Circuit breaker half-open for model: ${this.currentModel}`); }); this.circuitBreaker.on('close', () => { logInfo(`Circuit breaker closed for model: ${this.currentModel}`); }); this.circuitBreaker.on('fallback', (_result) => { logWarning(`Circuit breaker fallback executed for model: ${this.currentModel}`); }); } initClient() { const apiKey = process.env.GROQ_API_KEY || process.env.AI_API_KEY; if (apiKey) { const currentDate = new Date().toISOString().split('T')[0]; const limits = new limits_1.TokenLimits(this.currentModel); const systemMessage = `${this.options.systemMessage} Knowledge cutoff: ${limits.knowledgeCutOff} Current date: ${currentDate} IMPORTANT: Entire response must be in ISO code: ${this.options.language} `; this.api = new chatgpt_1.ChatGPTAPI({ apiBaseUrl: this.options.apiBaseUrl, systemMessage, apiKey, apiOrg: process.env.AI_API_ORG || undefined, debug: this.options.debug, maxModelTokens: limits.maxTokens, maxResponseTokens: limits.responseTokens, completionParams: { temperature: this.options.modelTemperature, model: this.currentModel } }); logInfo(`Initialized AI client with model: ${this.currentModel}`); } else { throw new Error("Missing 'GROQ_API_KEY' or 'AI_API_KEY'"); } } chat = async (message, ids) => { let res = ['', {}]; try { res = await this.chat_(message, ids); return res; } catch (e) { if (e instanceof chatgpt_1.ChatGPTError) { logWarning(`AI communication error: ${e.message}`); } return res; } }; chat_ = async (message, ids) => { const start = Date.now(); if (!message) return ['', {}]; let response; if (this.api != null) { const opts = { timeoutMs: this.options.timeoutMS }; if (ids.parentMessageId) { opts.parentMessageId = ids.parentMessageId; } try { // Check if circuit breaker is open if (this.circuitBreaker?.opened) { logWarning(`Circuit breaker is OPEN for ${this.currentModel}, attempting fallback...`); if (this.fallbackModels.length > 0) { const nextModel = this.fallbackModels.shift(); if (nextModel) { this.currentModel = nextModel; this.initClient(); this.initCircuitBreaker(); // Recursive retry with new model return this.chat_(message, ids); } } throw new Error(`Circuit breaker open and no fallback models available for ${this.currentModel}`); } const result = await (0, p_retry_1.default)(async () => { if (!this.circuitBreaker) { throw new Error('Circuit breaker not initialized'); } try { const messageResult = await this.circuitBreaker.fire({ message, opts }); return messageResult; } catch (e) { // Check if circuit breaker opened if (this.circuitBreaker.opened && this.fallbackModels.length > 0) { const nextModel = this.fallbackModels.shift(); if (nextModel) { logWarning(`Circuit breaker opened for ${this.currentModel}. Switching to fallback: ${nextModel}`); this.currentModel = nextModel; this.initClient(); this.initCircuitBreaker(); // Throw to trigger retry with new model throw new Error(`Model switched to ${nextModel}, retrying...`); } } // Handle rate limiting / context size if ((e?.status === 429 || e?.status === 413) && this.fallbackModels.length > 0) { const nextModel = this.fallbackModels.shift(); if (nextModel) { logWarning(`Rate limit or context size error (${e.status}) hit for ${this.currentModel}. Switching to fallback: ${nextModel}`); this.currentModel = nextModel; this.initClient(); this.initCircuitBreaker(); throw new Error(`Model switched to ${nextModel}, retrying...`); } } throw e; } }, { retries: this.options.retries, minTimeout: 20000, // 20s delay to allow TPM refresh onFailedAttempt: (err) => { logInfo(`Attempt ${err.attemptNumber} failed. ${err.retriesLeft} retries left. Error: ${err.message}`); } }); response = result; } catch (e) { if (e instanceof chatgpt_1.ChatGPTError) { logWarning(`AI Error after retries: ${e.message} (Last model used: ${this.currentModel})`); } else if (e instanceof Error) { logError(`Failed to get AI response: ${e.message}`); } } const end = Date.now(); logInfo(`AI response time: ${end - start} ms`); } else { setFailed('AI client is not initialized'); } let responseText = ''; if (response != null) { responseText = response.text; } const newIds = { parentMessageId: response?.id, conversationId: response?.conversationId }; return [responseText, newIds]; }; } exports.Bot = Bot; //# sourceMappingURL=bot.js.map