PRININIT / lib /src /bot.js
Rachit-Tw's picture
Upload 183 files
47606a7 verified
Raw
History Blame Contribute Delete
12.6 kB
"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