Laravel AI SDK supports the major AI services out of the box — OpenAI, Anthropic, Gemini, and Mistral. However, the standard providers cannot handle cases like:
An emerging AI service that is not yet officially supported
An internal model gateway or billing management layer you want to route requests through
An on-premises inference server that uses a proprietary protocol or authentication method
In these cases, implement a custom provider and register it with the SDK’s AiManager to use it through the same API as the standard providers.
Use the built-in driver for OpenAI-compatible APIsAs of SDK 0.9, the openai-compatible driver is included out of the box. For an internal OpenAI-compatible inference server, such as Ollama’s OpenAI-compatible endpoint, you only need to add its configuration to config/ai.php. You do not need to implement a custom provider.
The Laravel AI SDK consists of two layers: providers and gateways.
Layer
Role
Examples
Provider
Application-side interface. Resolves model names and holds configuration.
OpenAiProvider, AnthropicProvider
Gateway
Sends the actual API request for a single step.
AnthropicGateway, OpenAiCompatibleGateway
All providers extend the abstract class Laravel\Ai\Providers\Provider and implement feature-specific contracts (interfaces). For multi-step flows that include tool calls, TextGenerationLoop repeatedly invokes the gateway.
The interface that text generation providers implement (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;}
The prompt() and stream() implementations can be delegated to existing traits (GeneratesText, StreamsText), so you only need to implement four methods: the three model name methods and textGateway(). TextGenerationLoop manages the multi-step tool loop, so the gateway only processes a single request step.
This example registers an inference service with a proprietary API as a provider named my-inference.
1
Create the provider class
Create app/Ai/Providers/MyInferenceProvider.php.
<?phpdeclare(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
Register in AppServiceProvider
Register the provider in the boot method of App\Providers\AppServiceProvider using extend().
<?phpnamespace 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) ) ); }}
The interface is defined in src/Contracts/Gateway/StepTextGateway.php. A gateway processes one step of a conversation and returns a StepResponse. The calling TextGenerationLoop manages the tool call loop.
The pre-0.9 TextGateway contract and its generateText(), stream(), and onToolInvocation() methods were removed in 0.9. If you have a custom gateway, migrate it to StepTextGateway. Tool call handling from onToolInvocation() now lives in TextGenerationLoop.
To support tool calls (function calling), include the tool call results in toolCalls from generateTextStep() and set finishReason to FinishReason::ToolCalls. TextGenerationLoop automatically executes the tools and advances to the next step. Refer to AnthropicGateway.php for an implementation example.
To test agents that use your custom provider, call the fake method on the agent class. Regardless of whether a custom provider is in use, the fake gateway is set on the provider.
<?phpnamespace 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(['This is a summary.']); $response = SummaryAgent::make()->prompt('Long article text...', provider: 'my-inference'); $this->assertEquals('This is a summary.', $response->text); SummaryAgent::assertPrompted('Long article text...'); }}
As of 0.9, responses from Agent::fake() pass through the same TextGenerationLoop as real providers. If you configure a fake tool call for an agent that has not registered that tool, the SDK throws a NoSuchToolException.
You can also use extend() to register a test provider from the container.
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); } ); // Test code}