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.

Richard Joseph Porter
22 min read
laravelaiopenaiclaudeawsbedrockphp

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:

External Resources:

Richard Joseph Porter - Professional headshot

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.