Ir al contenido principal
Laravel, thinking fast.
Capitulo 1 · Tu primer endpoint de IA

Un driver, con todas las rarezas del proveedor

Julian Beaujardin

El driver es donde vive la API real del proveedor, y es el único sitio con permiso para conocerla.

final class OpenaiAPI implements AiContract
{
    use SendsRequests;

    public function __construct(
        protected PendingRequest $request
    ) {}

    /**
     * @return array<string, mixed>
     */
    public function getSiteContent(string $model, string $prompt): array
    {
        $data = $this->send('POST', '/chat/completions', [
            'model' => $model,
            'messages' => [
                ['role' => 'user', 'content' => $prompt],
            ],
            'response_format' => ['type' => 'json_object'],
        ]);

        $content = (string) $data->json('choices.0.message.content');
        $result = json_decode($content, true);

        if (! is_array($result) || $result === []) {
            throw new \RuntimeException(
                'AI site content response could not be decoded into a non-empty array.'
            );
        }

        /** @var array<string, mixed> $result */
        return $result;
    }
}

Dos cosas aquí valen más de lo que parecen.

El driver recibe un PendingRequest ya configurado, no un token. No sabe de dónde salieron sus credenciales, cuánto dura su tiempo de espera ni cuál es su política de reintentos. Eso es tarea del gestor, y significa que el driver sigue siendo una capa de traducción fina y trivial de simular en una prueba.

La garantía del driver es de transporte, y ahí se detiene. Te promete JSON decodificable y no vacío. Deliberadamente no valida que ese JSON tenga las claves que tu función necesita, porque el driver no tiene ni idea de qué necesita tu función: esa comprobación va donde se conoce el contexto del llamante, y tiene capítulo propio. Mezclar ambas produce un driver que hay que editar cada vez que una función cambia de opinión.

Ese throw importa más de lo que parece. La alternativa —devolver null o [] ante basura— empuja una pregunta indemostrable («¿funcionó?») a cada llamante. Falla aquí, una vez, y en voz alta.

El gestor, donde viven las decisiones

El gestor extiende el Manager del propio Laravel, lo que trae gratis la resolución de drivers, su cacheo y los creadores personalizados. Lo que añades es configuración y política.

final class AiManager extends Manager
{
    public function getDefaultDriver(): string
    {
        return (string) Config::get('services.ai.default');
    }

    public function createOpenaiDriver(): AiContract
    {
        return $this->buildDriver(
            driverClass: OpenaiAPI::class,
            config: $this->getConfig('openai'),
        );
    }
}

Añadir un segundo proveedor es ahora genuinamente aditivo: escribes un driver, añades un método createXDriver() y añades un bloque de configuración. Ningún llamante cambia. Ningún controlador cambia.

Y buildDriver es donde cada llamada de IA de la aplicación hereda sus modales:

protected function buildDriver(string $driverClass, array $config = []): AiContract
{
    $this->ensureValidDriver($driverClass);

    $token = $config['token'] ?? '';

    if (! is_string($token) || $token === '') {
        throw new InvalidArgumentException(
            message: 'AI driver token is not configured; expected a per-tenant credential or an environment fallback.',
            code: Response::HTTP_INTERNAL_SERVER_ERROR,
        );
    }

    return new $driverClass(
        request: Http::baseUrl($config['url'] ?? '')
            ->withToken($token)
            ->timeout(Config::integer('services.ai.config.timeout', default: 60))
            ->retry(
                times: max(1, Config::integer('services.ai.config.retry_times', default: 3)),
                sleepMilliseconds: max(0, Config::integer('services.ai.config.retry_sleep_ms', default: 500)),
                when: fn (Throwable $e): bool => self::shouldRetry($e),
            )
            ->throw()
    );
}

Toda llamada de IA de la aplicación tiene ya un tiempo de espera, una política de reintentos y una credencial validada, y nada de ello se repite en ningún sitio.