Que faut-il pour intégrer OpenAI ou Claude dans une application Laravel ?
Intégrer OpenAI ou Claude dans une application Laravel consiste à écrire une classe de service qui envoie des prompts à l'API du fournisseur depuis votre serveur, puis à construire autour la plomberie Laravel habituelle : des jobs en file pour le travail lent, une réponse en streaming pour le chat, une table qui journalise chaque appel, une recherche dans vos propres données, des limites de débit et des tests. L'appel API tient en dix lignes. C'est la plomberie qui permet de livrer sans risque.
Ce guide montre du code fonctionnel pour chaque brique, avec les conventions actuelles de Laravel. Il s'adresse aux développeurs Laravel et aux CTO qui valident leurs choix. Si vous préférez confier le travail à une équipe qui interviendra dans votre code, nos services de développement Laravel couvrent l'ajout de fonctionnalités IA aux applications Laravel existantes, et nos services d'intégration IA décrivent l'approche globale.
En résumé :
- Gardez les clés API dans la configuration et n'appelez l'API que depuis le back end.
- Placez chaque appel fournisseur derrière une seule classe de service.
- Exécutez les appels lents dans des jobs en file avec timeouts et nouvelles tentatives.
- Diffusez les réponses interactives en Server-Sent Events.
- Journalisez prompts, versions, tokens et coût pour chaque appel.
- Ajoutez la recherche documentaire (RAG) avec PostgreSQL et pgvector quand les réponses dépendent de vos données.
- Limitez le débit par utilisateur et par tenant.
- Testez avec
Http::fake, et mesurez la qualité avec un jeu d'évaluation.
Client HTTP de Laravel ou package SDK ?
Utilisez le client HTTP de Laravel si vous avez une ou deux fonctionnalités IA et voulez un contrôle total sans dépendance supplémentaire. Utilisez un package SDK si vous avez besoin de réponses typées, de fonctions propres à un fournisseur ou de plusieurs fournisseurs à la fois. Dans tous les cas, cachez-le derrière votre propre classe.
| Option | Adaptée pour | Points de vigilance |
|---|---|---|
Client HTTP de Laravel (Http::) |
Peu de fonctionnalités, contrôle total, tests faciles avec Http::fake |
Vous maintenez vous-même le format des requêtes et des réponses |
| SDK PHP officiel d'Anthropic | Fonctionnalités centrées sur Claude, réponses typées, outils de streaming | Vérifiez les noms de méthodes dans la documentation actuelle du SDK, ils changent d'une version à l'autre |
| Packages communautaires OpenAI pour PHP et Laravel | Fonctionnalités OpenAI avec facade et fakes de test | Maintenus par la communauté : vérifiez l'activité des releases |
| Packages communautaires multi-fournisseurs | Passer d'OpenAI à Claude ou à d'autres avec une seule API | Une couche d'abstraction de plus à maintenir à jour |
Commencez par ajouter les clés dans config/services.php. Le nom du modèle y figure aussi, pour que le changer ne demande pas de modifier le code.
// config/services.php
'anthropic' => [
'key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL'),
],
'openai' => [
'key' => env('OPENAI_API_KEY'),
'model' => env('OPENAI_MODEL'),
'embedding_model' => env('OPENAI_EMBEDDING_MODEL'),
],
Un client Claude minimal avec le client HTTP, qui appelle l'API Messages :
namespace App\Ai;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
final class ClaudeClient
{
public function complete(string $system, string $user, int $maxTokens = 4096): array
{
$response = Http::withHeaders([
'x-api-key' => config('services.anthropic.key'),
'anthropic-version' => '2023-06-01',
])
->timeout(120)
->retry(2, 1000, fn ($e) => $e instanceof ConnectionException, throw: false)
->post('https://api.anthropic.com/v1/messages', [
'model' => config('services.anthropic.model'),
'max_tokens' => $maxTokens,
'system' => $system,
'messages' => [['role' => 'user', 'content' => $user]],
])
->throw();
return [
'text' => collect($response->json('content'))
->where('type', 'text')->pluck('text')->implode(''),
'input_tokens' => $response->json('usage.input_tokens'),
'output_tokens' => $response->json('usage.output_tokens'),
];
}
}
L'équivalent OpenAI utilise l'API Responses. Le texte de la réponse se trouve dans les éléments output de type message :
$response = Http::withToken(config('services.openai.key'))
->timeout(120)
->post('https://api.openai.com/v1/responses', [
'model' => config('services.openai.model'),
'instructions' => $system,
'input' => $user,
])
->throw();
$text = collect($response->json('output'))
->where('type', 'message')
->flatMap(fn (array $item) => $item['content'])
->where('type', 'output_text')
->pluck('text')
->implode('');
Les deux réponses renvoient usage.input_tokens et usage.output_tokens, ce qui suffit pour suivre les coûts. Vous hésitez sur le fournisseur de départ ? Notre comparatif Claude ou OpenAI : quelle API choisir détaille les compromis.
Comment gérer les appels IA longs avec les files d'attente Laravel ?
Placez dans un job en file chaque appel IA que l'utilisateur ne suit pas en direct : résumés, étiquetage, extraction, traitement de documents, rapports nocturnes. Un appel LLM peut prendre de quelques secondes à plus d'une minute, et un worker web qui l'attend ne sert personne d'autre.
namespace App\Jobs;
use App\Ai\ClaudeClient;
use App\Models\Ticket;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
final class SummarizeTicket implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $timeout = 180;
public function __construct(public int $ticketId) {}
public function backoff(): array
{
return [10, 60, 300];
}
public function handle(ClaudeClient $claude): void
{
$ticket = Ticket::findOrFail($this->ticketId);
$result = $claude->complete(
system: view('prompts.ticket-summary-v3')->render(),
user: $ticket->body,
maxTokens: 1024,
);
$ticket->update(['ai_summary' => $result['text']]);
}
}
Trois réglages comptent. Le timeout du job doit dépasser l'appel le plus lent attendu. Le retry_after de la connexion de file doit dépasser le timeout du job, sinon un second worker reprendra un job encore en cours. Et les nouvelles tentatives doivent s'espacer, car la panne la plus fréquente est une limite de débit, que des requêtes supplémentaires aggravent. Faites tourner les jobs IA sur leur propre file pour qu'un retard de résumés ne bloque jamais les e-mails de réinitialisation de mot de passe.
Comment diffuser en streaming les réponses de ChatGPT ou Claude dans Laravel ?
Transmettez le flux du fournisseur au navigateur en Server-Sent Events, pour que les premiers mots s'affichent en une seconde environ au lieu d'attendre la réponse complète. Demandez un flux au fournisseur, lisez-le ligne par ligne et ne renvoyez que les fragments de texte.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
Route::post('/assistant/stream', function (Request $request) {
$question = $request->validate(['q' => 'required|string|max:4000'])['q'];
return response()->stream(function () use ($question) {
$upstream = Http::withHeaders([
'x-api-key' => config('services.anthropic.key'),
'anthropic-version' => '2023-06-01',
])
->withOptions(['stream' => true])
->timeout(120)
->post('https://api.anthropic.com/v1/messages', [
'model' => config('services.anthropic.model'),
'max_tokens' => 4096,
'stream' => true,
'messages' => [['role' => 'user', 'content' => $question]],
]);
$body = $upstream->toPsrResponse()->getBody();
$buffer = '';
while (! $body->eof()) {
$buffer .= $body->read(1024);
while (($pos = strpos($buffer, "\n")) !== false) {
$line = trim(substr($buffer, 0, $pos));
$buffer = substr($buffer, $pos + 1);
if (! str_starts_with($line, 'data: ')) {
continue;
}
$event = json_decode(substr($line, 6), true);
if (($event['type'] ?? '') === 'content_block_delta'
&& ($event['delta']['type'] ?? '') === 'text_delta') {
echo 'data: '.json_encode(['text' => $event['delta']['text']])."\n\n";
if (ob_get_level() > 0) { ob_flush(); }
flush();
}
}
}
echo "event: done\ndata: {}\n\n";
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
})->middleware(['auth', 'throttle:ai']);
L'en-tête X-Accel-Buffering empêche nginx de retenir le flux. Les versions récentes de Laravel proposent aussi un helper pour les flux d'événements : consultez la documentation actuelle avant d'écrire votre propre boucle. Le streaming d'OpenAI fonctionne de la même manière avec d'autres noms d'événements (response.output_text.delta). Avec PHP-FPM, chaque flux ouvert occupe un worker : dimensionnez votre pool pour les chats simultanés ou servez les endpoints de streaming avec Octane.
Où stocker les prompts et les journaux d'appels IA ?
Stockez les prompts sous forme de templates versionnés et journalisez chaque appel dans une table avec la version du prompt, le modèle, les compteurs de tokens, la latence et le coût. Sans ce journal, impossible d'expliquer une mauvaise réponse, un pic de coût ou de savoir si la modification de prompt de la semaine dernière a aidé.
Schema::create('ai_calls', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->index();
$table->foreignId('tenant_id')->nullable()->index();
$table->string('feature');
$table->string('prompt_version');
$table->string('provider');
$table->string('model');
$table->unsignedInteger('input_tokens')->default(0);
$table->unsignedInteger('output_tokens')->default(0);
$table->unsignedInteger('latency_ms')->default(0);
$table->decimal('cost_usd', 10, 6)->default(0);
$table->json('meta')->nullable();
$table->timestamps();
});
Gardez les templates de prompts dans des vues Blade ou une table prompts, nommés avec une version (ticket-summary-v3), et ne modifiez jamais une version existante. Stocker les prompts en base permet d'ajuster le ton sans déploiement ; nous avons utilisé ce modèle dans AI Grief Companion, où les prompts vivent en base avec un historique de versions. Masquez les données personnelles avant de journaliser les entrées complètes, et appliquez les mêmes règles de conservation que dans le reste de votre application.
Comment ajouter du RAG à Laravel avec pgvector ?
Si votre application tourne sur PostgreSQL, ajoutez l'extension pgvector et gardez les embeddings dans une table classique, à côté des identifiants de tenant et des permissions. La recherche devient une seule requête SQL qui filtre selon ce que l'utilisateur a le droit de voir et trie par similarité vectorielle.
// migration
DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
Schema::create('chunks', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->index();
$table->foreignId('document_id')->index();
$table->text('content');
$table->timestamps();
});
// la dimension doit correspondre à votre modèle d'embeddings
DB::statement('ALTER TABLE chunks ADD COLUMN embedding vector(1536)');
DB::statement('CREATE INDEX chunks_embedding_idx ON chunks USING hnsw (embedding vector_cosine_ops)');
Créez les embeddings avec l'endpoint embeddings d'OpenAI (Anthropic ne propose pas ses propres modèles d'embeddings, les projets Claude utilisent donc OpenAI ou un autre fournisseur d'embeddings), puis lancez la recherche :
$embedding = Http::withToken(config('services.openai.key'))
->post('https://api.openai.com/v1/embeddings', [
'model' => config('services.openai.embedding_model'),
'input' => $question,
])
->throw()
->json('data.0.embedding');
$chunks = DB::select(
'SELECT id, document_id, content
FROM chunks
WHERE tenant_id = ?
ORDER BY embedding <=> CAST(? AS vector)
LIMIT 8',
[$tenantId, '['.implode(',', $embedding).']']
);
Insérez les fragments trouvés dans le prompt avec leurs identifiants et demandez au modèle de les citer. Le filtre de tenant dans la clause WHERE est la ligne essentielle : les permissions s'appliquent avant que quoi que ce soit n'atteigne le modèle. Pour le découpage, la recherche hybride et l'évaluation, consultez notre guide de mise en œuvre du RAG.
Comment maîtriser les coûts IA et les limites de débit dans Laravel ?
Utilisez le rate limiter de Laravel par utilisateur et par tenant, mettez en cache les résultats répétés et orientez les étapes simples vers un modèle plus petit. Un script qui tourne en boucle ou un client très enthousiaste ne doit jamais produire une facture surprise.
// AppServiceProvider::boot()
RateLimiter::for('ai', function (Request $request) {
return [
Limit::perMinute(10)->by('user:'.$request->user()->id),
Limit::perDay(2000)->by('tenant:'.$request->user()->tenant_id),
];
});
Les autres leviers, dans l'ordre où ils sont généralement rentables :
- Cache de prompt. Placez en premier la partie longue et stable du prompt (instructions, exemples, texte de référence). OpenAI met automatiquement en cache les longs préfixes répétés ; Claude met en cache ce que vous marquez avec des points de cache. Chez les deux, les tokens d'entrée en cache sont facturés avec une forte remise.
- Routage de modèles. Un petit modèle pour la classification et l'extraction, un grand uniquement là où il apporte un gain mesurable.
- Cache des résultats.
Cache::rememberavec pour clé un hash de l'entrée et de la version du prompt, pour les tâches déterministes. - API batch pour les traitements nocturnes, avec une remise chez les deux fournisseurs.
- Budgets mensuels par tenant, vérifiés dans la table
ai_callsavant chaque appel.
Les coûts de fonctionnement que nous observons en 2026 : quelques dizaines de dollars par mois pour un outil interne peu utilisé, jusqu'à plusieurs milliers pour un assistant client très sollicité.
Comment tester les appels OpenAI et Claude dans Laravel ?
Simulez la couche HTTP pour que les tests soient rapides, gratuits et déterministes, et testez ce que votre code fait des réponses : parsing, enregistrement, contrôles de permissions et gestion des erreurs.
use Illuminate\Support\Facades\Http;
it('stores the ai summary on the ticket', function () {
Http::preventStrayRequests();
Http::fake([
'api.anthropic.com/*' => Http::response([
'content' => [['type' => 'text', 'text' => 'Customer cannot log in after reset.']],
'usage' => ['input_tokens' => 120, 'output_tokens' => 11],
]),
]);
$ticket = Ticket::factory()->create();
(new SummarizeTicket($ticket->id))->handle(app(ClaudeClient::class));
expect($ticket->fresh()->ai_summary)->toBe('Customer cannot log in after reset.');
Http::assertSent(fn ($request) => $request['max_tokens'] === 1024);
});
Testez aussi les cas d'échec : une réponse 429, un timeout, une réponse vide. Les tests unitaires ne mesurent pas la qualité des réponses. Pour cela, gardez un jeu d'évaluation de 50 à 200 entrées réelles avec les sorties attendues, et lancez-le à chaque changement de prompt ou de modèle, en CI ou via une commande artisan.
Quelle approche recommandons-nous ?
Pour la plupart des applications Laravel, notre verdict : commencez avec le client HTTP de Laravel derrière une seule classe de service, mettez en file tout ce qui n'est pas interactif, diffusez le chat en streaming, journalisez chaque appel, utilisez pgvector avant toute base vectorielle séparée, et n'ajoutez un package SDK que lorsque vous avez besoin de fonctions que l'API brute rend pénibles. Cette stack est facile à tester et permet de changer de fournisseur sans difficulté.
Nous avons construit notre propre SaaS IA, AI Resume Master, en PHP avec l'API OpenAI en trois mois environ, et il a atteint 50 000 utilisateurs actifs mensuels. Symfony est notre framework par défaut pour les nouveaux produits, mais nous intervenons dans des bases de code Laravel existantes sans imposer de migration. Si votre équipe travaille plutôt sous Symfony, lisez comment intégrer l'IA dans une application Symfony.
Prochaine étape
Si vous voulez qu'une équipe PHP senior ajoute une fonctionnalité OpenAI ou Claude à votre application Laravel, découvrez nos services de développement Laravel. La plupart des premières fonctionnalités prennent 3 à 8 semaines et démarrent à 10 000 USD, avec un devis forfaitaire après un court appel de cadrage. Contactez-nous en décrivant la fonctionnalité et les données dont elle a besoin.