- What this guide covers:
- ✓ Installing the OpenAI PHP package in Laravel
- ✓ Chat completions with conversation history
- ✓ Streaming responses to the browser in real time
- ✓ Running AI calls as queued background jobs
- ✓ Embeddings and semantic search (RAG pattern)
- ✓ Caching, rate limiting, and cost control
Prerequisites
You'll need a Laravel 10 or 11 project, PHP 8.1+, Composer, an OpenAI API key (from platform.openai.com), and optionally Redis for caching and queues. Basic Laravel knowledge is assumed.
Step 1: Install the OpenAI Laravel Package
The openai-php/laravel package wraps the official OpenAI PHP client with Laravel service provider integration:
composer require openai-php/laravel
Publish the config file:
php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"
Add your API key to .env:
OPENAI_API_KEY=sk-your-key-here OPENAI_REQUEST_TIMEOUT=60
The package auto-discovers via Laravel's package discovery — no manual service provider registration needed.
Step 2: Basic Chat Completion
Inject the OpenAI client via the facade or dependency injection. Here's a simple controller method:
use OpenAI\Laravel\Facades\OpenAI;
class AiController extends Controller
{
public function chat(Request $request)
{
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'You are a helpful business assistant.'],
['role' => 'user', 'content' => $request->input('message')],
],
'max_tokens' => 500,
'temperature' => 0.7,
]);
return response()->json([
'reply' => $response->choices[0]->message->content,
'tokens_used' => $response->usage->totalTokens,
]);
}
}Register the route in routes/api.php:
Route::post('/chat', [AiController::class, 'chat'])->middleware('auth:sanctum');Step 3: Maintain Conversation History
For a stateful chat experience, store message history in the user's session or a database table. Here's a session-based approach:
public function chat(Request $request)
{
$history = session('chat_history', [
['role' => 'system', 'content' => 'You are a helpful assistant for our SaaS platform.']
]);
$history[] = ['role' => 'user', 'content' => $request->input('message')];
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => $history,
'max_tokens' => 500,
]);
$reply = $response->choices[0]->message->content;
$history[] = ['role' => 'assistant', 'content' => $reply];
// Keep last 20 messages to control token cost
if (count($history) > 21) {
$history = [$history[0], ...array_slice($history, -20)];
}
session(['chat_history' => $history]);
return response()->json(['reply' => $reply]);
}Step 4: Stream Responses to the Browser
Streaming makes long AI responses feel instant — the user sees text as it generates rather than waiting for the complete response. Use Laravel's StreamedResponse:
use Symfony\Component\HttpFoundation\StreamedResponse;
public function stream(Request $request): StreamedResponse
{
return response()->stream(function () use ($request) {
$stream = OpenAI::chat()->createStreamed([
'model' => 'gpt-4o',
'messages' => [
['role' => 'user', 'content' => $request->input('message')]
],
]);
foreach ($stream as $response) {
$text = $response->choices[0]->delta->content ?? '';
if ($text !== '') {
echo "data: " . json_encode(['text' => $text]) . "\n\n";
ob_flush();
flush();
}
}
echo "data: [DONE]\n\n";
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no', // Disable Nginx buffering
]);
}On the frontend, consume with the EventSource API or a fetch stream reader. The X-Accel-Buffering: no header is important if your app sits behind Nginx — without it, Nginx buffers the response and streaming appears broken.
Step 5: Queue Long AI Jobs
For AI tasks that don't need real-time responses — document analysis, batch classification, email drafting — dispatch to a Laravel queue job to avoid HTTP timeouts and improve UX:
// app/Jobs/GenerateAiSummary.php
class GenerateAiSummary implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public function __construct(
private readonly int $documentId,
private readonly int $userId
) {}
public function handle(): void
{
$document = Document::findOrFail($this->documentId)
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'Summarize the following document in 3 bullet points.'],
['role' => 'user', 'content' => $document->content],
],
'max_tokens' => 300,
]);
$document->update([
'ai_summary' => $response->choices[0]->message->content,
'summarized_at' => now(),
]);
// Notify user via broadcast or notification
$this->userId && User::find($this->userId)?->notify(new SummaryReady($document));
}
}
// Dispatch from controller
GenerateAiSummary::dispatch($document->id, auth()->id());Step 6: Embeddings and Semantic Search (RAG)
Retrieval-Augmented Generation lets your AI answer questions from your own data — documentation, knowledge base, CRM records. First, embed your documents:
// Store embeddings when a document is created/updated
public function embed(Document $document): void
{
$response = OpenAI::embeddings()->create([
'model' => 'text-embedding-3-small',
'input' => $document->content,
]);
$document->update([
'embedding' => json_encode($response->embeddings[0]->embedding)
]);
}At query time, embed the user's question and find the most similar documents using cosine similarity (or use pgvector in PostgreSQL for efficient vector search):
public function search(string $query): array
{
$queryEmbedding = OpenAI::embeddings()->create([
'model' => 'text-embedding-3-small',
'input' => $query,
])->embeddings[0]->embedding;
// Fetch top matches — with pgvector this is a single SQL query
$docs = Document::all()->sortByDesc(function ($doc) use ($queryEmbedding) {
return $this->cosineSimilarity($queryEmbedding, json_decode($doc->embedding, true))
})->take(3);
// Feed context to GPT
$context = $docs->pluck('content')->join("\n\n---\n\n");
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => "Answer using only this context:\n\n{$context}"],
['role' => 'user', 'content' => $query],
],
]);
return [
'answer' => $response->choices[0]->message->content,
'sources' => $docs->pluck('title'),
];
}Step 7: Caching and Cost Control
OpenAI API costs can grow quickly without discipline. These patterns keep costs predictable:
- Cache identical prompts: Use
Cache::remember()with a hash of the prompt as the key. Identical support questions get cached answers without hitting the API again. - Use gpt-4o-mini for simple tasks: At roughly 15× cheaper than gpt-4o, mini handles classification, sentiment, extraction, and short summaries perfectly. Reserve gpt-4o for complex reasoning.
- Log token usage per request: Store
$response->usage->totalTokensin a database table with user ID and timestamp. Query weekly to spot unexpected spikes. - Rate limit per user: Laravel's built-in rate limiter can cap AI requests per user per hour — prevents a single user burning your monthly budget.
// In RouteServiceProvider or routes/api.php
RateLimiter::for('ai', function (Request $request) {
return Limit::perHour(20)->by($request->user()?->id);
});
Route::middleware(['auth:sanctum', 'throttle:ai'])
->post('/chat', [AiController::class, 'chat']);Handling Errors Gracefully
OpenAI API calls fail — rate limits, timeouts, and model unavailability happen in production. Wrap calls in try/catch and return user-friendly messages:
try {
$response = OpenAI::chat()->create([...]);
} catch (\OpenAI\Exceptions\ErrorException $e) {
Log::error('OpenAI API error', ['message' => $e->getMessage()]);
return response()->json(['error' => 'AI is temporarily unavailable. Please try again.'], 503);
} catch (\OpenAI\Exceptions\TransporterException $e) {
Log::error('OpenAI timeout', ['message' => $e->getMessage()]);
return response()->json(['error' => 'Request timed out. Please try again.'], 504);
}Production Checklist
- API key stored in
.env, never committed to version control - Rate limiting per user enabled on AI routes
- Token usage logged per request for cost monitoring
- Long AI jobs dispatched to queue (not synchronous HTTP calls)
- Streaming enabled for real-time response endpoints
- Error handling covers API rate limits and timeouts
- Cache layer (Redis) for repeated or near-identical prompts
For more on embedding AI capabilities into existing CRM workflows, see our guide on AI-powered Perfex CRM features — many of the same patterns apply.
Need OpenAI integrated into your Laravel application?
Websicon builds production AI features for Laravel apps — chat interfaces, document analysis, semantic search, and AI-powered automation. Serving US & UK clients. Projects from $500 USD.
Get a free AI integration quote →Building something more complex — multi-agent AI, RAG over large document sets, or AI features inside a CRM? Websicon's AI integration team can architect and build the full solution.
Ready to get started?
Let Websicon build your perfect CRM solution
Free consultation · Projects from $500 USD · No long-term contracts · EST/PST/GMT timezone support
Get Free Consultation