Aller au contenu
Projet de développement SGBD

Api

Intégration des APIs LLM dans Laravel

1. Introduction : Les APIs LLM en 2025

Bienvenue dans le monde merveilleux des Large Language Models. Vous allez apprendre à faire parler des modèles d'IA depuis votre application Laravel, sans vendre un rein pour payer la facture (enfin, on espère).

Pourquoi intégrer un LLM ?

  • Automatiser des tâches de génération de texte
  • Créer des chatbots (le fameux "clone ChatGPT" de votre examen 👀)
  • Analyser, résumer, transformer du contenu
  • Ajouter un effet "wow" à vos projets (ou un effet "flop" si vous vous y prenez mal)

Le paysage actuel

Les providers de LLM (OpenAI, Anthropic, Google, Meta, xAI...) proposent généralement :

  • Une documentation avec des exemples en Python et TypeScript (parce que PHP c'est pour les boomers, apparemment 🙄)
  • Des packages officiels pour ces langages
  • Un exemple cURL pour les autres (c'est nous ça)

C'est ce fameux exemple cURL qui va nous servir de pont vers PHP/Laravel.

2. Comprendre les docs : De cURL à PHP

2.1 C'est quoi cURL ?

cURL (Client URL) est un outil en ligne de commande qui permet d'effectuer des requêtes HTTP. Quand vous voyez un exemple cURL dans une documentation d'API, c'est essentiellement une recette pour envoyer une requête HTTP avec tous les ingrédients nécessaires.

2.2 Anatomie d'une requête API LLM

Prenons l'exemple typique qu'on trouve dans la documentation d'OpenRouter :

curl https://openrouter.ai/api/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -d '{
    "model": "openai/gpt-5-mini",
    "messages": [
      {
        "role": "user",
        "content": "What is the meaning of life?"
      }
    ]
  }'

Décomposons cette requête :

Élément Signification
curl https://... L'URL de l'endpoint API (où envoyer la requête)
-H "Content-Type: application/json" Header indiquant qu'on envoie du JSON
-H "Authorization: Bearer ..." Header d'authentification avec notre clé API
-d '{...}' Le corps (body) de la requête en JSON

Le body contient deux éléments essentiels :

  • model : Le modèle à utiliser (ex: openai/gpt-5-mini, anthropic/claude-sonnet-4.5)
  • messages : La conversation sous forme de tableau avec des rôles (system, user, assistant)

2.3 Transformer cURL en Laravel Http Client

Laravel fournit un client HTTP élégant via la facade Http. Voici comment traduire notre requête cURL :

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'Content-Type' => 'application/json',
    'Authorization' => 'Bearer ' . config('services.openrouter.api_key'),
])->post('https://openrouter.ai/api/v1/chat/completions', [
    'model' => 'openai/gpt-5-mini',
    'messages' => [
        ['role' => 'user', 'content' => 'What is the meaning of life?']
    ]
]);

$content = $response->json('choices.0.message.content');

C'est tout. Vous venez de faire votre première requête à un LLM en PHP. 🎉

3. OpenRouter : Un routeur pour tous les modèles

3.1 Pourquoi OpenRouter ?

Gérer plusieurs APIs de providers différents (OpenAI, Anthropic, Google...) devient vite un cauchemar : authentifications différentes, formats de requêtes variables, tarifications complexes...

OpenRouter résout ce problème en proposant une API unifiée qui donne accès à plus de 400 modèles de dizaines de providers différents. Vous utilisez une seule clé API, un seul format de requête, et vous pouvez switcher de modèle en changeant juste une string.

3.2 Ce que propose OpenRouter

  • Modèles de langage : GPT-5, Claude Sonnet 4.5, Gemini 3, Grok, Llama, Mistral, DeepSeek...
  • Génération d'images (nouveauté récente !) :
    • GPT-5 Image Mini (~$8/M tokens, le plus économique)
    • GPT-5 Image (~$40/M tokens)
    • FLUX.2 Pro/Flex (Black Forest Labs)
    • Nano Banana / Gemini 2.5 Flash Image (Google)
  • Vision : Analyse d'images avec les modèles multimodaux
  • Audio : Reconnaissance vocale (speech-to-text) et synthèse vocale (text-to-speech)
  • Embeddings : Pour la recherche sémantique et le RAG

⚠️ Attention aux coûts : La génération d'images consomme beaucoup plus de tokens que le texte. Privilégiez les modèles économiques comme openai/gpt-5-image-mini pour vos tests (moins de 1 centime par image).

3.3 Avantages

  • API compatible OpenAI : Le format de requête est standardisé
  • Fallback automatique : Si un provider tombe, OpenRouter peut basculer sur un autre
  • Tarification transparente : Prix affichés par modèle, facturation consolidée
  • Modèles gratuits : Plusieurs modèles sont disponibles gratuitement (avec des limites)
  • Infos détaillées : Usage, coûts, temps de réponse... tout est dans la réponse API

3.4 Créer un compte et obtenir une clé

  1. Rendez-vous sur openrouter.ai
  2. Créez un compte
  3. Générez une clé API dans les settings

🎁 Bonne nouvelle : Une clé API avec 1$ de crédit OpenRouter vous sera fournie pour ce cours. C'est largement suffisant pour développer et tester votre projet (sauf si vous décidez de générer 10 000 images 4K, évidemment).

4. Intégration dans Laravel

4.1 Pourquoi ne pas utiliser un package existant ?

Il existe des packages PHP comme openai-php/laravel qui wrappent l'API OpenAI. Cependant, pour ce cours, nous allons construire notre propre client pour plusieurs raisons :

  1. Accès complet aux données : Les packages masquent souvent des informations utiles (usage, coûts, thinking des modèles...)
  2. Compréhension : Vous saurez exactement ce qui se passe sous le capot
  3. Flexibilité : Facile d'ajouter des features spécifiques à OpenRouter
  4. Contrôle des erreurs : Messages d'erreur clairs et personnalisables
  5. Pédagogie : C'est l'occasion d'apprendre à structurer du code proprement

4.2 Configuration

Dans votre .env et .env.example :

OPENROUTER_API_KEY=sk-or-v1-votre-clé-ici
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1

Dans config/services.php, ajoutez :

'openrouter' => [
    'api_key' => env('OPENROUTER_API_KEY'),
    'base_url' => env('OPENROUTER_BASE_URL', 'https://openrouter.ai/api/v1'),
],

4.3 Architecture recommandée : Client + Services

Pour un code propre et maintenable, on sépare les responsabilités :

app/Services/
└── OpenRouter/
    └── Client.php          # Gère les appels HTTP à l'API
    
├── AskService.php          # Questions simples (exercice 1)
├── ChatService.php         # Conversations avec historique
├── ImageService.php        # Génération d'images
└── ...

Le Client s'occupe uniquement de :

  • Configurer les headers d'authentification
  • Envoyer les requêtes HTTP
  • Gérer les erreurs de base

Les Services s'occupent de :

  • La logique métier
  • Formater les données
  • Utiliser le Client pour communiquer avec l'API

4.4 Un Client basique

Voici un exemple de Client simple pour démarrer :

<?php

declare(strict_types=1);

namespace App\Services\OpenRouter;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

class Client
{
    private string $apiKey;
    private string $baseUrl;

    public function __construct()
    {
        $this->apiKey = config('services.openrouter.api_key');
        $this->baseUrl = rtrim(config('services.openrouter.base_url'), '/');
    }

    /**
     * Effectue une requête GET.
     */
    public function get(string $endpoint): Response
    {
        return $this->request()->get($this->baseUrl . $endpoint);
    }

    /**
     * Effectue une requête POST.
     */
    public function post(string $endpoint, array $data = []): Response
    {
        return $this->request()->post($this->baseUrl . $endpoint, $data);
    }

    /**
     * Configure la requête HTTP avec les headers nécessaires.
     */
    private function request(): PendingRequest
    {
        return Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->apiKey,
            'Content-Type' => 'application/json',
            'HTTP-Referer' => config('app.url'),
            'X-Title' => config('app.name'),
        ])->timeout(120);
    }
}

4.5 Utilisation du Client dans un Service

<?php

namespace App\Services;

use App\Services\OpenRouter\Client;

class ChatService
{
    public function __construct(private Client $client) {}

    public function sendMessage(array $messages, string $model): string
    {
        $response = $this->client->post('/chat/completions', [
            'model' => $model,
            'messages' => $messages,
            'temperature' => 1.0,
        ]);

        if ($response->failed()) {
            $error = $response->json('error.message', 'Erreur inconnue');
            throw new \RuntimeException("Erreur API: {$error}");
        }

        return $response->json('choices.0.message.content', '');
    }

    public function getModels(): array
    {
        $response = $this->client->get('/models');
        
        return $response->json('data', []);
    }
}

4.6 Aller plus loin : DTOs et architecture complète

Pour un projet plus conséquent, vous pouvez structurer davantage avec des Data Transfer Objects (DTOs) :

app/Services/OpenRouter/
├── Client.php
├── Data/
│   ├── Message.php           # Représente un message
│   ├── ChatOptions.php       # Options de la requête
│   └── Responses/
│       ├── ChatResponse.php  # Réponse complète
│       ├── Usage.php         # Tokens utilisés, coûts
│       └── ...

Cette approche permet de :

  • Typer fortement les données
  • Accéder facilement aux infos de pricing et d'usage
  • Récupérer le "thinking" des modèles qui le supportent (Claude, o1...)

Nous verrons cette architecture avancée dans les chapitres suivants.

5. Bonnes pratiques

5.1 Sécurité des clés API

Votre clé API est comme votre carte bancaire. Ne la laissez pas traîner.

// ❌ JAMAIS ÇA
$apiKey = 'sk-or-v1-abc123...';

// ⚠️ ÉVITER dans le code applicatif
$apiKey = env('OPENROUTER_API_KEY');

// ✅ TOUJOURS ÇA
$apiKey = config('services.openrouter.api_key');

💡 Pourquoi config() plutôt que env() ? Le helper env() ne fonctionne pas quand le cache de config est activé (php artisan config:cache). En production, utilisez toujours config() qui lit depuis les fichiers de configuration.

Règles d'or :

  • Ne jamais commiter une clé API dans Git (ajoutez .env au .gitignore)
  • Utiliser des clés différentes pour dev/staging/production
  • Surveiller votre consommation sur le dashboard OpenRouter
  • Révoquer immédiatement une clé compromise

5.2 Gestion des erreurs

Les APIs LLM peuvent échouer pour diverses raisons : rate limiting, quota dépassé, modèle indisponible...

use Illuminate\Http\Client\RequestException;

try {
    $response = Http::withHeaders([...])
        ->timeout(60) // Les LLMs peuvent être lents
        ->retry(3, 1000) // 3 tentatives, 1s entre chaque
        ->post($url, $data);
    
    if ($response->failed()) {
        $error = $response->json('error.message', 'Erreur inconnue');
        throw new \RuntimeException("Erreur API: {$error}");
    }
    
    return $response->json('choices.0.message.content');
    
} catch (\Exception $e) {
    Log::error('Erreur LLM', ['message' => $e->getMessage()]);
    throw $e;
}

5.3 Logging

Gardez une trace de vos requêtes pour débugger et analyser l'usage :

Log::info('LLM Request', [
    'model' => $model,
    'prompt_tokens' => $response->json('usage.prompt_tokens'),
    'completion_tokens' => $response->json('usage.completion_tokens'),
    'total_tokens' => $response->json('usage.total_tokens'),
    'cost' => $response->json('usage.cost'), // Spécifique OpenRouter
]);

5.4 Paramètres utiles

Temperature

La température contrôle le niveau de "créativité" ou d'aléatoire dans les réponses :

Valeur Comportement
1 Valeur recommandée par défaut pour les assistants conversationnels
0.5 - 0.8 Plus sérieux, moins de variabilité, bon pour du contenu factuel
0 Déterministe : mêmes inputs = mêmes outputs (utile pour les tests)
// Assistant créatif/conversationnel
'temperature' => 1,

// Rédaction sérieuse, documentation
'temperature' => 0.7,

// Réponses reproductibles (tests, pipelines)
'temperature' => 0,

Max Tokens

Ce paramètre limite le nombre de tokens en sortie. En général, ne le limitez pas trop — laissez le modèle répondre ce dont il a besoin. La limite technique du modèle bloquera naturellement.

Cas où limiter est pertinent :

  • Génération de titres courts (ex: max_tokens: 50)
  • Résumés contraints
  • Réponses très courtes attendues
// Génération de titre de conversation
$response = $client->post('/chat/completions', [
    'model' => 'openai/gpt-5-mini',
    'messages' => $messages,
    'max_tokens' => 50, // On veut juste quelques mots
]);

// Conversation normale : pas de limite artificielle
$response = $client->post('/chat/completions', [
    'model' => 'openai/gpt-5-mini',
    'messages' => $messages,
    'temperature' => 1,
    // max_tokens non spécifié = le modèle répond librement
]);

Autres paramètres

Paramètre Description
top_p Alternative à temperature (nucleus sampling), entre 0 et 1
stream Activer le streaming des tokens (voir chapitre dédié)
stop Séquences qui arrêtent la génération

6. Ressources utiles

Conclusion

Vous avez maintenant les bases pour intégrer des LLMs dans votre application Laravel avec le client HTTP natif. Cette approche vous donne un contrôle total sur les requêtes et les réponses, ce qui sera essentiel pour les fonctionnalités avancées comme le streaming ou l'accès aux données d'usage.

Dans les chapitres suivants, nous verrons comment implémenter le streaming pour afficher les réponses en temps réel, et comment structurer proprement votre code avec une architecture complète (DTOs, services spécialisés...).

Maintenant, au boulot ! 🚀