Integrating AI APIs in Laravel: OpenAI, Claude, Bedrock
Build AI features in Laravel with OpenAI, Claude, and AWS Bedrock. Complete guide covering content summarization, caching, and cost optimization.
After 14 years of PHP development, I have watched Laravel evolve through countless paradigm shifts. None has been as transformative as the current AI integration wave. What once required Python microservices or complex API wrappers is now achievable with elegant, Laravel-native solutions that feel right at home in your application architecture.
This guide walks you through integrating AI capabilities into Laravel applications using three major providers: OpenAI, Anthropic Claude, and AWS Bedrock. We will build a practical content summarization feature while covering the production essentials that most tutorials skip: proper error handling, retry logic, caching strategies, cost optimization, and security best practices. For developers using AI to enhance code quality, see my guide on AI-assisted code review for Laravel.
Choosing Your AI Provider
Before writing code, understand the trade-offs between providers. Each serves different use cases and budget constraints.
OpenAI
OpenAI remains the most popular choice for Laravel developers, offering excellent documentation and the most mature PHP ecosystem. The openai-php/laravel package provides a first-class Laravel experience with facades, configuration, and testing support.
Best for: General-purpose text generation, embeddings, image generation, and applications requiring the largest model selection.
Pricing (as of early 2026): GPT-4o at $2.50/1M input tokens, $10/1M output tokens. GPT-4o Mini at $0.15/1M input tokens, $0.60/1M output tokens.
Anthropic Claude
Claude excels at nuanced reasoning, following complex instructions, and maintaining context over long conversations. The models demonstrate strong performance in coding tasks and technical documentation.
Best for: Technical content analysis, code generation, long-form content, and applications requiring detailed instruction following.
Pricing: Claude 3.5 Sonnet at $3/1M input tokens, $15/1M output tokens. Claude 3.5 Haiku at $0.25/1M input tokens, $1.25/1M output tokens.
AWS Bedrock
Bedrock provides access to multiple foundation models through a unified AWS API, including Claude, Llama, and Amazon's Titan models. For teams already invested in AWS infrastructure, Bedrock simplifies deployment, security, and billing.
Best for: Enterprise environments with existing AWS infrastructure, applications requiring multiple model providers, and teams needing SOC2/HIPAA compliance through AWS.
Pricing: Pay-per-use based on the underlying model, with on-demand and provisioned throughput options.
Setting Up AI Clients in Laravel
We will explore two approaches: provider-specific packages for maximum control, and Prism for a unified multi-provider interface.
Approach 1: Provider-Specific Packages
OpenAI Setup
Install the official Laravel package:
composer require openai-php/laravel
php artisan openai:install
Configure your API key in .env:
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_ORGANIZATION=org-optional-org-id
Basic usage with the facade:
<?php
use OpenAI\Laravel\Facades\OpenAI;
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'Summarize this article in 3 bullet points.'],
],
'max_tokens' => 500,
'temperature' => 0.7,
]);
$summary = $response->choices[0]->message->content;
Claude Setup
Install the Anthropic Laravel package:
composer require mozex/anthropic-laravel
php artisan vendor:publish --tag=anthropic-config
Configure in .env:
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
Basic usage:
<?php
use Anthropic\Laravel\Facades\Anthropic;
$response = Anthropic::messages()->create([
'model' => 'claude-sonnet-4-20250514',
'max_tokens' => 1024,
'messages' => [
[
'role' => 'user',
'content' => 'Summarize the key points of this technical documentation.',
],
],
]);
$summary = $response->content[0]->text;
AWS Bedrock Setup
Bedrock requires the AWS SDK and proper IAM configuration:
composer require aws/aws-sdk-php
Configure AWS credentials in .env:
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_DEFAULT_REGION=us-east-1
Basic Bedrock client setup:
<?php
namespace App\Services;
use Aws\BedrockRuntime\BedrockRuntimeClient;
use Aws\Exception\AwsException;
class BedrockService
{
private BedrockRuntimeClient $client;
public function __construct()
{
$this->client = new BedrockRuntimeClient([
'region' => config('services.aws.region', 'us-east-1'),
'version' => 'latest',
'credentials' => [
'key' => config('services.aws.key'),
'secret' => config('services.aws.secret'),
],
]);
}
public function invokeClaudeModel(string $prompt, string $model = 'anthropic.claude-3-5-sonnet-20241022-v2:0'): string
{
$body = json_encode([
'anthropic_version' => 'bedrock-2023-05-31',
'max_tokens' => 1024,
'messages' => [
[
'role' => 'user',
'content' => $prompt,
],
],
]);
try {
$response = $this->client->invokeModel([
'modelId' => $model,
'contentType' => 'application/json',
'accept' => 'application/json',
'body' => $body,
]);
$result = json_decode($response['body']->getContents(), true);
return $result['content'][0]['text'];
} catch (AwsException $e) {
throw new \RuntimeException(
'Bedrock API error: ' . $e->getAwsErrorMessage(),
$e->getCode(),
$e
);
}
}
}
Approach 2: Unified Interface with Prism
Prism provides a Laravel-native abstraction over multiple AI providers, making it easy to switch providers without changing application code.
composer require prism-php/prism
php artisan vendor:publish --tag=prism-config
For AWS Bedrock support, add the Bedrock provider:
composer require prism-php/bedrock
Configure providers in config/prism.php:
<?php
return [
'default' => env('PRISM_PROVIDER', 'openai'),
'providers' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'organization' => env('OPENAI_ORGANIZATION'),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
],
],
];
Prism's fluent interface simplifies AI interactions:
<?php
use Prism\Prism\Prism;
// Using OpenAI
$response = Prism::text()
->using('openai', 'gpt-4o')
->withSystemPrompt('You are a technical content summarizer.')
->withPrompt('Summarize the following article in 3 concise bullet points.')
->generate();
$summary = $response->text;
// Switch to Claude with a single line change
$response = Prism::text()
->using('anthropic', 'claude-sonnet-4-20250514')
->withSystemPrompt('You are a technical content summarizer.')
->withPrompt('Summarize the following article in 3 concise bullet points.')
->generate();
Building a Content Summarization Feature
Let us build a production-ready content summarization service that demonstrates proper architecture, error handling, and caching.
Service Layer Architecture
Create an AI service that abstracts provider details:
<?php
namespace App\Services\AI;
use App\Contracts\AIProviderInterface;
use App\Exceptions\AIServiceException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ContentSummarizationService
{
private const CACHE_TTL = 86400; // 24 hours
private const MAX_RETRIES = 3;
private const RETRY_DELAY_MS = 1000;
public function __construct(
private AIProviderInterface $aiProvider,
) {}
/**
* Summarize content with caching and retry logic
*
* @throws AIServiceException
*/
public function summarize(
string $content,
int $maxBulletPoints = 5,
string $style = 'technical'
): array {
// Generate cache key based on content hash
$cacheKey = $this->generateCacheKey($content, $maxBulletPoints, $style);
// Return cached result if available
if ($cached = Cache::get($cacheKey)) {
Log::debug('AI summarization cache hit', ['key' => $cacheKey]);
return $cached;
}
// Build the prompt
$prompt = $this->buildPrompt($content, $maxBulletPoints, $style);
// Execute with retry logic
$result = $this->executeWithRetry(fn () => $this->aiProvider->complete($prompt));
// Parse and validate the response
$summary = $this->parseResponse($result);
// Cache the successful result
Cache::put($cacheKey, $summary, self::CACHE_TTL);
return $summary;
}
private function generateCacheKey(string $content, int $bullets, string $style): string
{
$contentHash = hash('xxh3', $content);
return "ai:summary:{$contentHash}:{$bullets}:{$style}";
}
private function buildPrompt(string $content, int $maxBulletPoints, string $style): string
{
$styleInstructions = match ($style) {
'technical' => 'Use precise technical terminology. Focus on implementation details and specifications.',
'executive' => 'Focus on business impact, key decisions, and outcomes. Avoid technical jargon.',
'casual' => 'Use conversational language. Make the content accessible to a general audience.',
default => 'Provide a balanced summary suitable for a general technical audience.',
};
return <<<PROMPT
Analyze the following content and provide a summary.
Instructions:
- Create exactly {$maxBulletPoints} bullet points
- {$styleInstructions}
- Each bullet should be a complete, standalone insight
- Focus on the most important and actionable information
- Return ONLY the bullet points, one per line, starting with "- "
Content to summarize:
{$content}
PROMPT;
}
/**
* Execute a callback with exponential backoff retry
*/
private function executeWithRetry(callable $callback): string
{
$lastException = null;
for ($attempt = 1; $attempt <= self::MAX_RETRIES; $attempt++) {
try {
return $callback();
} catch (\Exception $e) {
$lastException = $e;
Log::warning('AI API call failed', [
'attempt' => $attempt,
'max_retries' => self::MAX_RETRIES,
'error' => $e->getMessage(),
]);
if ($attempt < self::MAX_RETRIES) {
// Exponential backoff: 1s, 2s, 4s
$delayMs = self::RETRY_DELAY_MS * pow(2, $attempt - 1);
usleep($delayMs * 1000);
}
}
}
throw new AIServiceException(
"AI service unavailable after " . self::MAX_RETRIES . " attempts",
previous: $lastException
);
}
private function parseResponse(string $response): array
{
$lines = explode("\n", trim($response));
$bulletPoints = [];
foreach ($lines as $line) {
$line = trim($line);
if (str_starts_with($line, '- ')) {
$bulletPoints[] = substr($line, 2);
} elseif (str_starts_with($line, '* ')) {
$bulletPoints[] = substr($line, 2);
}
}
if (empty($bulletPoints)) {
throw new AIServiceException('Failed to parse AI response into bullet points');
}
return [
'bullet_points' => $bulletPoints,
'generated_at' => now()->toISOString(),
'model' => $this->aiProvider->getModelIdentifier(),
];
}
}
Provider Interface and Implementations
Define a contract for AI providers:
<?php
namespace App\Contracts;
interface AIProviderInterface
{
/**
* Send a completion request to the AI provider
*/
public function complete(string $prompt, array $options = []): string;
/**
* Get the model identifier being used
*/
public function getModelIdentifier(): string;
/**
* Estimate token count for a prompt
*/
public function estimateTokens(string $text): int;
}
OpenAI implementation:
<?php
namespace App\Services\AI\Providers;
use App\Contracts\AIProviderInterface;
use App\Exceptions\AIServiceException;
use OpenAI\Laravel\Facades\OpenAI;
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\TransporterException;
class OpenAIProvider implements AIProviderInterface
{
public function __construct(
private string $model = 'gpt-4o',
private int $maxTokens = 1024,
private float $temperature = 0.7,
) {}
public function complete(string $prompt, array $options = []): string
{
try {
$response = OpenAI::chat()->create([
'model' => $options['model'] ?? $this->model,
'messages' => [
[
'role' => 'system',
'content' => 'You are a precise content analyzer. Follow instructions exactly.',
],
[
'role' => 'user',
'content' => $prompt,
],
],
'max_tokens' => $options['max_tokens'] ?? $this->maxTokens,
'temperature' => $options['temperature'] ?? $this->temperature,
]);
return $response->choices[0]->message->content;
} catch (ErrorException $e) {
throw new AIServiceException(
"OpenAI API error: {$e->getMessage()}",
$e->getCode(),
$e
);
} catch (TransporterException $e) {
throw new AIServiceException(
"OpenAI connection error: {$e->getMessage()}",
$e->getCode(),
$e
);
}
}
public function getModelIdentifier(): string
{
return "openai:{$this->model}";
}
public function estimateTokens(string $text): int
{
// Rough estimation: ~4 characters per token for English text
return (int) ceil(strlen($text) / 4);
}
}
Claude implementation:
<?php
namespace App\Services\AI\Providers;
use App\Contracts\AIProviderInterface;
use App\Exceptions\AIServiceException;
use Anthropic\Laravel\Facades\Anthropic;
class ClaudeProvider implements AIProviderInterface
{
public function __construct(
private string $model = 'claude-sonnet-4-20250514',
private int $maxTokens = 1024,
private float $temperature = 0.7,
) {}
public function complete(string $prompt, array $options = []): string
{
try {
$response = Anthropic::messages()->create([
'model' => $options['model'] ?? $this->model,
'max_tokens' => $options['max_tokens'] ?? $this->maxTokens,
'messages' => [
[
'role' => 'user',
'content' => $prompt,
],
],
]);
return $response->content[0]->text;
} catch (\Exception $e) {
throw new AIServiceException(
"Claude API error: {$e->getMessage()}",
$e->getCode(),
$e
);
}
}
public function getModelIdentifier(): string
{
return "anthropic:{$this->model}";
}
public function estimateTokens(string $text): int
{
// Claude uses similar tokenization to GPT models
return (int) ceil(strlen($text) / 4);
}
}
Service Provider Registration
Register the AI service with dependency injection:
<?php
namespace App\Providers;
use App\Contracts\AIProviderInterface;
use App\Services\AI\ContentSummarizationService;
use App\Services\AI\Providers\ClaudeProvider;
use App\Services\AI\Providers\OpenAIProvider;
use Illuminate\Support\ServiceProvider;
class AIServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register the AI provider based on configuration
$this->app->singleton(AIProviderInterface::class, function ($app) {
$provider = config('services.ai.default_provider', 'openai');
return match ($provider) {
'openai' => new OpenAIProvider(
model: config('services.ai.openai.model', 'gpt-4o'),
maxTokens: config('services.ai.openai.max_tokens', 1024),
),
'claude' => new ClaudeProvider(
model: config('services.ai.anthropic.model', 'claude-sonnet-4-20250514'),
maxTokens: config('services.ai.anthropic.max_tokens', 1024),
),
default => throw new \InvalidArgumentException("Unknown AI provider: {$provider}"),
};
});
// Register the summarization service
$this->app->singleton(ContentSummarizationService::class);
}
}
Controller Integration
Create an API endpoint for content summarization:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\SummarizeContentRequest;
use App\Services\AI\ContentSummarizationService;
use App\Exceptions\AIServiceException;
use Illuminate\Http\JsonResponse;
class ContentSummarizationController extends Controller
{
public function __construct(
private ContentSummarizationService $summarizationService,
) {}
public function summarize(SummarizeContentRequest $request): JsonResponse
{
try {
$summary = $this->summarizationService->summarize(
content: $request->validated('content'),
maxBulletPoints: $request->validated('bullet_points', 5),
style: $request->validated('style', 'technical'),
);
return response()->json([
'data' => $summary,
]);
} catch (AIServiceException $e) {
return response()->json([
'error' => [
'code' => 'AI_SERVICE_ERROR',
'message' => 'Unable to generate summary. Please try again later.',
],
], 503);
}
}
}
Request validation:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SummarizeContentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'content' => ['required', 'string', 'min:100', 'max:50000'],
'bullet_points' => ['sometimes', 'integer', 'min:1', 'max:10'],
'style' => ['sometimes', 'string', 'in:technical,executive,casual'],
];
}
public function messages(): array
{
return [
'content.min' => 'Content must be at least 100 characters for meaningful summarization.',
'content.max' => 'Content exceeds the maximum length of 50,000 characters.',
];
}
}
Error Handling and Retry Logic
Production AI integrations must handle the reality of API failures gracefully. The retry logic in our service handles transient errors, but we need comprehensive error classification.
Custom Exception Hierarchy
<?php
namespace App\Exceptions;
use Exception;
class AIServiceException extends Exception
{
public function __construct(
string $message,
int $code = 0,
?Exception $previous = null,
public readonly bool $isRetryable = true,
) {
parent::__construct($message, $code, $previous);
}
}
class AIRateLimitException extends AIServiceException
{
public function __construct(
string $message,
public readonly int $retryAfterSeconds = 60,
?Exception $previous = null,
) {
parent::__construct($message, 429, $previous, isRetryable: true);
}
}
class AIQuotaExceededException extends AIServiceException
{
public function __construct(string $message, ?Exception $previous = null)
{
parent::__construct($message, 402, $previous, isRetryable: false);
}
}
class AIInvalidRequestException extends AIServiceException
{
public function __construct(string $message, ?Exception $previous = null)
{
parent::__construct($message, 400, $previous, isRetryable: false);
}
}
Error Classification Middleware
<?php
namespace App\Services\AI\Middleware;
use App\Exceptions\AIRateLimitException;
use App\Exceptions\AIQuotaExceededException;
use App\Exceptions\AIInvalidRequestException;
use App\Exceptions\AIServiceException;
use Closure;
class ErrorClassificationMiddleware
{
public function handle(Closure $next)
{
try {
return $next();
} catch (\Exception $e) {
throw $this->classifyException($e);
}
}
private function classifyException(\Exception $e): AIServiceException
{
$message = $e->getMessage();
$code = $e->getCode();
// Rate limiting
if ($code === 429 || str_contains($message, 'rate limit')) {
return new AIRateLimitException(
'API rate limit exceeded. Please retry later.',
retryAfterSeconds: $this->extractRetryAfter($e),
previous: $e
);
}
// Quota/billing issues
if ($code === 402 || str_contains($message, 'quota') || str_contains($message, 'billing')) {
return new AIQuotaExceededException(
'API quota exceeded or billing issue.',
previous: $e
);
}
// Invalid requests
if ($code === 400 || str_contains($message, 'invalid')) {
return new AIInvalidRequestException(
'Invalid request to AI API: ' . $message,
previous: $e
);
}
// Default to retryable service exception
return new AIServiceException(
'AI service error: ' . $message,
$code,
$e
);
}
private function extractRetryAfter(\Exception $e): int
{
// Try to extract retry-after from exception or headers
// Default to 60 seconds
return 60;
}
}
Cost Management and Token Optimization
AI API costs can escalate quickly without proper management. Implement these strategies to control spending while maintaining quality.
Token Estimation and Limits
<?php
namespace App\Services\AI;
class TokenManager
{
// Approximate costs per 1M tokens (adjust based on current pricing)
private const COSTS = [
'gpt-4o' => ['input' => 2.50, 'output' => 10.00],
'gpt-4o-mini' => ['input' => 0.15, 'output' => 0.60],
'claude-sonnet-4-20250514' => ['input' => 3.00, 'output' => 15.00],
'claude-3-5-haiku-20241022' => ['input' => 0.25, 'output' => 1.25],
];
/**
* Estimate token count for text
* More accurate estimation using character-based heuristics
*/
public function estimateTokens(string $text): int
{
// Average of 4 characters per token for English
// Code and technical content often have more tokens per character
$charCount = strlen($text);
$wordCount = str_word_count($text);
// Hybrid estimation: average of character-based and word-based
$charBasedEstimate = $charCount / 4;
$wordBasedEstimate = $wordCount * 1.3;
return (int) ceil(($charBasedEstimate + $wordBasedEstimate) / 2);
}
/**
* Estimate cost for a request
*/
public function estimateCost(string $model, int $inputTokens, int $outputTokens): float
{
$costs = self::COSTS[$model] ?? self::COSTS['gpt-4o-mini'];
$inputCost = ($inputTokens / 1_000_000) * $costs['input'];
$outputCost = ($outputTokens / 1_000_000) * $costs['output'];
return round($inputCost + $outputCost, 6);
}
/**
* Check if request is within budget
*/
public function checkBudget(string $userId, float $estimatedCost): bool
{
$dailyBudget = config('services.ai.daily_budget_per_user', 1.00);
$currentSpend = $this->getUserDailySpend($userId);
return ($currentSpend + $estimatedCost) <= $dailyBudget;
}
/**
* Track usage for billing and monitoring
*/
public function trackUsage(
string $userId,
string $model,
int $inputTokens,
int $outputTokens,
float $cost
): void {
\DB::table('ai_usage_logs')->insert([
'user_id' => $userId,
'model' => $model,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'cost' => $cost,
'created_at' => now(),
]);
}
private function getUserDailySpend(string $userId): float
{
return (float) \DB::table('ai_usage_logs')
->where('user_id', $userId)
->whereDate('created_at', today())
->sum('cost');
}
}
Model Cascading Strategy
Route requests to appropriate models based on complexity:
<?php
namespace App\Services\AI;
use App\Contracts\AIProviderInterface;
class ModelCascadeService
{
public function __construct(
private AIProviderInterface $cheapProvider,
private AIProviderInterface $premiumProvider,
private TokenManager $tokenManager,
) {}
/**
* Use cheap model for simple tasks, premium for complex ones
*/
public function complete(string $prompt, array $options = []): string
{
$tokenCount = $this->tokenManager->estimateTokens($prompt);
$complexity = $this->assessComplexity($prompt);
// Use cheap model for:
// - Short prompts (< 1000 tokens)
// - Low complexity tasks
// - When explicitly requested
if ($tokenCount < 1000 && $complexity < 0.6) {
try {
return $this->cheapProvider->complete($prompt, $options);
} catch (\Exception $e) {
// Fall through to premium on failure
}
}
return $this->premiumProvider->complete($prompt, $options);
}
/**
* Assess prompt complexity (0-1 scale)
*/
private function assessComplexity(string $prompt): float
{
$score = 0.5; // Base complexity
// Increase for analytical keywords
$analyticalKeywords = ['analyze', 'compare', 'evaluate', 'synthesize', 'critique'];
foreach ($analyticalKeywords as $keyword) {
if (stripos($prompt, $keyword) !== false) {
$score += 0.1;
}
}
// Increase for code-related content
if (preg_match('/```|\bfunction\b|\bclass\b|\bdef\b/', $prompt)) {
$score += 0.15;
}
// Increase for long content
if (strlen($prompt) > 10000) {
$score += 0.1;
}
return min(1.0, $score);
}
}
Prompt Optimization
Reduce token usage through efficient prompt design:
<?php
namespace App\Services\AI;
class PromptOptimizer
{
/**
* Compress prompt while preserving meaning
*/
public function optimize(string $prompt): string
{
// Remove excessive whitespace
$prompt = preg_replace('/\s+/', ' ', $prompt);
// Remove redundant phrases
$redundantPhrases = [
'please ' => '',
'I would like you to ' => '',
'Can you please ' => '',
'It would be great if you could ' => '',
];
$prompt = str_ireplace(
array_keys($redundantPhrases),
array_values($redundantPhrases),
$prompt
);
return trim($prompt);
}
/**
* Truncate content to fit within token limits
*/
public function truncateToLimit(string $content, int $maxTokens): string
{
$estimatedTokens = (int) ceil(strlen($content) / 4);
if ($estimatedTokens <= $maxTokens) {
return $content;
}
// Calculate approximate character limit
$maxChars = $maxTokens * 4;
// Truncate at sentence boundary
$truncated = substr($content, 0, $maxChars);
$lastSentence = strrpos($truncated, '. ');
if ($lastSentence !== false && $lastSentence > $maxChars * 0.8) {
$truncated = substr($truncated, 0, $lastSentence + 1);
}
return $truncated . "\n\n[Content truncated for length]";
}
}
Caching AI Responses
Effective caching dramatically reduces costs and improves response times. Implement a multi-tier caching strategy.
Semantic Cache Implementation
<?php
namespace App\Services\AI;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class AIResponseCache
{
private const CACHE_PREFIX = 'ai:response:';
private const DEFAULT_TTL = 86400; // 24 hours
/**
* Get cached response or execute callback
*/
public function remember(
string $prompt,
array $options,
callable $callback,
?int $ttl = null
): string {
$cacheKey = $this->generateKey($prompt, $options);
$ttl = $ttl ?? self::DEFAULT_TTL;
// Check cache first
$cached = Cache::get($cacheKey);
if ($cached !== null) {
$this->recordCacheHit($cacheKey);
return $cached;
}
// Execute callback and cache result
$result = $callback();
Cache::put($cacheKey, $result, $ttl);
$this->recordCacheMiss($cacheKey);
return $result;
}
/**
* Generate deterministic cache key
*/
private function generateKey(string $prompt, array $options): string
{
// Normalize options for consistent caching
ksort($options);
$hashInput = json_encode([
'prompt' => $this->normalizePrompt($prompt),
'options' => $options,
]);
return self::CACHE_PREFIX . hash('xxh128', $hashInput);
}
/**
* Normalize prompt for better cache hit rate
*/
private function normalizePrompt(string $prompt): string
{
// Lowercase
$prompt = strtolower($prompt);
// Normalize whitespace
$prompt = preg_replace('/\s+/', ' ', $prompt);
// Trim
return trim($prompt);
}
/**
* Invalidate cache entries matching a pattern
*/
public function invalidatePattern(string $pattern): int
{
$keys = Redis::keys(self::CACHE_PREFIX . $pattern . '*');
$count = count($keys);
if ($count > 0) {
Redis::del($keys);
}
return $count;
}
/**
* Get cache statistics
*/
public function getStats(): array
{
return [
'hits' => (int) Redis::get('ai:cache:hits') ?? 0,
'misses' => (int) Redis::get('ai:cache:misses') ?? 0,
'hit_rate' => $this->calculateHitRate(),
];
}
private function recordCacheHit(string $key): void
{
Redis::incr('ai:cache:hits');
}
private function recordCacheMiss(string $key): void
{
Redis::incr('ai:cache:misses');
}
private function calculateHitRate(): float
{
$hits = (int) Redis::get('ai:cache:hits') ?? 0;
$misses = (int) Redis::get('ai:cache:misses') ?? 0;
$total = $hits + $misses;
return $total > 0 ? round($hits / $total, 4) : 0.0;
}
}
Cache-Aware Service Wrapper
<?php
namespace App\Services\AI;
use App\Contracts\AIProviderInterface;
class CachedAIProvider implements AIProviderInterface
{
public function __construct(
private AIProviderInterface $provider,
private AIResponseCache $cache,
private int $defaultTtl = 86400,
) {}
public function complete(string $prompt, array $options = []): string
{
// Skip cache for certain options
if ($options['no_cache'] ?? false) {
return $this->provider->complete($prompt, $options);
}
return $this->cache->remember(
$prompt,
$options,
fn () => $this->provider->complete($prompt, $options),
$options['cache_ttl'] ?? $this->defaultTtl
);
}
public function getModelIdentifier(): string
{
return $this->provider->getModelIdentifier();
}
public function estimateTokens(string $text): int
{
return $this->provider->estimateTokens($text);
}
}
Security Considerations
Protecting API keys and preventing abuse requires multiple layers of security.
Secure Configuration
Store sensitive keys properly in your .env file and never commit them:
<?php
// config/services.php
return [
'ai' => [
'default_provider' => env('AI_DEFAULT_PROVIDER', 'openai'),
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'organization' => env('OPENAI_ORGANIZATION'),
'model' => env('OPENAI_MODEL', 'gpt-4o'),
'max_tokens' => (int) env('OPENAI_MAX_TOKENS', 1024),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL', 'claude-sonnet-4-20250514'),
'max_tokens' => (int) env('ANTHROPIC_MAX_TOKENS', 1024),
],
'bedrock' => [
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'model' => env('BEDROCK_MODEL', 'anthropic.claude-3-5-sonnet-20241022-v2:0'),
],
// Cost controls
'daily_budget_per_user' => (float) env('AI_DAILY_BUDGET_PER_USER', 1.00),
'max_request_tokens' => (int) env('AI_MAX_REQUEST_TOKENS', 10000),
],
];
Rate Limiting for AI Endpoints
Apply stricter rate limits to AI endpoints to prevent abuse:
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Strict rate limiting for AI endpoints
RateLimiter::for('ai', function (Request $request) {
$user = $request->user();
if (!$user) {
return Limit::perMinute(5)->by($request->ip());
}
// Tiered limits based on subscription
return match ($user->subscription_tier) {
'enterprise' => Limit::perMinute(60)->by($user->id),
'professional' => Limit::perMinute(30)->by($user->id),
'starter' => Limit::perMinute(10)->by($user->id),
default => Limit::perMinute(5)->by($user->id),
};
});
}
}
Input Validation and Sanitization
Protect against prompt injection and malicious input:
<?php
namespace App\Services\AI;
class InputSanitizer
{
private const MAX_INPUT_LENGTH = 50000;
private const BLOCKED_PATTERNS = [
'/ignore\s+(all\s+)?previous\s+instructions/i',
'/disregard\s+(all\s+)?above/i',
'/you\s+are\s+now\s+/i',
'/act\s+as\s+if\s+you\s+were/i',
];
/**
* Sanitize and validate user input for AI processing
*
* @throws \InvalidArgumentException
*/
public function sanitize(string $input): string
{
// Length check
if (strlen($input) > self::MAX_INPUT_LENGTH) {
throw new \InvalidArgumentException(
'Input exceeds maximum length of ' . self::MAX_INPUT_LENGTH . ' characters'
);
}
// Check for prompt injection patterns
foreach (self::BLOCKED_PATTERNS as $pattern) {
if (preg_match($pattern, $input)) {
throw new \InvalidArgumentException(
'Input contains blocked patterns'
);
}
}
// Remove potentially dangerous characters
$input = $this->removeControlCharacters($input);
return $input;
}
private function removeControlCharacters(string $input): string
{
// Remove ASCII control characters except newlines and tabs
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $input);
}
}
Audit Logging
Track all AI interactions for security and compliance:
<?php
namespace App\Services\AI;
use Illuminate\Support\Facades\Log;
class AIAuditLogger
{
public function logRequest(
string $userId,
string $provider,
string $model,
int $inputTokens,
array $metadata = []
): void {
Log::channel('ai_audit')->info('AI Request', [
'user_id' => $userId,
'provider' => $provider,
'model' => $model,
'input_tokens' => $inputTokens,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'metadata' => $metadata,
'timestamp' => now()->toISOString(),
]);
}
public function logResponse(
string $requestId,
int $outputTokens,
float $cost,
int $latencyMs
): void {
Log::channel('ai_audit')->info('AI Response', [
'request_id' => $requestId,
'output_tokens' => $outputTokens,
'cost' => $cost,
'latency_ms' => $latencyMs,
'timestamp' => now()->toISOString(),
]);
}
public function logError(
string $requestId,
string $errorType,
string $errorMessage
): void {
Log::channel('ai_audit')->error('AI Error', [
'request_id' => $requestId,
'error_type' => $errorType,
'error_message' => $errorMessage,
'timestamp' => now()->toISOString(),
]);
}
}
Testing AI Integrations
Proper testing ensures reliability without incurring API costs during development.
Mocking AI Providers
<?php
namespace Tests\Feature;
use App\Contracts\AIProviderInterface;
use App\Services\AI\ContentSummarizationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ContentSummarizationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_summarizes_content_into_bullet_points(): void
{
// Create mock provider
$mockProvider = \Mockery::mock(AIProviderInterface::class);
$mockProvider->shouldReceive('complete')
->once()
->andReturn("- First key point\n- Second key point\n- Third key point");
$mockProvider->shouldReceive('getModelIdentifier')
->andReturn('mock:test-model');
$this->app->instance(AIProviderInterface::class, $mockProvider);
$service = $this->app->make(ContentSummarizationService::class);
$result = $service->summarize(
'This is a long article about Laravel development...',
maxBulletPoints: 3
);
$this->assertCount(3, $result['bullet_points']);
$this->assertEquals('First key point', $result['bullet_points'][0]);
}
/** @test */
public function it_retries_on_transient_failures(): void
{
$mockProvider = \Mockery::mock(AIProviderInterface::class);
$mockProvider->shouldReceive('complete')
->times(2)
->andThrow(new \Exception('Transient error'))
->andReturn("- Successful response");
$mockProvider->shouldReceive('getModelIdentifier')
->andReturn('mock:test-model');
$this->app->instance(AIProviderInterface::class, $mockProvider);
$service = $this->app->make(ContentSummarizationService::class);
$result = $service->summarize('Test content for summarization...');
$this->assertNotEmpty($result['bullet_points']);
}
/** @test */
public function it_caches_identical_requests(): void
{
$mockProvider = \Mockery::mock(AIProviderInterface::class);
$mockProvider->shouldReceive('complete')
->once() // Should only be called once
->andReturn("- Cached response");
$mockProvider->shouldReceive('getModelIdentifier')
->andReturn('mock:test-model');
$this->app->instance(AIProviderInterface::class, $mockProvider);
$service = $this->app->make(ContentSummarizationService::class);
$content = 'Test content for caching verification...';
// First call - hits API
$result1 = $service->summarize($content);
// Second call - should use cache
$result2 = $service->summarize($content);
$this->assertEquals($result1, $result2);
}
}
Using Fake Facades
For OpenAI, use the built-in fake method:
<?php
namespace Tests\Feature;
use OpenAI\Laravel\Facades\OpenAI;
use OpenAI\Responses\Chat\CreateResponse;
use Tests\TestCase;
class OpenAIIntegrationTest extends TestCase
{
/** @test */
public function it_handles_openai_responses(): void
{
OpenAI::fake([
CreateResponse::fake([
'choices' => [
[
'message' => [
'content' => '- Test bullet point',
],
],
],
]),
]);
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'user', 'content' => 'Test prompt'],
],
]);
$this->assertEquals('- Test bullet point', $response->choices[0]->message->content);
}
}
Key Takeaways
Integrating AI APIs into Laravel applications opens tremendous possibilities, but production success requires attention to architecture, reliability, and cost management:
-
Choose providers strategically: OpenAI for breadth, Claude for nuanced reasoning, Bedrock for AWS-native environments. Consider Prism for provider flexibility.
-
Build abstraction layers: Isolate AI provider details behind interfaces. This enables testing, provider switching, and graceful degradation.
-
Implement robust error handling: Classify errors, implement exponential backoff, and provide meaningful fallbacks for users.
-
Optimize costs aggressively: Cache responses, cascade models by complexity, truncate inputs thoughtfully, and monitor usage constantly.
-
Secure everything: Protect API keys, validate inputs against injection, rate limit endpoints, and maintain audit logs.
-
Test without API calls: Use mocks and fakes extensively. Reserve integration tests for critical paths only.
The code patterns in this guide represent production-tested approaches from real Laravel applications. Start with the simplest implementation that meets your needs, then add caching, cascading, and advanced error handling as your usage scales. For practical Laravel development workflows that complement these AI integration patterns, see my Claude Code for Laravel guide.
Need help integrating AI capabilities into your Laravel application? I specialize in building production-ready AI features for PHP and Laravel applications, with expertise spanning OpenAI, Claude, and AWS Bedrock integrations. My API Development service includes AI architecture design, cost optimization, and security implementation. Schedule a free consultation to discuss your AI integration requirements.
Related Reading:
- Laravel API Development Best Practices: A Complete Guide
- AWS Cost Optimization for PHP Apps: A Complete Guide
- Migrating Legacy Laravel Apps: Lessons from 14 Years of PHP Development
External Resources:
- OpenAI PHP for Laravel - Official Laravel Package
- Anthropic Laravel Package - Community Claude Integration
- Prism PHP - Unified AI Interface for Laravel
- AWS Bedrock PHP Examples - Official AWS Documentation
- Prism Bedrock Provider - AWS Bedrock Support for Prism

Richard Joseph Porter
Full-stack developer with expertise in modern web technologies. Passionate about building scalable applications and sharing knowledge through technical writing.
Need Help Upgrading Your Laravel App?
I specialize in modernizing legacy Laravel applications with zero downtime. Get a free codebase audit and upgrade roadmap.
Related Articles
AI-Assisted Code Review: Claude for Laravel Quality
Use Claude for Laravel code reviews. Catch security flaws, N+1 queries, and anti-patterns with proven prompts and CI/CD integration. Boost code quality.
Claude Code for Laravel: A Practitioner's Workflow
Master Claude Code for Laravel development. Battle-tested workflows for migrations, Eloquent debugging, testing, and API scaffolding. Boost productivity.
Laravel Vapor: Deploying Serverless PHP Applications
Deploy Laravel on AWS Lambda with Vapor. Complete guide covering setup, cost analysis vs EC2, cold start mitigation, and when to choose serverless.