symfony/ai-open-router-platform
Symfony AI bridge for the OpenRouter platform. Provides integration for chat completions (including streaming), model listing, and rerank requests via OpenRouter’s API, enabling Symfony apps to access multiple LLM providers through a single gateway.
Install the Package:
composer require symfony/ai-open-router-platform
Optional: If using Symfony’s AiClient abstractions, also install:
composer require symfony/ai
Configure API Key:
Add to .env:
OPENROUTER_API_KEY=your_api_key_here
Load in config/services.php:
'openrouter' => [
'api_key' => env('OPENROUTER_API_KEY'),
'default_model' => 'openrouter/free', // or 'openrouter/mistral-latest'
],
First Use Case: Chat Completion
use Symfony\Component\AI\OpenRouter\Client;
use Symfony\Component\AI\Message\ChatMessage;
$client = new Client(
new \Symfony\Component\HttpClient\HttpClient(),
config('services.openrouter.api_key')
);
$response = $client->chat([
new ChatMessage('Hello, how are you?', 'user'),
]);
echo $response->getContent();
chat(), stream(), rerank()).// Using Symfony Client
$client = app(Client::class);
$response = $client->chat([
new ChatMessage('Explain Laravel dependency injection', 'user'),
], config('services.openrouter.default_model'));
// Using Laravel HTTP (direct)
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.openrouter.api_key'),
])->post('https://openrouter.ai/api/v1/chat/completions', [
'model' => config('services.openrouter.default_model'),
'messages' => [['role' => 'user', 'content' => 'Explain Laravel DI']],
]);
// Symfony Client (streaming)
$stream = $client->streamChat([
new ChatMessage('Stream this response', 'user'),
]);
foreach ($stream as $chunk) {
echo $chunk->getContent() . "\n";
}
// Laravel + Symfony Streaming (custom event handler)
$stream = $client->streamChat([new ChatMessage('Stream to Laravel')]);
$stream->onChunk(function ($chunk) {
event(new OpenRouterChunkReceived($chunk->getContent()));
});
// Define providers in config/services.php
'openrouter' => [
'providers' => [
'free' => 'openrouter/free',
'pro' => 'openrouter/mistral-latest',
],
'default' => 'free',
],
// Dynamic routing in service
$model = config('services.openrouter.providers.' . request()->input('model_type', config('services.openrouter.default')));
$response = $client->chat([new ChatMessage('Dynamic model!')], $model);
$client = app(Client::class);
$results = $client->rerank(
'Find the best match for "Laravel AI"',
['Laravel is a PHP framework', 'AI is artificial intelligence'],
config('services.openrouter.default_model')
);
Register the client in AppServiceProvider:
public function register()
{
$this->app->singleton(Client::class, function ($app) {
return new Client(
new \Symfony\Component\HttpClient\HttpClient(),
$app['config']['services.openrouter.api_key']
);
});
}
// app/Facades/OpenRouter.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class OpenRouter extends Facade
{
protected static function getFacadeAccessor() { return 'openrouter.client'; }
}
// Register in AppServiceProvider
$this->app->bind('openrouter.client', function ($app) {
return new \Symfony\Component\AI\OpenRouter\Client(
new \Symfony\Component\HttpClient\HttpClient(),
$app['config']['services.openrouter.api_key']
);
});
Usage:
use App\Facades\OpenRouter;
$response = OpenRouter::chat([new ChatMessage('Hello')]);
Wrap calls in a try-catch for OpenRouter-specific errors:
try {
$response = $client->chat([new ChatMessage('Test')]);
} catch (\Symfony\Component\AI\Exception\AiException $e) {
Log::error('OpenRouter error: ' . $e->getMessage());
return response()->json(['error' => 'AI service unavailable'], 503);
}
Use Laravel’s throttle middleware for API calls:
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('/ai/chat', [AIController::class, 'chat']);
});
Symfony Dependency Overhead
symfony/ai adds ~50KB and tightens coupling to Symfony.symfony/ai-open-router-platform and Laravel’s HTTP client.Streaming Quirks
StreamingResponse doesn’t natively integrate with Laravel’s event system.$stream = $client->streamChat([new ChatMessage('Stream')]);
$stream->onChunk(function ($chunk) {
event(new AiStreamChunk($chunk->getContent()));
});
Model Availability
openrouter/free) may have usage limits.$model = config('services.openrouter.default_model');
if (!OpenRouter::modelExists($model)) {
$model = 'openrouter/mistral-latest';
}
Authentication Leaks
.env and validate in bootstrap/app.php:
if (!env('OPENROUTER_API_KEY')) {
throw new \RuntimeException('OpenRouter API key not set.');
}
Response Parsing
choices).$response = $client->chat([new ChatMessage('Parse this')]);
$content = collect($response->getContent())->first()['message']['content'];
Enable HTTP Logging Configure Symfony’s HTTP client to log requests:
$client = new Client(
\Symfony\Component\HttpClient\HttpClient::create([
'debug' => true,
]),
config('services.openrouter.api_key')
);
Validate API Key Test connectivity with a simple request:
$response = Http::get('https://openrouter.ai/api/v1/models', [
'headers' => ['Authorization' => 'Bearer ' . config('services.openrouter.api_key')],
]);
Token Limits OpenRouter enforces token limits per request. Validate input:
$messages = [new ChatMessage('Long message...')];
$tokenCount = $client->estimateTokenCount($messages);
if ($tokenCount > 4096) { // OpenRouter’s max for free tier
throw new \RuntimeException('Message exceeds token limit.');
}
Custom Providers
Extend the Provider abstraction to support other APIs:
namespace App\Providers;
use Symfony\Component\AI\Provider\ProviderInterface;
class CustomProvider implements ProviderInterface
{
public function getModel(string $modelName): string
{
return match ($modelName) {
'custom' => 'openrouter/mistral-latest',
default => $modelName,
};
}
}
Laravel Events for Streaming Dispatch events for each chunk:
$stream = $client->streamChat([new ChatMessage('Event-driven')]);
$stream->onChunk(function ($chunk) {
event(new AiStreamEvent($chunk->getContent()));
});
Caching Responses Cache frequent queries (e.g., model listings):
How can I help you explore Laravel packages today?