메인 콘텐츠로 건너뛰기

커스텀 프로바이더가 필요한 장면

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, AnthropicProvider
Gateway실제 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라는 프로바이더로서 등록하는 예입니다.
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월 13일