symfony/ai-docker-model-runner-platform
Symfony AI bridge for Docker Model Runner. Connect Symfony apps to local/containerized models via Docker’s Model Runner API. Includes links to official docs and API reference; issues and PRs handled in the main Symfony AI repository.
Install the Package
composer require symfony/ai-docker-model-runner-platform
Ensure your project uses PHP 8.2+ and Symfony 7.x (Laravel 10+ compatible).
Configure Docker Model Runner
Add Docker Model Runner to your environment (e.g., via docker-compose.yml):
services:
model-runner:
image: docker.ai/model-runner:latest
ports:
- "8080:8080"
Update Laravel’s .env:
AI_DOCKER_MODEL_RUNNER_URL=http://localhost:8080
First Use Case: Text Completion
Create a Laravel service to wrap Symfony’s ModelClient:
// app/Services/AiService.php
namespace App\Services;
use Symfony\Component\Ai\ModelClient;
use Symfony\Component\Ai\ModelProviderInterface;
class AiService
{
public function __construct(
private ModelClient $modelClient,
private ModelProviderInterface $provider
) {}
public function generateText(string $prompt): string
{
return $this->modelClient->completion(
$this->provider->getModel(),
$prompt
);
}
}
Register the Service
Bind dependencies in AppServiceProvider:
public function register(): void
{
$this->app->bind(ModelClient::class, function ($app) {
return new ModelClient(
$app->make(ModelProviderInterface::class),
$app['http_client']
);
});
}
Test the Integration Call the service from a controller:
use App\Services\AiService;
public function chat(AiService $ai)
{
$response = $ai->generateText("Hello, world!");
return response()->json(['response' => $response]);
}
Leverage the Provider abstraction to dynamically route requests to different models:
// app/Providers/AiModelProvider.php
namespace App\Providers;
use Symfony\Component\Ai\ModelProviderInterface;
class AiModelProvider implements ModelProviderInterface
{
public function getModel(): string
{
// Route based on request or config
return config('ai.models.default') ?: 'ollama/llama3';
}
}
Use DeltaInterface for real-time streaming (e.g., chat apps):
use Symfony\Component\Ai\Streaming\StreamedResponse;
public function streamChat(AiService $ai)
{
$stream = $ai->streamCompletion("What's Laravel?");
return new StreamedResponse(
fn () => $stream->toStream(),
200,
['Content-Type' => 'text/event-stream']
);
}
Extract token usage for cost monitoring:
use Symfony\Component\Ai\Embedding\EmbeddingModelInterface;
public function generateEmbedding(EmbeddingModelInterface $model)
{
$embedding = $model->embed("Sample text");
$tokenUsage = $model->getTokenUsage(); // Track costs
// Log to Laravel's Telescope or database
\Log::info("Token usage: {$tokenUsage->inputTokens} input, {$tokenUsage->outputTokens} output");
return $embedding;
}
Create a facade for cleaner syntax:
// app/Facades/Ai.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Ai extends Facade
{
protected static function getFacadeAccessor(): string
{
return AiService::class;
}
}
Usage:
use App\Facades\Ai;
Ai::generateText("Hello") // Instead of $aiService->generateText("Hello")
Bind Symfony’s HttpClient to Laravel’s container:
// config/services.php
'http_client' => [
'client' => Symfony\Component\HttpClient\HttpClient::create([
'base_uri' => env('AI_DOCKER_MODEL_Runner_URL'),
]),
],
Use Laravel Forge or Docker Compose for deployment:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
depends_on:
- model-runner
model-runner:
image: docker.ai/model-runner:latest
environment:
- MODELS=ollama/llama3,baai/bge
Map Symfony exceptions to Laravel’s problem details:
use Symfony\Component\Ai\Exception\InvalidArgumentException;
use Symfony\Component\HttpFoundation\JsonResponse;
try {
$ai->generateText("Invalid prompt");
} catch (InvalidArgumentException $e) {
return new JsonResponse([
'error' => 'Invalid input',
'details' => $e->getMessage(),
], 400);
}
Mock the ModelClient in PHPUnit:
use Symfony\Component\Ai\ModelClient;
public function testAiService()
{
$mockClient = $this->createMock(ModelClient::class);
$mockClient->method('completion')->willReturn("Mocked response");
$service = new AiService($mockClient, $this->createMock(ModelProviderInterface::class));
$this->assertEquals("Mocked response", $service->generateText("Test"));
}
# .github/workflows/test.yml
services:
docker:
image: docker:dind
ModelClient API.// app/Dtos/AiRequest.php
namespace App\Dtos;
class AiRequest
{
public function __construct(
public string $prompt,
public ?string $model = null,
public array $options = []
) {}
}
DeltaInterface may not play well with Laravel’s StreamedResponse if not configured properly.$stream = $ai->streamCompletion($prompt);
$response = new StreamedResponse(
fn () => $stream->toStream(),
200,
['Content-Type' => 'text/event-stream']
);
$response->send(); // Critical for cleanup
EmbeddingModelInterface for custom models:
use Symfony\Component\Ai\Embedding\TokenUsage;
class CustomEmbeddingModel implements EmbeddingModelInterface
{
public function embed(string $text): array
{
$result = $this->callDockerModel($text);
return [
'embedding' => $result['embedding'],
'tokenUsage' => new TokenUsage(100, 50), // Mock values
];
}
}
// config/ai.php
return [
'models' => [
'default' => env('AI_DEFAULT_MODEL', 'ollama/llama3'),
],
'docker' => [
'url' => env('AI_DOCKER_MODEL_RUNNER_URL', 'http://localhost:8080'),
],
];
Enable Symfony’s HTTP client logging:
// config/services.php
'http_client' => [
'client' => HttpClient::create([
'base_uri' => env('AI_DOCKER_MODEL_RUNNER_URL'),
'logger' => function () {
return new \Monolog\Logger('ai_docker', [
new \Monolog\Handler\StreamHandler(storage_path('logs/ai_docker.log')),
]);
},
]),
],
Check for malformed JSON or unexpected types:
use Symfony\Component\Ai\Exception\RuntimeException;
try {
$response = $ai->generateText("Test");
if (!is_string($response)) {
throw new RuntimeException("Invalid response type: " . gettype($response));
}
} catch (RuntimeException $e) {
\Log::
How can I help you explore Laravel packages today?