import { Injectable, Logger } from "@nestjs/common" import axios from "axios" import https from "https" interface AiCallOptions { systemPrompt: string userMessage: string temperature?: number maxTokens?: number } const httpAgent = new https.Agent({ rejectUnauthorized: true, keepAlive: true }) @Injectable() export class AiService { private readonly logger = new Logger(AiService.name) private readonly primaryUrl = process.env.AI_PRIMARY_URL || "https://token.sensenova.cn/v1" private readonly primaryKey = process.env.AI_PRIMARY_KEY || "" private readonly primaryModel = process.env.AI_PRIMARY_MODEL || "deepseek-v4-flash" private readonly primaryFallbackModel = process.env.AI_PRIMARY_FALLBACK_MODEL || "sensenova-6.7-flash-lite" private readonly backupUrl = process.env.AI_BACKUP_URL || "https://integrate.api.nvidia.com/v1" private readonly backupKey = process.env.AI_BACKUP_KEY || "" private readonly backupModel = process.env.AI_BACKUP_MODEL || "meta/llama-3.1-8b-instruct" async call(options: AiCallOptions): Promise { const { systemPrompt, userMessage, temperature = 0.7, maxTokens = 2048 } = options // Try primary AI (deepseek-v4-flash on sensenova) try { const result = await this.callApi(this.primaryUrl, this.primaryKey, this.primaryModel, systemPrompt, userMessage, temperature, maxTokens, 60000) if (result) return result // Primary returned empty content (thinking model exhausted tokens); retry with more tokens const retry = await this.callApi(this.primaryUrl, this.primaryKey, this.primaryModel, systemPrompt, userMessage, temperature, Math.min(maxTokens * 2, 4096), 60000) if (retry) return retry } catch (e) { this.logger.warn(`Primary AI failed: ${(e as Error).message}, trying primary fallback...`) } // Try primary fallback model (sensenova-6.7-flash-lite, same provider) try { const result = await this.callApi(this.primaryUrl, this.primaryKey, this.primaryFallbackModel, systemPrompt, userMessage, temperature, maxTokens, 60000) if (result) return result } catch (e) { this.logger.warn(`Primary fallback AI also failed: ${(e as Error).message}, trying backup...`) } // Try backup AI (NVIDIA - meta/llama-3.1-8b-instruct) try { const result = await this.callApi(this.backupUrl, this.backupKey, this.backupModel, systemPrompt, userMessage, temperature, Math.max(maxTokens, 2048), 120000) if (result) return result } catch (e) { this.logger.warn(`Backup AI also failed: ${(e as Error).message}`) } throw new Error("AI 服务暂时不可用,请稍后重试") } private async callApi( baseUrl: string, apiKey: string, model: string, systemPrompt: string, userMessage: string, temperature: number, maxTokens: number, timeout: number, ): Promise { try { const res = await axios.post( `${baseUrl}/chat/completions`, { model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userMessage }, ], temperature, max_tokens: maxTokens, }, { headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, timeout, httpsAgent: httpAgent, transitional: { clarifyTimeoutError: true }, }, ) return res.data?.choices?.[0]?.message?.content || null } catch (e: any) { if (e.code === 'ECONNABORTED') { this.logger.warn(`AI call timeout (${timeout}ms): ${model}`) } return null } } }