import { AzureOpenAIProvider, createFallbackChain, GeminiProvider, OpenAIProvider, PerplexityProvider, type LLMProvider, } from '@bytelyst/llm'; import { config } from '../config/index.js'; import logger from '../utils/logger.js'; export type AIProvider = 'openai' | 'perplexity' | 'gemini'; export interface AIProviderHealth { provider: AIProvider; configured: boolean; model: string; inFallbackList: boolean; fallbackIndex: number | null; status: 'configured' | 'missing_key' | 'ok' | 'error'; message: string; } interface ProviderEntry { name: AIProvider; make: () => LLMProvider; defaultModel: string; } function resolveModel(provider: AIProvider): string { switch (provider) { case 'perplexity': return config.AI.MODEL.includes('sonar') ? config.AI.MODEL : 'sonar'; case 'openai': return config.AI.MODEL.includes('gpt') ? config.AI.MODEL : 'gpt-4o-mini'; case 'gemini': return config.AI.MODEL.includes('gemini') ? config.AI.MODEL : 'gemini-1.5-flash'; } } function makeOpenAIProvider(): LLMProvider { // Prefer Azure OpenAI when endpoint + key + deployment are all configured if (config.AZURE_OPENAI_ENDPOINT && config.AZURE_OPENAI_KEY && config.AZURE_OPENAI_DEPLOYMENT) { return new AzureOpenAIProvider({ endpoint: config.AZURE_OPENAI_ENDPOINT, apiKey: config.AZURE_OPENAI_KEY, deployment: config.AZURE_OPENAI_DEPLOYMENT, }); } return new OpenAIProvider({ apiKey: config.AI.OPENAI_API_KEY, model: resolveModel('openai') }); } const PROVIDER_REGISTRY: ProviderEntry[] = [ { name: 'perplexity', make: () => new PerplexityProvider({ apiKey: config.AI.PERPLEXITY_API_KEY, model: resolveModel('perplexity') }), defaultModel: 'sonar', }, { name: 'openai', make: makeOpenAIProvider, defaultModel: 'gpt-4o', }, { name: 'gemini', make: () => new GeminiProvider({ apiKey: config.AI.GEMINI_API_KEY, model: resolveModel('gemini') }), defaultModel: 'gemini-1.5-flash', }, ]; export class AIClient { public async generateAnalysis(prompt: string): Promise { const ordered = this.buildOrderedProviders(); const chain = createFallbackChain(ordered.map(e => e.make())); if (!chain.isConfigured()) { logger.warn('[AI] No providers configured — skipping analysis'); return null; } try { const result = await chain.chatCompletion({ messages: [ { role: 'system', content: 'You are an expert crypto trading assistant. Strictly output JSON.' }, { role: 'user', content: prompt }, ], temperature: 0.2, }); logger.info('[AI] Analysis generated successfully'); return result.content; } catch (error: any) { logger.error(`[AI] All providers failed: ${error.message}`); return null; } } public async getProviderHealth(probe: boolean = false): Promise { const fallbackList = config.AI.FALLBACK_LIST; const results: AIProviderHealth[] = []; for (const entry of PROVIDER_REGISTRY) { const provider = entry.make(); const configured = provider.isConfigured(); const fallbackIndex = fallbackList.indexOf(entry.name); const inFallbackList = fallbackIndex >= 0; const model = resolveModel(entry.name); if (!configured) { results.push({ provider: entry.name, configured: false, model, inFallbackList, fallbackIndex: inFallbackList ? fallbackIndex : null, status: 'missing_key', message: 'API key missing' }); continue; } if (!probe) { results.push({ provider: entry.name, configured: true, model, inFallbackList, fallbackIndex: inFallbackList ? fallbackIndex : null, status: 'configured', message: 'Configured (probe skipped)' }); continue; } try { await provider.chatCompletion({ messages: [{ role: 'user', content: 'Return JSON: {"action":"HOLD","confidence":0,"reasoning":"probe"}' }], temperature: 0, }); results.push({ provider: entry.name, configured: true, model, inFallbackList, fallbackIndex: inFallbackList ? fallbackIndex : null, status: 'ok', message: 'Provider probe succeeded' }); } catch (error: any) { results.push({ provider: entry.name, configured: true, model, inFallbackList, fallbackIndex: inFallbackList ? fallbackIndex : null, status: 'error', message: error?.message || 'Provider probe failed' }); } } return results; } private buildOrderedProviders(): ProviderEntry[] { const fallbackList = config.AI.FALLBACK_LIST; return fallbackList .map(name => PROVIDER_REGISTRY.find(e => e.name === name)) .filter((e): e is ProviderEntry => e !== undefined); } }