| name | llm-integration |
| description | Multi-provider LLM integration for phrase generation (Groq, OpenAI, Gemini, extensible) |
| license | MIT |
| compatibility | opencode |
| metadata | {"type":"ai-integration","pattern":"factory"} |
SKILL: LLM Integration (Multi-Provider)
Arquitectura
El proyecto usa un patrón Factory para soportar múltiples proveedores LLM de forma intercambiable.
El Domain define el contrato, Infrastructure implementa cada provider.
Domain/
Phrase/Service/PhraseGeneratorInterface.php # Contrato agnóstico
Infrastructure/
ExternalApi/
PhraseGeneratorFactory.php # Factory (selecciona provider por env)
Shared/PhrasePrompt.php # Prompt compartido entre providers
OpenAI/
RealOpenAIPhraseGenerator.php # Provider OpenAI-compatible (Groq, OpenAI)
Exception/OpenAIException.php
Gemini/
GeminiPhraseGenerator.php # Provider Gemini (REST API propia)
Exception/GeminiException.php
Phrase/
FakePhraseGenerator.php # Fake (templates sin API, dev/test)
Providers Disponibles
| Provider | Env PHRASE_PROVIDER | Modelo default |
|---|
| Groq GPT-OSS 120B | openai (API compatible) | openai/gpt-oss-120b |
| Groq Llama 3.3 70B | openai (API compatible) | meta-llama/llama-3.3-70b-versatile |
| OpenAI | openai | gpt-4o-mini |
| Gemini | gemini | gemini-2.5-flash |
| Fake | fake | ninguno (templates locales) |
Nota: Groq usa API compatible OpenAI. Cambiar de OpenAI a Groq (o viceversa) es solo cambiar OPENAI_BASE_URL y OPENAI_MODEL en .env. No requiere cambios de código. Cualquier provider puede usarse en cualquier entorno.
Contrato Domain (Agnóstico)
interface PhraseGeneratorInterface
{
public function generate(PictogramSequence $sequence, array $labels): array;
}
El Domain NO conoce que LLM se usa. Solo define el contrato.
Factory Pattern
public function create(): PhraseGeneratorInterface
{
return match ($this->provider) {
'gemini' => new GeminiPhraseGenerator(...),
'openai' => new RealOpenAIPhraseGenerator(...),
'fake' => new FakePhraseGenerator(),
default => new FakePhraseGenerator(),
};
}
Prompt Compartido
Todos los providers usan el mismo prompt (consistencia para usuarios SAAC):
final class PhrasePrompt
{
public const string SYSTEM = <<<PROMPT
Eres un asistente especializado en comunicación aumentativa y alternativa (SAAC).
Tu tarea es convertir palabras clave de pictogramas en frases naturales en español.
Genera exactamente 3 variaciones de la frase.
Responde SOLO con un JSON válido: {"variations": ["frase 1", "frase 2", "frase 3"]}
PROMPT;
public const string USER_TEMPLATE = 'Genera 3 variaciones de frase natural para las siguientes palabras: %s';
public const int VARIATIONS_COUNT = 3;
public const int MAX_LABEL_LENGTH = 50;
}
Añadir Nuevo Provider
- Crear directorio:
Infrastructure/ExternalApi/{ProviderName}/
- Implementar
PhraseGeneratorInterface
- Crear excepción específica:
Exception/{ProviderName}Exception.php
- Reutilizar
PhrasePrompt::SYSTEM y PhrasePrompt::USER_TEMPLATE
- Añadir caso al
match() en PhraseGeneratorFactory
- Añadir env vars en
.env.example y docker-compose.yml
- Registrar en
config/packages/services.yaml si necesita argumentos DI
Patron de cada provider:
final class NuevoProvider implements PhraseGeneratorInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly string $apiUrl,
private readonly string $apiKey,
private readonly string $model,
private readonly float $temperature,
private readonly int $maxTokens,
private readonly int $timeout,
) {}
public function generate(PictogramSequence $sequence, array $labels): array
{
}
}
Security Checklist
Variables de Entorno
PHRASE_PROVIDER=fake
OPENAI_API_URL=https://api.groq.com/openai/v1/chat/completions
OPENAI_API_KEY=gsk_...
OPENAI_MODEL=openai/gpt-oss-120b
GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models
GEMINI_API_KEY=AIza...
GEMINI_MODEL=gemini-2.5-flash
PHRASE_TEMPERATURE=0.7
PHRASE_MAX_TOKENS=2048
PHRASE_TIMEOUT=10
Errores Comunes
1. Provider no definido en .env
Problema: PHRASE_PROVIDER vacío o no definido.
Solución: El factory hace fallback a fake. En producción, asegurar que está definido explícitamente.
2. Timeout en LLM
Problema: Request tarda más del timeout configurado.
Solución: Configurar timeout adecuado al provider + cache de frases generadas para sequences repetidas.
3. Prompt injection vía labels
Problema: Labels maliciosos pueden inyectar instrucciones al LLM.
Solución: Sanitizar con regex [^\p{L}\p{N}\s\-] y limitar a 50 chars (ya implementado en cada provider).