Skip to content

Proveedores LLM

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.

Archivo: apps/ai-backend/src/modules/providers/anthropic/anthropic.adapter.ts

  • SDK: @anthropic-ai/sdk v0.36+
  • Streaming: client.messages.stream() → itera sobre content_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;
}
}
}

Archivo: apps/ai-backend/src/modules/providers/openai/openai.adapter.ts

  • SDK: openai v4.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_search con text-embedding-3-small

Archivo: apps/ai-backend/src/modules/providers/gemini/gemini.adapter.ts

  • SDK: @google/generative-ai v0.21+
  • Sistema de mensajes: Gemini no tiene campo system nativo — el system prompt se inyecta como primer mensaje de user con respuesta de model
  • 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 }],
})),
];
}

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.

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/:provider en apps/api
  • Descifra el encryptedValue antes 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;
}