Proveedores LLM
Interfaz común BaseProviderAdapter
Section titled “Interfaz común BaseProviderAdapter”Archivo: apps/ai-backend/src/modules/providers/base-provider.adapter.ts
export abstract class BaseProviderAdapter { abstract readonly provider: AIProvider;
// Respuesta en streaming (para tutor_chat) abstract stream(options: StreamOptions): AsyncIterable<string>;
// Respuesta completa (para el resto de casos de uso) abstract complete(options: CompleteOptions): Promise<string>;
// Test de conectividad (usado por HealthController) abstract ping(): Promise<{ latencyMs: number }>;}Los 3 adapters implementan esta interfaz, lo que permite al RouterService usarlos de forma intercambiable.
AnthropicAdapter
Section titled “AnthropicAdapter”Archivo: apps/ai-backend/src/modules/providers/anthropic/anthropic.adapter.ts
- SDK:
@anthropic-ai/sdkv0.36+ - Streaming:
client.messages.stream()→ itera sobrecontent_block_delta - Casos de uso primarios:
tutor_chat,content_gen_case,open_answer_evaluation - La API key se obtiene en cada request vía
CredentialsService.getKey(AIProvider.ANTHROPIC)
async *stream(options: StreamOptions): AsyncIterable<string> { const client = await this.getClient(); // getKey + new Anthropic({apiKey}) const stream = await client.messages.stream({ model: options.model, max_tokens: options.maxTokens ?? 2048, temperature: options.temperature ?? 0.7, system: options.systemPrompt, messages: options.messages.map(m => ({ role: m.role, content: m.content })), });
for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { yield event.delta.text; } }}OpenAIAdapter
Section titled “OpenAIAdapter”Archivo: apps/ai-backend/src/modules/providers/openai/openai.adapter.ts
- SDK:
openaiv4.77+ - Streaming:
client.chat.completions.create({ stream: true }) - Casos de uso primarios:
adaptive_engine,content_gen_quiz,churn_prediction - También gestiona embeddings para
semantic_searchcontext-embedding-3-small
GeminiAdapter
Section titled “GeminiAdapter”Archivo: apps/ai-backend/src/modules/providers/gemini/gemini.adapter.ts
- SDK:
@google/generative-aiv0.21+ - Sistema de mensajes: Gemini no tiene campo
systemnativo — el system prompt se inyecta como primer mensaje deusercon respuesta demodel - Casos de uso primarios:
content_gen_summary,fluency_test_scoring
private buildContents(systemPrompt: string, messages: ChatMessage[]) { return [ { role: 'user', parts: [{ text: systemPrompt }] }, { role: 'model', parts: [{ text: 'Entendido. Estoy listo para ayudar.' }] }, ...messages.map(m => ({ role: m.role === 'user' ? 'user' : 'model', parts: [{ text: m.content }], })), ];}EncryptionService
Section titled “EncryptionService”Archivo: apps/ai-backend/src/modules/credentials/encryption.service.ts
Cifra y descifra API keys con AES-256-GCM:
// Cifrar (usado al guardar desde apps/api)encrypt(plaintext: string): string // → "iv:ciphertext:tag" (hex)
// Descifrar (usado al recuperar para llamadas LLM)decrypt(encrypted: string): string // ← "iv:ciphertext:tag" (hex)La clave maestra viene de AI_CREDENTIALS_MASTER_KEY (64 chars hex = 32 bytes). Nunca cambia en producción sin migrar los valores cifrados en BD.
CredentialsService
Section titled “CredentialsService”Archivo: apps/ai-backend/src/modules/credentials/credentials.service.ts
- Mantiene un caché en memoria con TTL de 5 minutos
- En cache miss: llama a
GET /internal/ai-credentials/:providerenapps/api - Descifra el
encryptedValueantes de devolver la key al adapter - Si
isActive === false: lanza excepción — el proveedor no está configurado
async getKey(provider: AIProvider): Promise<string> { const cached = this.cache.get(provider); if (cached && !this.isExpired(cached)) return cached.key;
const credential = await this.fetchFromApi(provider); if (!credential.isActive) throw new Error(`Provider ${provider} not configured`);
const key = this.encryption.decrypt(credential.encryptedValue); this.cache.set(provider, { key, cachedAt: Date.now() }); return key;}