Using Multiple LLM Providers with the Laravel AI SDK
The Laravel AI SDK (laravel/ai , v0.8.1) ships with built-in support for 14 AI providers: OpenAI, Anthropic, Google Gemini, Groq, Mistral, DeepSeek, xAI, Ollama, Azure OpenAI, Cohere, OpenRouter, Jina, VoyageAI, and ElevenLabs. That breadth is genuinely useful - but unlocking it correctly requires more than swapping an env variable. This article covers how to configure multiple providers in the same application, switch between them at runtime, handle provider-specific failures, and test the whole setup without hitting a live API. Prerequisites - PHP 8.3+ - Laravel 12 or 13 - laravel/ai v0.8 (pin to^0.8 incomposer.json until v1.0 ships - the package is still pre-1.0) - At least two provider API keys (we'll use OpenAI and Anthropic for the walkthrough) Install the SDK if you haven't already: composer require laravel/ai php artisan vendor:publish --tag=ai-config How the SDK Resolves Providers When you call AI::text()->using('gpt-4o-mini')->... , the SDK maps the model string to a provider using the published config/ai.php . The default config reads a single AI_PROVIDER env variable. That's fine for a simple setup, but in a multi-provider application you need named provider instances instead. Here is the relevant section of config/ai.php after customisation: // config/ai.php 'providers' => [ 'openai' => [ 'driver' => 'openai', 'api_key' => env('OPENAI_API_KEY'), ], 'anthropic' => [ 'driver' => 'anthropic', 'api_key' => env('ANTHROPIC_API_KEY'), ], 'groq' => [ 'driver' => 'groq', 'api_key' => env('GROQ_API_KEY'), ], 'openrouter' => [ 'driver' => 'openrouter', 'api_key' => env('OPENROUTER_API_KEY'), ], ], And the matching .env entries: OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... GROQ_API_KEY=gsk_... OPENROUTER_API_KEY=sk-or-... Never prefix these with NEXT_PUBLIC_ - all AI calls must go through the Laravel backend. A frontend that calls an LLM directly exposes your API key to every browser that loads the page. Selecting a Provider at Call Time The using() method accepts a model identifier that the SDK resolves to a provider. You can be explicit: use Illuminate\Support\Facades\AI; // GPT-4o-mini via OpenAI $openAiResult = AI::text() ->using('gpt-4o-mini') ->prompt($userMessage) ->generate(); // Claude Sonnet via Anthropic $claudeResult = AI::text() ->using('claude-sonnet-4-5') ->prompt($userMessage) ->generate(); // Llama 3 via Groq (fast inference) $groqResult = AI::text() ->using('llama3-70b-8192') ->prompt($userMessage) ->generate(); The SDK looks at the model string and matches it to the correct provider driver configured in config/ai.php . If a model string is ambiguous (e.g. two providers offer a model with the same name), qualify it with a provider prefix: // Explicit provider prefix avoids ambiguity $result = AI::text() ->using('openai:gpt-4o') ->prompt($prompt) ->generate(); Building a Provider Router A common production pattern is to choose the provider based on the task type. Create a simple service class rather than scattering using() strings throughout controllers: using('llama3-70b-8192') // served by Groq ->prompt($prompt) ->generate() ->text(); } /** * High-quality reasoning - use GPT-4o for complex tasks. / public function precise(string $prompt): string { return AI::text() ->using('gpt-4o') ->prompt($prompt) ->generate() ->text(); } /* * Long-context summarisation - Anthropic handles large contexts well. */ public function summarise(string $document): string { return AI::text() ->using('claude-sonnet-4-5') ->prompt('Summarise the following in 5 bullet points: ' . $document) ->generate() ->text(); } } Bind this in a service provider and inject it wherever needed. The approach keeps provider decisions out of controllers and makes them testable in isolation. Provider Failover: Catching Provider Errors The SDK throws distinct exceptions for provider-side failures: - Laravel\AI\Exceptions\RateLimitedException - HTTP 429 from the provider - Laravel\AI\Exceptions\ProviderOverloadedException - HTTP 5xx from the provider You can catch these to implement fallback logic: use Laravel\AI\Exceptions\RateLimitedException; use Laravel\AI\Exceptions\ProviderOverloadedException; use Illuminate\Support\Facades\AI; use Illuminate\Support\Facades\Log; function generateWithFallback(string $prompt): string { $providers = [ 'gpt-4o-mini', // primary: OpenAI 'claude-haiku-3-5', // fallback 1: Anthropic 'llama3-70b-8192', // fallback 2: Groq ]; foreach ($providers as $model) { try { return AI::text() ->using($model) ->prompt($prompt) ->generate() ->text(); } catch (RateLimitedException | ProviderOverloadedException $e) { Log::warning('AI provider unavailable, trying next', [ 'model' => $model, 'error' => $e->getMessage(), ]); continue; } } throw new \RuntimeException('All AI providers failed.'); } Handle both exception types explicitly - a 429 (rate limit) and a 503 (overload) have different implications for retry strategy. In a queue-based system, you may want to release the job back to the queue with exponential backoff instead of falling over to a different provider immediately. OpenRouter as a Multi-Model Aggregator OpenRouter is a single API endpoint that routes to 100+ models. It is worth understanding the trade-off before using it: Advantages: - One API key, one billing dashboard, access to models from OpenAI, Anthropic, Mistral, Meta, and dozens of others - Automatic cost-based routing: configure a budget and OpenRouter picks the cheapest capable model - Useful for prototyping without juggling multiple provider accounts Disadvantages: - Per-token pricing markup compared to direct provider calls - An extra network hop adds latency - Provider-specific features (e.g. OpenAI's function calling schema format vs Anthropic's tool use format) are abstracted away - if you rely on provider-specific behaviour, OpenRouter may not expose it To add OpenRouter as a named provider: // config/ai.php 'openrouter' => [ 'driver' => 'openrouter', 'api_key' => env('OPENROUTER_API_KEY'), ], $result = AI::text() ->using('openrouter:anthropic/claude-3-haiku') ->prompt($prompt) ->generate(); For the main Laravel AI SDK: Complete Guide to Building AI Applications covering agents, embeddings, MCP, and structured output, that pillar article has broader coverage of the full SDK surface area. Provider-Specific Capabilities: What Each Provider Supports Not every provider supports every SDK feature. Before routing a task to a provider, verify capability: | Feature | OpenAI | Anthropic | Groq | Gemini | Mistral | |---|---|---|---|---|---| | Text generation | Yes | Yes | Yes | Yes | Yes | | Tool calling (agents) | Yes | Yes | Yes | Yes | Yes | | Structured output | Yes | Yes | Limited | Yes | Yes | | Image generation | Yes (DALL-E) | No | No | Yes | No | | Audio transcription | Yes (Whisper) | No | Yes | No | No | | Embeddings | Yes | No | No | Yes | Yes | This matters because if you write an agent that uses image generation and route it to Anthropic, the SDK will throw. Build capability checks into your router if you support a mix of features: // Pseudocode: route based on task type public function routeModel(string $taskType): string { return match($taskType) { 'image' => 'dall-e-3', // OpenAI only 'transcription' => 'whisper-1', // OpenAI only 'embeddings' => 'text-embedding-3-small', // OpenAI only 'fast-chat' => 'llama3-70b-8192', // Groq default => 'gpt-4o-mini', }; } Testing Multi-Provider Logic Without API Calls The SDK ships with test fakes that prevent any network call from leaving your test suite: use Laravel\AI\Fakes\TextFake; use Illuminate\Support\Facades\AI; public function test_provider_router_uses_groq_for_fast_tasks(): void { AI::fake([ 'text' => new TextFake('Mocked fast response'), ]); $router = new AiProviderRouter(); $result = $router->fast('What is 2 + 2?'); $this->assertSame('Mocked fast response', $result); } public function test_fallback_triggers_on_rate_limit(): void { // Simulate RateLimitedException on first call, success on second AI::fake([ 'text' => new TextFake( responses: ['Fallback response'], throwOnFirst: new \Laravel\AI\Exceptions\RateLimitedException(), ), ]); $result = generateWithFallback('Test prompt'); $this->assertSame('Fallback response', $result); } This is the correct approach - never let tests hit live providers. Tests that call real APIs are slow, cost money, and fail non-deterministically when the provider is down. Common Mistakes Storing provider keys in config() calls inside controllers. All API keys must live in .env , referenced via config/ai.php using env() . The config() helper is the only correct access point in application code. Expecting every provider to behave identically. The SDK abstracts the API surface, but output quality, token limits, response latency, and pricing are provider-specific. A prompt that works well on GPT-4o may produce weaker output from a smaller Groq model. Test with your actual prompts. Not pinning the minor version. laravel/ai is at v0.8.x and has not reached v1.0. The package can introduce breaking changes in minor versions before the stable release. Use ^0.8 in composer.json , not ^0 or * . Running multi-provider fallback in the HTTP request cycle. If the first provider times out after 30 seconds and you try two fallbacks, you're looking at a 90-second request. Queue AI work as jobs. Return a job ID to the client and poll for the result. Assuming Ollama (local) has the same performance characteristics as hosted providers. Ollama is useful for development and privacy-sensitive workloads, but inference speed depends on local hardware. Do not include Ollama in a production fallback chain unless you have the GPU resources to back it up. Configuring Providers Per Environment A common requirement is to use a cheap local model during development (to avoid API costs) and a hosted provider in production. You can handle this cleanly by conditionally loading provider configuration based on the app environment: // config/ai.php 'providers' => [ 'primary' => [ 'driver'
Comments
No comments yet. Start the discussion.