PRIX / src /bot.ts
Rachit-Tw's picture
fix: implement staggered throttling and sequential file processing for free tier
0dab1eb
Raw
History Blame
5.5 kB
// Node 20 includes native fetch support
// AI Client for Groq with Failover Support
// Fallback shims for Probot runtime
/* eslint-disable no-console */
import {
ChatGPTAPI as AIClient,
ChatGPTError as AIError,
ChatMessage,
SendMessageOptions
} from 'chatgpt'
import pRetry from 'p-retry'
import {AIOptions, Options} from './options'
import {TokenLimits} from './limits'
const logInfo = console.log
const logWarning = console.warn
const logError = console.error
/* eslint-enable no-console */
const setFailed = (msg: string) => {
console.error(msg)
process.exit(1)
}
export interface Ids {
parentMessageId?: string
conversationId?: string
}
export class Bot {
private api: AIClient | null = null
private readonly options: Options
private readonly aiOptions: AIOptions
private currentModel: string
private fallbackModels: string[]
constructor(options: Options, aiOptions: AIOptions) {
this.options = options
this.aiOptions = aiOptions
this.currentModel = aiOptions.model
// Setup fallback chain based on tier
if (Options.HEAVY_MODELS.includes(this.currentModel)) {
this.fallbackModels = [...Options.HEAVY_MODELS]
} else {
this.fallbackModels = [...Options.LIGHT_MODELS]
}
// Remove current model from fallbacks to avoid immediate repeat
this.fallbackModels = this.fallbackModels.filter(
m => m !== this.currentModel
)
this.initClient()
}
private initClient(): void {
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 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 AIClient({
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: string, ids: Ids): Promise<[string, Ids]> => {
let res: [string, Ids] = ['', {}]
try {
res = await this.chat_(message, ids)
return res
} catch (e: unknown) {
if (e instanceof AIError) {
logWarning(`AI communication error: ${e.message}`)
}
return res
}
}
private readonly chat_ = async (
message: string,
ids: Ids
): Promise<[string, Ids]> => {
const start = Date.now()
if (!message) return ['', {}]
let response: ChatMessage | undefined
if (this.api != null) {
const opts: SendMessageOptions = {
timeoutMs: this.options.timeoutMS
}
if (ids.parentMessageId) {
opts.parentMessageId = ids.parentMessageId
}
try {
response = await pRetry(
async attemptState => {
try {
const result = await this.api!.sendMessage(message, opts)
return result
} catch (e: any) {
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()
}
}
throw e
}
},
{
retries: this.options.retries,
minTimeout: 10000, // 10s delay to allow TPM refresh
onFailedAttempt: (err: any) => {
logInfo(
`Attempt ${err.attemptNumber} failed. ${err.retriesLeft} retries left. Error: ${err.message}`
)
const errorResponse = (err as any).response || (err as any).status
if ((errorResponse === 429 || errorResponse === 413) && this.fallbackModels.length > 0) {
const nextModel = this.fallbackModels.shift()
if (nextModel) {
logWarning(
`Proactive switch to fallback model: ${nextModel} due to ${errorResponse}`
)
this.currentModel = nextModel
this.initClient()
}
}
}
}
)
} catch (e: unknown) {
if (e instanceof AIError) {
logInfo(
`AI Error: ${e.message} (Last model used: ${this.currentModel})`
)
}
}
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: Ids = {
parentMessageId: response?.id,
conversationId: response?.conversationId
}
return [responseText, newIds]
}
}