メインコンテンツへスキップ

カスタムプロバイダーが必要な場面

Laravel AI SDKは OpenAI、Anthropic、Gemini、Mistral など主要なAIサービスを標準でサポートしています。しかし次のようなケースでは標準プロバイダーでは対応できません。
  • まだ公式対応されていない新興のAIサービス
  • 社内のモデルゲートウェイや課金管理レイヤーを経由させたい
  • 独自プロトコルや認証方式を持つオンプレミス推論サーバー
このような場合に、カスタムプロバイダーを実装してSDKの AiManager に登録することで、標準プロバイダーと同じAPIで利用できます。
OpenAI互換APIの場合は組み込みドライバーを使うSDK 0.9以降、openai-compatible ドライバーが標準で提供されています。社内のOpenAI互換推論サーバー(Ollama の OpenAI互換エンドポイントなど)には config/ai.php に設定を追加するだけで利用でき、カスタムプロバイダーの実装は不要です。
'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'],
    ],
],

アーキテクチャの概要

2層構造

Laravel AI SDKはプロバイダーとゲートウェイの2層で構成されています。
レイヤー役割
Providerアプリ側のインターフェース。モデル名の解決、設定の保持OpenAiProviderAnthropicProvider
Gateway実際のAPIリクエストを1ステップ分送信するAnthropicGatewayOpenAiCompatibleGateway
すべてのプロバイダーは抽象クラス Laravel\Ai\Providers\Provider を継承し、機能ごとのコントラクト(インターフェース)を実装します。ツール呼び出しを含むマルチステップのループは TextGenerationLoop がゲートウェイを繰り返し呼び出すことで実現されます。

コントラクト一覧

提供したい機能に応じて必要なコントラクトだけを実装します。
コントラクト名前空間機能
TextProviderLaravel\Ai\Contracts\Providers\TextProviderテキスト生成・エージェント
EmbeddingProviderLaravel\Ai\Contracts\Providers\EmbeddingProviderベクトル埋め込み生成
ImageProviderLaravel\Ai\Contracts\Providers\ImageProvider画像生成
AudioProviderLaravel\Ai\Contracts\Providers\AudioProvider音声合成(TTS)
TranscriptionProviderLaravel\Ai\Contracts\Providers\TranscriptionProvider音声認識(STT)
ほとんどの場合は TextProvider だけ実装すれば十分です。

TextProviderコントラクト

テキスト生成プロバイダーが実装するインターフェースです(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;
}
prompt()stream() の実装は既存のトレイト(GeneratesTextStreamsText)に任せられるため、実際に実装が必要なのはモデル名を返す3つのメソッドと textGateway() の合計4つです。マルチステップのツールループは TextGenerationLoop が管理するため、ゲートウェイは1ステップ分のリクエストのみを処理します。

実装例:カスタムプロバイダー

独自APIを持つ推論サービスを my-inference というプロバイダーとして登録する例です。
1

プロバイダークラスを作成する

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

AppServiceProviderに登録する

App\Providers\AppServiceProviderboot メソッドで extend() を使って登録します。
<?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

config/ai.phpにプロバイダーを追加する

'providers' => [
    // ...既存のプロバイダー...

    'my-inference' => [
        'driver' => 'my-inference',
        'key'    => env('MY_INFERENCE_API_KEY'),
        'url'    => env('MY_INFERENCE_URL', 'http://localhost:8080'),
    ],
],
.env にも追加します。
MY_INFERENCE_API_KEY=your-api-key
MY_INFERENCE_URL=https://inference.example.internal
4

エージェントから使う

登録後は prompt()provider 引数にプロバイダー名を指定するだけで標準プロバイダーと同じように使えます。
use App\Ai\Agents\SummaryAgent;

$response = SummaryAgent::make()->prompt('この記事を要約してください。', provider: 'my-inference');

echo $response->text;
デフォルトのプロバイダーとして使う場合は config/ai.phpdefault キーを変更します。
'default' => 'my-inference',

カスタムゲートウェイの実装

OpenAI互換ではない独自APIを持つサービスには、StepTextGateway コントラクトを実装したカスタムゲートウェイが必要です。

StepTextGatewayコントラクト

src/Contracts/Gateway/StepTextGateway.php が定義するインターフェースです。ゲートウェイは会話の1ステップ分のリクエストを処理し、StepResponse を返します。ツール呼び出しのループは呼び出し元の TextGenerationLoop が管理します。
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;
}
0.8以前の TextGateway コントラクト(generateText()stream()onToolInvocation())は0.9で削除されました。カスタムゲートウェイを持っている場合は StepTextGateway への移行が必要です。ツール呼び出しの onToolInvocation()TextGenerationLoop に移動しています。

カスタムゲートウェイの実装例

独自の推論APIにHTTPリクエストを送るシンプルなゲートウェイの骨格です。
<?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 {
        // ストリーミングの実装(省略)
        yield from [];

        return null;
    }

    protected function formatMessages(array $messages): array
    {
        return array_map(fn ($message) => [
            'role'    => $message->role->value,
            'content' => $message->content,
        ], $messages);
    }
}
ツール呼び出し(function calling)をサポートする場合は generateTextStep() でツールの呼び出し結果を toolCalls に含め、finishReasonFinishReason::ToolCalls にします。ツールの実行と次のステップへの移行は TextGenerationLoop が自動で処理します。実装の参考には AnthropicGateway.php を参照してください。

テスト方法

エージェントクラスの fake() を使う

カスタムプロバイダーを使うエージェントのテストには、エージェントクラスの fake() メソッドを使います。カスタムプロバイダーかどうかに関係なく、フェイクゲートウェイがプロバイダーにセットされます。
<?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(['これは要約です。']);

        $response = SummaryAgent::make()->prompt('長い記事のテキスト...', provider: 'my-inference');

        $this->assertEquals('これは要約です。', $response->text);

        SummaryAgent::assertPrompted('長い記事のテキスト...');
    }
}
0.9以降、Agent::fake() のレスポンスは実際のプロバイダーと同じ TextGenerationLoop を通ります。ツールを登録していないエージェントでフェイクのツール呼び出しを設定した場合、NoSuchToolException がスローされます。

extend() を使ったモックプロバイダー

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);
        }
    );

    // テストコード
}

参考リンク

OllamaProvider.php — シンプルなプロバイダー実装例

ローカルモデルサーバーに接続するプロバイダーの最小構成です。カスタムプロバイダー実装の参考になります。

AnthropicGateway.php — ゲートウェイの実装例

StepTextGateway を実装したゲートウェイの実装例です。generateTextStep()generateStreamStep() の実装が確認できます。

StepTextGateway Contract

テキスト生成ゲートウェイが実装するインターフェースの定義です。

TextProvider Contract

テキスト生成プロバイダーが実装するインターフェースの定義です。
最終更新日 2026年7月11日