Lo más valioso de este capítulo es lo poco llamativo que resulta.
Vamos a construir un endpoint que envía un prompt a un proveedor y devuelve lo que vuelve. No tiene ninguna astucia. Lo que tiene, en cambio, es una frontera: un único sitio donde se describe al proveedor, un único contrato con el que habla el resto de la aplicación, y un único conjunto de decisiones sobre credenciales, tiempos de espera y reintentos que toda función de IA futura hereda gratis.
Equivócate aquí y lo pagarás en cada capítulo que sigue. Acierta y la mayor parte de los capítulos restantes se vuelven pequeños.
Empecemos por el sitio equivocado, brevemente
Esto es lo que casi todo el mundo escribe primero, y merece la pena mirarlo con honestidad, porque no es una tontería: simplemente está sin terminar.
public function generate(Request $request): JsonResponse
{
$response = Http::withToken(config('services.openai.token'))
->post('https://api.provider.example/v1/chat/completions', [
'model' => 'some-model',
'messages' => [['role' => 'user', 'content' => $request->input('prompt')]],
]);
return response()->json([
'content' => $response->json('choices.0.message.content'),
]);
}
Esto funciona. En un buen día funciona siempre.
Lo que le falta es cada decisión que todavía no has tomado. No hay tiempo de espera, así que un proveedor lento retiene este worker hasta que PHP se rinde. No hay reintento, así que un solo 503 es un fallo visible para el usuario. No se comprueba que la credencial exista, así que una vacía se envía como un bearer vacío y vuelve como un error de autenticación que te culpa de algo que no hiciste. La URL del proveedor y la forma de su carga útil están soldadas a un controlador, así que el día que añadas un segundo proveedor —o el día que este cambie— estarás editando controladores.
Y, sobre todo: no hay dónde poner la siguiente decisión. Cuando aprendas algo sobre cómo llamar a este proveedor, esta forma no te ofrece ningún sitio donde anotarlo.
La forma que sobrevive
El patrón es el que Laravel ya usa para cada subsistema intercambiable que posee: caché, colas, sistema de ficheros, correo. Un contrato que dice cuál es la capacidad, un driver por proveedor que la implementa y un gestor que construye drivers a partir de la configuración.
Tres ficheros.
app/Services/Ai/AiContract.php qué puede hacer un proveedor de IA
app/Services/Ai/AiManager.php construye y configura drivers
app/Http/Integrations/Ai/OpenaiAPI.php un proveedor
El contrato va primero, y debe describir las necesidades de tu aplicación, no la superficie de la API del proveedor.
interface AiContract
{
/**
* Generate structured site content as a decoded array.
*
* @return array<string, mixed>
*/
public function getSiteContent(string $model, string $prompt): array;
/**
* Generate a single block of text, capped at maxTokens.
*/
public function getContent(string $model, string $prompt, int $maxTokens): string;
}
Fíjate en lo que no está ahí. No hay array messages, ni response_format, ni temperature. Ese es el vocabulario de un proveedor concreto. Si se filtra al contrato, el contrato no es una abstracción: es un cambio de nombre.
La prueba es sencilla: ¿podrías implementar esta interfaz contra un proveedor completamente distinto sin tocar ni un solo llamante? Si la respuesta es sí, la frontera está en el sitio correcto.