カスタムプロバイダーが必要な場面
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 アプリ側のインターフェース。モデル名の解決、設定の保持 OpenAiProvider、AnthropicProviderGateway 実際のAPIリクエストを1ステップ分送信する AnthropicGateway、OpenAiCompatibleGateway
すべてのプロバイダーは抽象クラス 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() の実装は既存のトレイト(GeneratesText、StreamsText)に任せられるため、実際に実装が必要なのはモデル名を返す3つのメソッドと textGateway() の合計4つです。マルチステップのツールループは TextGenerationLoop が管理するため、ゲートウェイは1ステップ分のリクエストのみを処理します。
実装例:カスタムプロバイダー
独自APIを持つ推論サービスを my-inference というプロバイダーとして登録する例です。
プロバイダークラスを作成する
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' ;
}
}
AppServiceProviderに登録する
App\Providers\AppServiceProvider の boot メソッドで 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 )
)
);
}
}
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
エージェントから使う
登録後は prompt() の provider 引数にプロバイダー名を指定するだけで標準プロバイダーと同じように使えます。 use App\Ai\Agents\ SummaryAgent ;
$response = SummaryAgent :: make () -> prompt ( 'この記事を要約してください。' , provider : 'my-inference' );
echo $response -> text ;
デフォルトのプロバイダーとして使う場合は config/ai.php の default キーを変更します。 '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 に含め、finishReason を FinishReason::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 テキスト生成プロバイダーが実装するインターフェースの定義です。