> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# AI SDKのカスタムプロバイダーを作る

> Laravel AI SDKのソースコードを読み解き、標準で提供されていないAIサービスに対応するカスタムプロバイダーを実装する方法を解説します。

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

Laravel AI SDKは OpenAI、Anthropic、Gemini、Mistral など主要なAIサービスを標準でサポートしています。しかし次のようなケースでは標準プロバイダーでは対応できません。

* まだ公式対応されていない新興のAIサービス
* 社内のモデルゲートウェイや課金管理レイヤーを経由させたい
* 独自プロトコルや認証方式を持つオンプレミス推論サーバー

このような場合に、カスタムプロバイダーを実装してSDKの `AiManager` に登録することで、標準プロバイダーと同じAPIで利用できます。

<Info>
  **OpenAI互換APIの場合は組み込みドライバーを使う**

  SDK 0.9以降、`openai-compatible` ドライバーが標準で提供されています。社内のOpenAI互換推論サーバー（Ollama の OpenAI互換エンドポイントなど）には `config/ai.php` に設定を追加するだけで利用でき、カスタムプロバイダーの実装は不要です。

  ```php theme={null}
  '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'],
      ],
  ],
  ```
</Info>

## アーキテクチャの概要

### 2層構造

Laravel AI SDKはプロバイダーとゲートウェイの2層で構成されています。

| レイヤー         | 役割                          | 例                                            |
| ------------ | --------------------------- | -------------------------------------------- |
| **Provider** | アプリ側のインターフェース。モデル名の解決、設定の保持 | `OpenAiProvider`、`AnthropicProvider`         |
| **Gateway**  | 実際のAPIリクエストを1ステップ分送信する      | `AnthropicGateway`、`OpenAiCompatibleGateway` |

すべてのプロバイダーは抽象クラス `Laravel\Ai\Providers\Provider` を継承し、機能ごとのコントラクト（インターフェース）を実装します。ツール呼び出しを含むマルチステップのループは `TextGenerationLoop` がゲートウェイを繰り返し呼び出すことで実現されます。

### コントラクト一覧

提供したい機能に応じて必要なコントラクトだけを実装します。

| コントラクト                  | 名前空間                                                   | 機能            |
| ----------------------- | ------------------------------------------------------ | ------------- |
| `TextProvider`          | `Laravel\Ai\Contracts\Providers\TextProvider`          | テキスト生成・エージェント |
| `EmbeddingProvider`     | `Laravel\Ai\Contracts\Providers\EmbeddingProvider`     | ベクトル埋め込み生成    |
| `ImageProvider`         | `Laravel\Ai\Contracts\Providers\ImageProvider`         | 画像生成          |
| `AudioProvider`         | `Laravel\Ai\Contracts\Providers\AudioProvider`         | 音声合成（TTS）     |
| `TranscriptionProvider` | `Laravel\Ai\Contracts\Providers\TranscriptionProvider` | 音声認識（STT）     |

<Info>
  ほとんどの場合は `TextProvider` だけ実装すれば十分です。
</Info>

## TextProviderコントラクト

テキスト生成プロバイダーが実装するインターフェースです（`src/Contracts/Providers/TextProvider.php`）。

```php theme={null}
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()` の実装は既存のトレイト（`GeneratesText`、`StreamsText`）に任せられるため、実際に実装が必要なのはモデル名を返す3つのメソッドと `textGateway()` の合計4つです。マルチステップのツールループは `TextGenerationLoop` が管理するため、ゲートウェイは1ステップ分のリクエストのみを処理します。

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

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

<Steps>
  <Step title="プロバイダークラスを作成する">
    `app/Ai/Providers/MyInferenceProvider.php` を作成します。

    ```php theme={null}
    <?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';
        }
    }
    ```
  </Step>

  <Step title="AppServiceProviderに登録する">
    `App\Providers\AppServiceProvider` の `boot` メソッドで `extend()` を使って登録します。

    ```php theme={null}
    <?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)
                )
            );
        }
    }
    ```
  </Step>

  <Step title="config/ai.phpにプロバイダーを追加する">
    ```php theme={null}
    'providers' => [
        // ...既存のプロバイダー...

        'my-inference' => [
            'driver' => 'my-inference',
            'key'    => env('MY_INFERENCE_API_KEY'),
            'url'    => env('MY_INFERENCE_URL', 'http://localhost:8080'),
        ],
    ],
    ```

    `.env` にも追加します。

    ```ini theme={null}
    MY_INFERENCE_API_KEY=your-api-key
    MY_INFERENCE_URL=https://inference.example.internal
    ```
  </Step>

  <Step title="エージェントから使う">
    登録後は `prompt()` の `provider` 引数にプロバイダー名を指定するだけで標準プロバイダーと同じように使えます。

    ```php theme={null}
    use App\Ai\Agents\SummaryAgent;

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

    echo $response->text;
    ```

    デフォルトのプロバイダーとして使う場合は `config/ai.php` の `default` キーを変更します。

    ```php theme={null}
    'default' => 'my-inference',
    ```
  </Step>
</Steps>

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

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

### StepTextGatewayコントラクト

`src/Contracts/Gateway/StepTextGateway.php` が定義するインターフェースです。ゲートウェイは会話の**1ステップ分**のリクエストを処理し、`StepResponse` を返します。ツール呼び出しのループは呼び出し元の `TextGenerationLoop` が管理します。

```php theme={null}
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;
}
```

<Warning>
  0.8以前の `TextGateway` コントラクト（`generateText()`、`stream()`、`onToolInvocation()`）は0.9で削除されました。カスタムゲートウェイを持っている場合は `StepTextGateway` への移行が必要です。ツール呼び出しの `onToolInvocation()` は `TextGenerationLoop` に移動しています。
</Warning>

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

独自の推論APIにHTTPリクエストを送るシンプルなゲートウェイの骨格です。

```php theme={null}
<?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);
    }
}
```

<Info>
  ツール呼び出し（function calling）をサポートする場合は `generateTextStep()` でツールの呼び出し結果を `toolCalls` に含め、`finishReason` を `FinishReason::ToolCalls` にします。ツールの実行と次のステップへの移行は `TextGenerationLoop` が自動で処理します。実装の参考には `AnthropicGateway.php` を参照してください。
</Info>

## テスト方法

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

カスタムプロバイダーを使うエージェントのテストには、エージェントクラスの `fake()` メソッドを使います。カスタムプロバイダーかどうかに関係なく、フェイクゲートウェイがプロバイダーにセットされます。

```php theme={null}
<?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('長い記事のテキスト...');
    }
}
```

<Info>
  0.9以降、`Agent::fake()` のレスポンスは実際のプロバイダーと同じ `TextGenerationLoop` を通ります。ツールを登録していないエージェントでフェイクのツール呼び出しを設定した場合、`NoSuchToolException` がスローされます。
</Info>

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

`extend()` を使って、テスト用のプロバイダーをコンテナから登録することもできます。

```php theme={null}
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);
        }
    );

    // テストコード
}
```

## 参考リンク

<Card title="OllamaProvider.php — シンプルなプロバイダー実装例" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Providers/OllamaProvider.php">
  ローカルモデルサーバーに接続するプロバイダーの最小構成です。カスタムプロバイダー実装の参考になります。
</Card>

<Card title="AnthropicGateway.php — ゲートウェイの実装例" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Gateway/Anthropic/AnthropicGateway.php">
  `StepTextGateway` を実装したゲートウェイの実装例です。`generateTextStep()` と `generateStreamStep()` の実装が確認できます。
</Card>

<Card title="StepTextGateway Contract" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Contracts/Gateway/StepTextGateway.php">
  テキスト生成ゲートウェイが実装するインターフェースの定義です。
</Card>

<Card title="TextProvider Contract" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Contracts/Providers/TextProvider.php">
  テキスト生成プロバイダーが実装するインターフェースの定義です。
</Card>


## Related topics

- [カスタムプロバイダー](/jp/packages/laravel-copilot-sdk/custom-providers.md)
- [Laravel Socialite(ソーシャル認証)](/jp/socialite.md)
- [Telemetry](/jp/packages/laravel-copilot-sdk/telemetry.md)
- [はじめに - GitHub Copilot SDK for Laravel](/jp/packages/laravel-copilot-sdk/getting-started.md)
- [Laravel AI SDK](/jp/ai-sdk.md)
