Passer au contenu principal

Quand un fournisseur personnalisé est nécessaire

Laravel AI SDK prend en charge en standard les principaux services d’IA tels qu’OpenAI, Anthropic, Gemini et Mistral. Cependant, les fournisseurs standards ne suffisent pas dans les cas suivants :
  • Un service d’IA émergent qui n’est pas encore officiellement pris en charge
  • Vous voulez faire transiter les requêtes par une passerelle de modèles interne ou une couche de gestion de la facturation
  • Un serveur d’inférence on-premise possédant son propre protocole ou mode d’authentification
Dans ces cas, implémentez un fournisseur personnalisé et enregistrez-le auprès de l’AiManager du SDK afin de pouvoir l’utiliser avec la même API que les fournisseurs standards.
Pour les API compatibles OpenAI, utilisez le pilote intégréDepuis la version 0.9 du SDK, un pilote openai-compatible est fourni en standard. Pour un serveur d’inférence interne compatible OpenAI (par exemple l’endpoint compatible OpenAI d’Ollama), il suffit d’ajouter la configuration dans config/ai.php pour l’utiliser, sans avoir besoin d’implémenter un fournisseur personnalisé.
'my-server' => [
    'driver' => 'openai-compatible',
    'key'    => env('MY_SERVER_API_KEY'),
    'url'    => env('MY_SERVER_URL', 'http://localhost:8080/v1'),
    'models' => [
        'text' => ['default' => 'llama3.3-70b'],
    ],
],

Vue d’ensemble de l’architecture

Structure en deux couches

Laravel AI SDK est composé de deux couches : les fournisseurs (providers) et les passerelles (gateways).
CoucheRôleExemples
ProviderInterface côté application. Résolution du nom de modèle, conservation de la configurationOpenAiProvider, AnthropicProvider
GatewayEnvoie une seule étape de requête à l’APIAnthropicGateway, OpenAiCompatibleGateway
Tous les fournisseurs héritent de la classe abstraite Laravel\Ai\Providers\Provider et implémentent les contrats (interfaces) correspondant à chaque fonctionnalité. Les boucles multi-étapes incluant des appels d’outils sont réalisées par TextGenerationLoop, qui appelle la passerelle de manière itérative.

Liste des contrats

Implémentez uniquement les contrats correspondant aux fonctionnalités que vous souhaitez fournir.
ContratEspace de nomsFonctionnalité
TextProviderLaravel\Ai\Contracts\Providers\TextProviderGénération de texte, agents
EmbeddingProviderLaravel\Ai\Contracts\Providers\EmbeddingProviderGénération d’embeddings vectoriels
ImageProviderLaravel\Ai\Contracts\Providers\ImageProviderGénération d’images
AudioProviderLaravel\Ai\Contracts\Providers\AudioProviderSynthèse vocale (TTS)
TranscriptionProviderLaravel\Ai\Contracts\Providers\TranscriptionProviderReconnaissance vocale (STT)
Dans la plupart des cas, implémenter uniquement TextProvider est suffisant.

Le contrat TextProvider

Voici l’interface qu’un fournisseur de génération de texte doit implémenter (src/Contracts/Providers/TextProvider.php).
interface TextProvider extends Provider
{
    public function prompt(AgentPrompt $prompt): AgentResponse;
    public function stream(AgentPrompt $prompt): StreamableAgentResponse;
    public function useTextGateway(StepTextGateway $gateway): self;
    public function textGenerationLoop(): TextGenerationLoop;
    public function defaultTextModel(): string;
    public function cheapestTextModel(): string;
    public function smartestTextModel(): string;
}
L’implémentation de prompt() et stream() peut être déléguée aux traits existants (GeneratesText, StreamsText), donc au total seules quatre méthodes doivent réellement être implémentées : les trois qui retournent les noms de modèles et textGateway(). La boucle multi-étapes d’outils est gérée par TextGenerationLoop, la passerelle ne traite donc qu’une seule étape de requête à la fois.

Exemple d’implémentation : fournisseur personnalisé

Voici un exemple d’enregistrement d’un service d’inférence disposant d’une API propre en tant que fournisseur nommé my-inference.
1

Créer la classe du fournisseur

Créez app/Ai/Providers/MyInferenceProvider.php.
<?php

declare(strict_types=1);

namespace App\Ai\Providers;

use Illuminate\Contracts\Events\Dispatcher;
use Laravel\Ai\Contracts\Gateway\StepTextGateway;
use Laravel\Ai\Contracts\Providers\TextProvider;
use Laravel\Ai\Providers\Concerns\GeneratesText;
use Laravel\Ai\Providers\Concerns\HasTextGateway;
use Laravel\Ai\Providers\Concerns\StreamsText;
use Laravel\Ai\Providers\Provider;

class MyInferenceProvider extends Provider implements TextProvider
{
    use GeneratesText;
    use HasTextGateway;
    use StreamsText;

    public function __construct(protected array $config, protected Dispatcher $events)
    {
        //
    }

    /**
     * Get the credentials for the provider.
     */
    public function providerCredentials(): array
    {
        return ['key' => $this->config['key'] ?? null];
    }

    /**
     * Get the provider's text gateway.
     */
    public function textGateway(): StepTextGateway
    {
        return $this->textGateway ??= new \App\Ai\Gateway\MyInferenceGateway($this->events);
    }

    public function defaultTextModel(): string
    {
        return $this->config['models']['text']['default'] ?? 'my-model-v1';
    }

    public function cheapestTextModel(): string
    {
        return $this->config['models']['text']['cheapest'] ?? 'my-model-v1';
    }

    public function smartestTextModel(): string
    {
        return $this->config['models']['text']['smartest'] ?? 'my-model-v1';
    }
}
2

Enregistrer dans l'AppServiceProvider

Enregistrez-le en utilisant extend() dans la méthode boot de App\Providers\AppServiceProvider.
<?php

namespace App\Providers;

use App\Ai\Providers\MyInferenceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Laravel\Ai\AiManager;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->make(AiManager::class)->extend(
            'my-inference',
            fn (array $config) => new MyInferenceProvider(
                $config,
                $this->app->make(Dispatcher::class)
            )
        );
    }
}
3

Ajouter le fournisseur dans config/ai.php

'providers' => [
    // ...fournisseurs existants...

    'my-inference' => [
        'driver' => 'my-inference',
        'key'    => env('MY_INFERENCE_API_KEY'),
        'url'    => env('MY_INFERENCE_URL', 'http://localhost:8080'),
    ],
],
Ajoutez également les variables dans .env.
MY_INFERENCE_API_KEY=your-api-key
MY_INFERENCE_URL=https://inference.example.internal
4

Utiliser depuis un agent

Une fois enregistré, il suffit de passer le nom du fournisseur à l’argument provider de prompt() pour l’utiliser comme un fournisseur standard.
use App\Ai\Agents\SummaryAgent;

$response = SummaryAgent::make()->prompt('Veuillez résumer cet article.', provider: 'my-inference');

echo $response->text;
Pour l’utiliser comme fournisseur par défaut, modifiez la clé default de config/ai.php.
'default' => 'my-inference',

Implémentation d’une passerelle personnalisée

Pour les services qui possèdent une API propre non compatible OpenAI, une passerelle personnalisée implémentant le contrat StepTextGateway est nécessaire.

Le contrat StepTextGateway

Voici l’interface définie par src/Contracts/Gateway/StepTextGateway.php. La passerelle traite une seule étape de la conversation et retourne une StepResponse. La boucle d’appels d’outils est gérée par le TextGenerationLoop appelant.
interface StepTextGateway
{
    public function generateTextStep(
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): StepResponse;

    public function generateStreamStep(
        string $invocationId,
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): Generator;
}
L’ancien contrat TextGateway (avec generateText(), stream(), onToolInvocation()) présent avant la 0.8 a été supprimé en 0.9. Si vous avez une passerelle personnalisée, vous devez migrer vers StepTextGateway. La méthode onToolInvocation() liée aux appels d’outils a été déplacée dans TextGenerationLoop.

Exemple d’implémentation d’une passerelle personnalisée

Voici le squelette d’une passerelle simple qui envoie des requêtes HTTP à une API d’inférence propriétaire.
<?php

declare(strict_types=1);

namespace App\Ai\Gateway;

use Generator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Facades\Http;
use Laravel\Ai\Contracts\Gateway\StepTextGateway;
use Laravel\Ai\Contracts\Providers\TextProvider;
use Laravel\Ai\Gateway\StepContext;
use Laravel\Ai\Gateway\StepResponse;
use Laravel\Ai\Gateway\TextGenerationOptions;
use Laravel\Ai\Responses\Data\FinishReason;
use Laravel\Ai\Responses\Data\Meta;
use Laravel\Ai\Responses\Data\Usage;

class MyInferenceGateway implements StepTextGateway
{
    public function __construct(protected Dispatcher $events) {}

    public function generateTextStep(
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): StepResponse {
        $credentials = $provider->providerCredentials();
        $config      = $provider->additionalConfiguration();

        $response = Http::withToken($credentials['key'])
            ->baseUrl($config['url'])
            ->timeout($timeout ?? 30)
            ->post('/generate', [
                'model'        => $model,
                'instructions' => $instructions,
                'messages'     => $this->formatMessages($messages),
            ]);

        $data = $response->json();

        return new StepResponse(
            text: $data['output']['text'] ?? '',
            toolCalls: [],
            finishReason: FinishReason::Stop,
            usage: new Usage(
                promptTokens: $data['usage']['input_tokens'] ?? 0,
                completionTokens: $data['usage']['output_tokens'] ?? 0,
            ),
            meta: new Meta(
                provider: $provider->name(),
                model: $model,
            ),
        );
    }

    public function generateStreamStep(
        string $invocationId,
        TextProvider $provider,
        string $model,
        ?string $instructions,
        array $messages,
        array $tools,
        ?array $schema,
        ?TextGenerationOptions $options,
        ?int $timeout,
        StepContext $stepContext,
    ): Generator {
        // Implémentation du streaming (omise)
        yield from [];

        return null;
    }

    protected function formatMessages(array $messages): array
    {
        return array_map(fn ($message) => [
            'role'    => $message->role->value,
            'content' => $message->content,
        ], $messages);
    }
}
Pour prendre en charge les appels d’outils (function calling), incluez les résultats des appels d’outils dans toolCalls au sein de generateTextStep() et définissez finishReason à FinishReason::ToolCalls. L’exécution des outils et le passage à l’étape suivante sont gérés automatiquement par TextGenerationLoop. Reportez-vous à AnthropicGateway.php pour un exemple concret d’implémentation.

Comment tester

Utiliser la méthode fake() de la classe d’agent

Pour tester un agent qui utilise un fournisseur personnalisé, utilisez la méthode fake() de la classe d’agent. Une passerelle factice est assignée au fournisseur, qu’il soit personnalisé ou non.
<?php

namespace Tests\Feature;

use App\Ai\Agents\SummaryAgent;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SummaryAgentTest extends TestCase
{
    use RefreshDatabase;

    public function test_summary_agent_returns_text(): void
    {
        SummaryAgent::fake(['Ceci est un résumé.']);

        $response = SummaryAgent::make()->prompt('Texte du long article...', provider: 'my-inference');

        $this->assertEquals('Ceci est un résumé.', $response->text);

        SummaryAgent::assertPrompted('Texte du long article...');
    }
}
Depuis la 0.9, les réponses de Agent::fake() passent par le même TextGenerationLoop que le fournisseur réel. Si vous configurez un appel d’outil factice sur un agent qui n’a pas d’outils enregistrés, une exception NoSuchToolException sera levée.

Fournisseur mock avec extend()

Vous pouvez également enregistrer un fournisseur de test depuis le conteneur en utilisant extend().
public function test_with_mock_provider(): void
{
    $this->app->make(AiManager::class)->extend(
        'my-inference',
        function (array $config) {
            $events = $this->app->make(\Illuminate\Contracts\Events\Dispatcher::class);

            return new MyInferenceProvider($config, $events);
        }
    );

    // Code de test
}

Liens de référence

OllamaProvider.php — Exemple d'implémentation simple d'un fournisseur

Configuration minimale d’un fournisseur qui se connecte à un serveur de modèle local. Utile comme référence pour implémenter un fournisseur personnalisé.

AnthropicGateway.php — Exemple d'implémentation d'une passerelle

Exemple d’implémentation d’une passerelle qui implémente StepTextGateway. Vous pouvez y examiner l’implémentation de generateTextStep() et generateStreamStep().

Contrat StepTextGateway

Définition de l’interface qu’une passerelle de génération de texte doit implémenter.

Contrat TextProvider

Définition de l’interface qu’un fournisseur de génération de texte doit implémenter.
Dernière modification le 13 juillet 2026