n4nAI

Laravel's HTTP facade for LLM API integration

A hands-on tutorial for PHP engineers: use Laravel's HTTP facade to integrate LLM APIs with clean code, error handling, and OpenAI-compatible endpoints.

n4n Team3 min read703 words

Audio narration

Coming soon — every post will get a voice note here.

The Laravel HTTP facade hides Guzzle’s verbosity behind a clean fluent interface, which makes it a solid fit for calling an OpenAI-compatible endpoint. In this tutorial we’ll build a small service that uses the laravel http facade llm api pattern to send chat completions, parse responses, and handle failures without pulling in a heavyweight SDK. You’ll end up with a reusable class, a console command, and tests that run offline.

Prerequisites

  • PHP 8.1 or newer (Laravel 10/11 requires it)
  • A fresh or existing Laravel 10+ project
  • Composer for dependency management
  • An API key from an OpenAI-compatible provider (OpenAI, or a gateway such as n4n.ai)
  • Comfort with Laravel service providers, env config, and artisan commands

If you need a scratch project:

composer create-project laravel/laravel llm-demo
cd llm-demo

Why the HTTP facade instead of an SDK

Official Python and JS SDKs are common, but PHP teams often regret adding a vendor package that lags behind the API. The laravel http facade llm api approach gives you direct control over the request shape, headers, and retry policy. You are one minor version away from any new model parameter because you are just sending JSON to a documented endpoint.

The facade is backed by Guzzle, so you keep connection pooling, middleware, and timeout control. You lose auto-generated types, but in PHP that is a small price for avoiding a bloated dependency tree.

Configuring credentials

Never hardcode secrets. Put them in .env:

LLM_API_KEY=sk-your-key-here
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o-mini

Expose them through config/services.php so they are parsed once and cached in production:

'llm' => [
    'key' => env('LLM_API_KEY'),
    'base_url' => env('LLM_BASE_URL', 'https://api.openai.com/v1'),
    'model' => env('LLM_MODEL', 'gpt-4o-mini'),
],

Reading from config keeps the transport layer decoupled from business logic. The laravel http facade llm api calls will reference config('services.llm.*') rather than env() directly, which is the Laravel-recommended practice.

Building the service class

Create app/Services/LlmClient.php. We wrap the facade with a single chat() method and normalize errors.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class LlmClient
{
    public function __construct(
        protected string $baseUrl = '',
        protected string $apiKey = '',
        protected string $model = ''
    ) {
        $this->baseUrl = $baseUrl ?: config('services.llm.base_url');
        $this->apiKey = $apiKey ?: config('services.llm.key');
        $this->model = $model ?: config('services.llm.model');
    }

    public function chat(array $messages, float $temperature = 0.7): string
    {
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->apiKey,
            'Content-Type' => 'application/json',
        ])
        ->timeout(30)
        ->post($this->baseUrl . '/chat/completions', [
            'model' => $this->model,
            'messages' => $messages,
            'temperature' => $temperature,
        ]);

        if ($response->failed()) {
            throw new RuntimeException('LLM request failed: ' . $response->body());
        }

        $data = $response->json();
        return $data['choices'][0]['message']['content'] ?? '';
    }
}

This is the core of the laravel http facade llm api integration: a single POST to /chat/completions with a bearer token. The method returns the assistant message content as a plain string, which is what most callers want.

Adding retries and backoff

Networks flap and providers rate-limit. The facade supports retry() natively, so we extend the method:

$response = Http::withHeaders([/* ... */])
    ->timeout(30)
    ->retry(3, 500, throw: false)
    ->post($this->baseUrl . '/chat/completions', $payload);

if ($response->failed()) {
    throw new RuntimeException('LLM request failed after retries: ' . $response->body());
}

For exponential backoff, pass a closure:

->retry(3, fn (int $attempt) => $attempt * 200)

The laravel http facade llm api pattern stays readable even as resilience grows. You are not writing loop boilerplate.

Making the first request from a command

Generate a console command to manually test:

php artisan make:command LlmChat

Edit app/Console/Commands/LlmChat.php:

<?php

namespace App\Console\Commands;

use App\Services\LlmClient;
use Illuminate\Console\Command;

class LlmChat extends Command
{
    protected $signature = 'llm:chat {prompt}';
    protected $description = 'Send a prompt to the LLM and print the reply';

    public function handle(LlmClient $client): int
    {
        $prompt = $this->argument('prompt');

        try {
            $reply = $client->chat([
                ['role' => 'user', 'content' => $prompt]
            ]);
        } catch (\RuntimeException $e) {
            $this->error($e->getMessage());
            return self::FAILURE;
        }

        $this->info($reply);
        return self::SUCCESS;
    }
}

Run it:

php artisan llm:chat "Explain the Laravel HTTP facade in one sentence."

Expected output (model-dependent):

The Laravel HTTP facade provides a fluent, static interface to Guzzle for making HTTP requests without manually managing client instances.

Validating response shape

Providers occasionally return empty choices or malformed JSON. Defensive access avoids undefined index notices:

$data = $response->json();

if (!isset($data['choices'][0]['message']['content'])) {
    throw new RuntimeException('Unexpected LLM response: ' . json_encode($data));
}

For production, map into a small DTO to capture usage metadata for cost tracking:

public function chatWithUsage(array $messages): array
{
    // ... HTTP call ...
    return [
        'content' => $data['choices'][0]['message']['content'],
        'model' => $data['model'],
        'prompt_tokens' => $data['usage']['prompt_tokens'] ?? 0,
        'completion_tokens' => $data['usage']['completion_tokens'] ?? 0,
    ];
}

Per-token metering matters if you bill customers; the OpenAI-compatible response includes usage by default.

Using a gateway with automatic fallback

When you point the same request shape at an OpenAI-compatible inference gateway such as n4n.ai, you get automatic fallback across providers when one is rate-limited or degraded. That means your chat() method doesn’t need custom multi-provider logic—the gateway returns a standard completion response, and your existing parsing code works unchanged. Set LLM_BASE_URL to the gateway’s endpoint and keep the rest of the code identical.

Testing without network calls

Laravel’s HTTP fake prevents real calls in tests:

use Illuminate\Support\Facades\Http;

Http::fake([
    '*/chat/completions' => Http::response([
        'choices' => [['message' => ['content' => 'fake reply']]],
        'model' => 'gpt-4o-mini',
        'usage' => ['prompt_tokens' => 5, 'completion_tokens' => 2],
    ], 200),
]);

$client = new LlmClient();
$result = $client->chatWithUsage([['role' => 'user', 'content' => 'test']]);
$this->assertSame('fake reply', $result['content']);

This keeps CI fast and free of API spend. You can also assert the request payload with Http::assertSent().

Handling concurrent requests

If you need to fan out multiple prompts, the facade works inside Parallel or just sequential calls. For true concurrency, use Http::pool():

$responses = Http::pool(fn ($pool) => [
    $pool->post($url, $payload1),
    $pool->post($url, $payload2),
]);

But note that each request shares the same timeout and retry config only if you set them inside the pool closure. The laravel http facade llm api pattern scales to batch jobs without a separate async library.

When to avoid this pattern

If you need SSE streaming, function calling with strict schemas, or websocket transport, the raw facade becomes cumbersome. At that point, a thin community package or a generated OpenAPI client may save time. For standard chat and completion calls, the facade is enough.

Wrapping up

The laravel http facade llm api approach gives you full control over headers, timeouts, and retries without a vendor SDK. You can swap providers by changing env vars, and the fluent interface keeps the client code small. For multi-provider resilience, route through a compatible gateway; otherwise, the pattern above is enough to ship a maintainable integration.

Tagslaravelhttp-facadellm-integrationphp

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All php & laravel llm integration posts →