learning_ai_invt_trdg/backend/src/services/aiClient.ts
Saravana Achu Mac aaa516122e feat(backend): wire Azure Key Vault secret resolution at startup
- Add bootstrap.ts as new entry point — resolves Key Vault secrets via
  DefaultAzureCredential before config/index.ts is evaluated, so all
  process.env reads pick up KV values (Azure CLI in dev, Managed Identity
  in prod). Falls back to .env if AZURE_KEYVAULT_URL is not set.
- Define INVTTRDG_SECRETS mappings for Cosmos, Azure OpenAI, product-id
- Add AZURE_OPENAI_ENDPOINT / KEY / DEPLOYMENT to config
- aiClient: prefer AzureOpenAIProvider (AI Foundry) when Azure OpenAI
  config is present; falls back to direct OpenAI if not configured
- Add @azure/identity, @azure/keyvault-secrets, @bytelyst/config deps
- Update dev/start scripts to use bootstrap.ts entry point
- Document AZURE_KEYVAULT_URL and Azure OpenAI vars in .env.example

Key Vault: https://kv-mywisprai.vault.azure.net/
Secrets prefix: invttrdg-*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:28:47 -07:00

139 lines
5.1 KiB
TypeScript

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<string | null> {
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<AIProviderHealth[]> {
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);
}
}