> ## 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 的自定義 Provider

> 解讀 Laravel AI SDK 的原始碼，講解如何為標準未提供的 AI 服務實現自定義 Provider。

## 何時需要自定義 Provider

Laravel AI SDK 內建支援 OpenAI、Anthropic、Gemini、Mistral 等主流 AI 服務。然而在以下場景，標準 Provider 無法滿足需求：

* 尚未獲得官方支援的新興 AI 服務
* 想要經過公司內部的模型閘道器或計費管理層
* 擁有獨立協議或認證方式的本地部署推理伺服器

在這些情況下，你可以實現自定義 Provider 並註冊到 SDK 的 `AiManager`，就能像標準 Provider 一樣透過同一 API 使用它。

<Info>
  **OpenAI 相容 API 請使用內建驅動**

  從 SDK 0.9 開始，`openai-compatible` 驅動已內建提供。對於公司內部的 OpenAI 相容推理伺服器（例如 Ollama 的 OpenAI 相容端點），只需在 `config/ai.php` 中新增配置即可使用，無需實現自定義 Provider。

  ```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>

## 架構概覽

### 兩層結構

Laravel AI SDK 由 Provider 與 Gateway 兩層構成。

| 層            | 職責                    | 示例                                           |
| ------------ | --------------------- | -------------------------------------------- |
| **Provider** | 面向應用的介面。負責模型名解析與配置的持有 | `OpenAiProvider`、`AnthropicProvider`         |
| **Gateway**  | 傳送一個步驟的實際 API 請求      | `AnthropicGateway`、`OpenAiCompatibleGateway` |

所有 Provider 都繼承抽象類 `Laravel\Ai\Providers\Provider`，並按功能實現對應的契約（介面）。包含工具呼叫的多步驟迴圈由 `TextGenerationLoop` 反覆呼叫 Gateway 來實現。

### 契約一覽

根據要支援的功能，實現所需的契約即可。

| 契約                      | 名稱空間                                                   | 功能         |
| ----------------------- | ------------------------------------------------------ | ---------- |
| `TextProvider`          | `Laravel\Ai\Contracts\Providers\TextProvider`          | 文字生成、Agent |
| `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 契約

這是文字生成 Provider 所要實現的介面（`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()` 的實現可交由現有 trait（`GeneratesText`、`StreamsText`）處理，因此實際需要實現的只有返回模型名的 3 個方法以及 `textGateway()`，合計 4 個方法。多步驟的工具迴圈由 `TextGenerationLoop` 管理，Gateway 只需處理一步的請求。

## 實現示例：自定義 Provider

以下示例將一個擁有獨立 API 的推理服務作為 `my-inference` Provider 進行註冊。

<Steps>
  <Step title="建立 Provider 類">
    建立 `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 中新增 Provider">
    ```php theme={null}
    'providers' => [
        // ...已有的 Provider...

        '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="在 Agent 中使用">
    註冊後，只需將 Provider 名稱傳給 `prompt()` 的 `provider` 引數，就能像使用標準 Provider 一樣使用它。

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

    $response = SummaryAgent::make()->prompt('請總結這篇文章。', provider: 'my-inference');

    echo $response->text;
    ```

    若要將其作為預設 Provider，請修改 `config/ai.php` 的 `default` 鍵。

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

## 自定義 Gateway 的實現

對於並非 OpenAI 相容、擁有獨立 API 的服務，需要實現 `StepTextGateway` 契約的自定義 Gateway。

### StepTextGateway 契約

以下是 `src/Contracts/Gateway/StepTextGateway.php` 定義的介面。Gateway 負責處理會話的**單步**請求並返回 `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 中移除。如果你擁有自定義 Gateway，需要遷移到 `StepTextGateway`。工具呼叫的 `onToolInvocation()` 已被移動到 `TextGenerationLoop`。
</Warning>

### 自定義 Gateway 實現示例

以下是向自定義推理 API 發起 HTTP 請求的簡單 Gateway 骨架。

```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>

## 測試方法

### 使用 Agent 類的 fake()

要測試使用自定義 Provider 的 Agent，可使用 Agent 類的 `fake()` 方法。不論是否是自定義 Provider，都會將 fake Gateway 注入到 Provider 中。

```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()` 的響應會經過與真實 Provider 相同的 `TextGenerationLoop`。如果在沒有註冊工具的 Agent 上設定了 fake 工具呼叫，會丟擲 `NoSuchToolException`。
</Info>

### 使用 extend() 的 Mock Provider

也可以透過 `extend()` 從容器註冊測試用 Provider。

```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 — 簡單的 Provider 實現示例" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Providers/OllamaProvider.php">
  連線本地模型伺服器的 Provider 最小實現。可作為自定義 Provider 實現的參考。
</Card>

<Card title="AnthropicGateway.php — Gateway 實現示例" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Gateway/Anthropic/AnthropicGateway.php">
  實現了 `StepTextGateway` 的 Gateway 示例。可檢視 `generateTextStep()` 與 `generateStreamStep()` 的實現。
</Card>

<Card title="StepTextGateway Contract" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Contracts/Gateway/StepTextGateway.php">
  文字生成 Gateway 所要實現的介面定義。
</Card>

<Card title="TextProvider Contract" icon="github" href="https://github.com/laravel/ai/blob/0.x/src/Contracts/Providers/TextProvider.php">
  文字生成 Provider 所要實現的介面定義。
</Card>


## Related topics

- [建立 Boost 的自定義 Agent](/zh-TW/advanced/boost-custom-agent.md)
- [進階主題](/zh-TW/advanced/index.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [請求生命週期](/zh-TW/lifecycle.md)
- [用 Laravel 構建 MCP 伺服器](/zh-TW/advanced/mcp-server.md)
