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.