"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 = exports.AllModelsFailedError = void 0; // AI Client with Failover Support // Fallback shims for Probot runtime const info = console.log; const warning = console.warn; const error = console.error; const debug = process.env.LOG_LEVEL === 'debug' ? console.log.bind(console) : () => { }; const setFailed = (msg) => { // Throw instead of process.exit to avoid killing the Probot server throw new Error(msg); }; const chatgpt_1 = require("chatgpt"); const p_retry_1 = __importDefault(require("p-retry")); const options_1 = require("./options"); const limits_1 = require("./limits"); const tokenizer_1 = require("./tokenizer"); const utils_1 = require("./utils"); const async_mutex_1 = require("async-mutex"); class TokenThrottler { static usage = []; static WINDOW_MS = 60000; static mutex = new async_mutex_1.Mutex(); // Increased TPM limit to prevent self-throttling // Previous limit of 30,000 was too conservative for actual usage static get LIMIT() { return 1000000; // 1M tokens per minute - much more realistic } static async throttle(estimatedTokens) { // CRITICAL: Wrap all state mutation in mutex to prevent race conditions await this.mutex.runExclusive(async () => { const now = Date.now(); this.usage = this.usage.filter(u => now - u.timestamp < this.WINDOW_MS); const currentTokens = this.usage.reduce((sum, u) => sum + u.tokens, 0); const limit = this.LIMIT; if (currentTokens + estimatedTokens > limit) { // Calculate wait time based on oldest entry const oldestTimestamp = this.usage.length > 0 ? this.usage[0].timestamp : now; const waitTime = this.WINDOW_MS - (now - oldestTimestamp) + 1000; info(`TPM Limit approaching (${currentTokens}/${limit} tokens used). Throttling for ${Math.round(waitTime / 1000)}s...`); // Release mutex while sleeping, then re-acquire this.mutex.release(); await (0, utils_1.sleep)(Math.max(waitTime, 1000)); // Recursive call to re-check limits after sleep // No need to re-acquire as recursive call will acquire its own lock await this.throttle(estimatedTokens); return; } this.usage.push({ timestamp: Date.now(), tokens: estimatedTokens }); }); } } class AllModelsFailedError extends Error { constructor() { super('All AI models failed after exhausting fallback chain'); this.name = 'AllModelsFailedError'; } } exports.AllModelsFailedError = AllModelsFailedError; class Bot { api = null; options; aiOptions; currentModel; fallbackModels; constructor(options, aiOptions, fallbackModels) { this.options = options; this.aiOptions = aiOptions; this.currentModel = aiOptions.model; // Setup fallback chain based on tier if (fallbackModels) { this.fallbackModels = fallbackModels.filter(m => m !== this.currentModel); } else if (options_1.Options.FREE_TIER_MODELS.includes(this.currentModel)) { this.fallbackModels = options_1.Options.FREE_TIER_MODELS.filter(m => m !== this.currentModel); } else if (options_1.Options.HEAVY_MODELS.includes(this.currentModel)) { this.fallbackModels = options_1.Options.HEAVY_MODELS.filter(m => m !== this.currentModel); } else { this.fallbackModels = options_1.Options.LIGHT_MODELS.filter(m => m !== this.currentModel); } this.initClient(); } getCurrentModel() { return this.currentModel; } initClient() { const apiKey = process.env.OPENROUTER_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} Cutoff: ${limits.knowledgeCutOff} Date: ${currentDate} Language: ${this.options.language}`; const completionParams = { temperature: this.options.modelTemperature, model: this.currentModel }; // Route paid OSS 120b through Deallm (primary) → DeepInfra (fallback) if (this.currentModel === 'openai/gpt-oss-120b') { completionParams.provider = { order: ['deallm', 'deepinfra'] }; } // Route free OSS 120b through DeepInfra only if (this.currentModel === 'openai/gpt-oss-120b:free') { completionParams.provider = { order: ['deepinfra'] }; } // Route qwen3 235b through DeepInfra (primary) with WandB as fallback. // If BOTH providers fail, OpenRouter returns an error → we switch to next model (oss 120b) if (this.currentModel === 'qwen/qwen3-235b-a22b-2507') { completionParams.provider = { order: ['deepinfra', 'wandb'] }; } 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: Math.min(1800, limits.responseTokens), completionParams }); info(`Initialized AI client with model: ${this.currentModel}`); } else { throw new Error("Missing 'OPENROUTER_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) { warning(`AI communication error: ${e.message}`); } if (e instanceof AllModelsFailedError) { throw e; } 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 { // Bug #2 Fix: use known limits instead of accessing private library field const limits = new limits_1.TokenLimits(this.currentModel); const estimatedTokens = (0, tokenizer_1.getTokenCount)(message) + limits.responseTokens; await TokenThrottler.throttle(estimatedTokens); // Log payload size before request to detect hidden context const messageSize = message.length; debug('MESSAGE_SIZE_CHARS', messageSize); debug('MESSAGE_SIZE_KB', messageSize / 1024); debug('HAS_PARENT_MESSAGE_ID', !!opts.parentMessageId); debug('HAS_CONVERSATION_ID', !!ids.conversationId); debug('MESSAGE_PREVIEW', message.substring(0, 500)); // CRITICAL: Detect if message contains generated file patterns (safety check) const generatedPatterns = [ 'node_modules/', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', '.next/', 'dist/', 'build/', '.git/' ]; const hasGeneratedContent = generatedPatterns.some(pattern => message.includes(pattern)); if (hasGeneratedContent) { console.warn('WARNING: Message may contain generated file content - upstream filtering should have removed this'); for (const pattern of generatedPatterns) { if (message.includes(pattern)) { console.warn(` Found pattern: ${pattern}`); } } } // HARD PAYLOAD LIMIT - fail early before sending giant requests const MAX_PAYLOAD_CHARS = 120000; if (messageSize > MAX_PAYLOAD_CHARS) { error(`PAYLOAD_TOO_LARGE_PREVENTED: Message is ${messageSize} chars, limit is ${MAX_PAYLOAD_CHARS}`); throw new Error(`PAYLOAD_TOO_LARGE_PREVENTED: Message is ${messageSize} chars, limit is ${MAX_PAYLOAD_CHARS}`); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.options.timeoutMS); try { response = await (0, p_retry_1.default)(async () => { if (controller.signal.aborted) throw new Error('Request timed out'); try { return await this.api.sendMessage(message, opts); } catch (e) { // CRITICAL: 413 errors should NEVER retry - they waste tokens and time if (e?.status === 413 || e?.message?.includes('Request Entity Too Large')) { error(`413 Payload Too Large - NOT retrying. Message size: ${messageSize} chars`); throw e; // Re-throw to fail immediately without retry } // Any error: try fallback model if available (cycles through chain) if (this.fallbackModels.length > 0) { const nextModel = this.fallbackModels.shift(); if (nextModel) { warning(`Model ${this.currentModel} failed (${e?.status || e?.message}). Switching to fallback: ${nextModel}`); this.currentModel = nextModel; this.initClient(); delete opts.parentMessageId; throw new AllModelsFailedError(); } } throw e; } }, { retries: this.options.retries, onFailedAttempt: error => { info(`Attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`); } }); } finally { clearTimeout(timeoutId); } } catch (e) { if (e instanceof chatgpt_1.ChatGPTError) { info(`AI Error: ${e.message} (Last model used: ${this.currentModel})`); } else if (e instanceof Error) { error(`Non-AI error in chat: ${e.message} (Last model used: ${this.currentModel})`); } } const end = Date.now(); info(`AI response time: ${end - start} ms`); // Token usage logging if (response != null) { const inputTokens = (0, tokenizer_1.getTokenCount)(message); const outputTokens = (0, tokenizer_1.getTokenCount)(response.text); const totalTokens = inputTokens + outputTokens; info(`Token usage: ${inputTokens}+${outputTokens}=${totalTokens} tokens (${this.currentModel})`); } } 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