Spaces:
Paused
Paused
| // 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] | |
| } | |
| } | |