symfony/ai-open-ai-platform
OpenAI platform integration for Symfony AI Platform, adding GPT chat completions, embeddings, DALL-E image generation, Whisper audio transcription, and text-to-speech support.
composer require symfony/ai-open-ai-platform
.env:
AI_OPEN_AI_KEY="your-openai-api-key"
use Symfony\AI\OpenAI\Client\ChatCompletionClientInterface;
class ChatController extends AbstractController
{
public function __construct(
private ChatCompletionClientInterface $chatClient
) {}
public function ask(ChatMessage $message): Response
{
$response = $this->chatClient->complete(
'gpt-4',
[$message]
);
return $this->json($response->getContent());
}
}
// src/Controller/AiChatController.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class AiChatController extends AbstractController
{
public function __invoke(
Request $request,
ChatCompletionClientInterface $chatClient
): Response {
$prompt = $request->request->get('prompt');
$response = $chatClient->complete(
'gpt-3.5-turbo',
[new ChatMessage(ChatRole::User, $prompt)]
);
return $this->json(['reply' => $response->getContent()]);
}
}
Route it:
# config/routes.yaml
ai_chat:
path: /ai/chat
controller: App\Controller\AiChatController
methods: POST
Chat Completions
// Single turn
$response = $chatClient->complete(
'gpt-4',
[new ChatMessage(ChatRole::User, 'Explain quantum computing')]
);
// Multi-turn conversation
$messages = [
new ChatMessage(ChatRole::System, 'You are a helpful assistant'),
new ChatMessage(ChatRole::User, 'What is PHP?'),
new ChatMessage(ChatRole::Assistant, 'PHP is a server-side scripting language...'),
new ChatMessage(ChatRole::User, 'Give me an example')
];
$response = $chatClient->complete('gpt-3.5-turbo', $messages);
Streaming Responses
use Symfony\AI\OpenAI\Client\StreamingChatCompletionClientInterface;
$stream = $streamingChatClient->stream(
'gpt-3.5-turbo',
[new ChatMessage(ChatRole::User, 'Tell me a joke')]
);
foreach ($stream as $delta) {
echo $delta->getContent();
}
Embeddings
$embeddingClient = $container->get(EmbeddingClientInterface::class);
$embeddings = $embeddingClient->create('text-embedding-ada-002', ['Your text here']);
Batch Requests (v0.14.0)
// Submit many chat jobs at once via the asynchronous jobs API
$batch = $batchClient->submit([
new ChatMessage(ChatRole::User, 'Summarise document A'),
new ChatMessage(ChatRole::User, 'Summarise document B'),
new ChatMessage(ChatRole::User, 'Summarise document C'),
], 'gpt-3.5-turbo');
// Persist the batch id; poll later from a worker/console command
$batchId = $batch->getId();
// In a scheduled command
$result = $batchClient->retrieve($batchId);
foreach ($result->getResponses() as $response) {
$this->logger->info($response->getContent());
}
Realtime Sessions — Voice/Audio Agents (v0.14.0)
use Symfony\AI\OpenAI\Client\RealtimeClientInterface;
$session = $realtimeClient->createSession([
'model' => 'gpt-4o-realtime-preview',
'voice' => 'alloy',
'modalities' => ['audio', 'text'],
]);
// Wire $session->getUrl() / ephemeral token to your browser WebRTC client
return $this->json([
'url' => $session->getUrl(),
'token' => $session->getClientSecret(),
]);
Cost Tracking via TokenUsage (v0.14.0)
$response = $chatClient->complete('gpt-4', $messages);
$usage = $response->getUsage();
$cost = $this->costCalculator->estimate(
$usage->getModel(), // NEW: model now attached to usage
$usage->getPromptTokens(),
$usage->getCompletionTokens()
);
Service Layer Abstraction
// src/Service/AiChatService.php
class AiChatService
{
public function __construct(
private ChatCompletionClientInterface $chatClient
) {}
public function generateResponse(string $prompt): string
{
$response = $this->chatClient->complete(
'gpt-3.5-turbo',
[new ChatMessage(ChatRole::User, $prompt)]
);
return $response->getContent();
}
}
Event-Driven Architecture
$eventDispatcher->addListener(
ChatCompletionEvent::class,
function (ChatCompletionEvent $event) {
$this->logger->info('AI Response:', ['content' => $event->getContent()]);
}
);
Async Batch Processing
// src/Command/DispatchBatchCommand.php
class DispatchBatchCommand extends Command
{
protected static string $defaultName = 'app:ai:dispatch-batch';
public function __construct(
private BatchClientInterface $batchClient,
private EntityManagerInterface $em
) { parent::__construct(); }
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jobs = $this->em->getRepository(PromptJob::class)->findPending();
$batch = $this->batchClient->submit(
array_map(fn($j) => new ChatMessage(ChatRole::User, $j->getPrompt()), $jobs)
);
$this->em->flush(); // store $batch->getId() against jobs
return Command::SUCCESS;
}
}
Realtime Voice Agent Endpoint
// Return an ephemeral session to the frontend; never expose your real API key
class RealtimeTokenController extends AbstractController
{
public function __invoke(RealtimeClientInterface $realtimeClient): Response
{
return $this->json($realtimeClient->createEphemeralSession('gpt-4o-realtime-preview'));
}
}
Environment-Specific Config
# config/packages/ai_open_ai.yaml
ai_open_ai:
client:
api_key: '%env(AI_OPEN_AI_KEY)%'
base_uri: '%env(AI_OPEN_AI_BASE_URI)%'
timeout: 30
retries: 3
model_routing:
default: 'gpt-3.5-turbo'
premium: 'gpt-4'
realtime: 'gpt-4o-realtime-preview'
Model Routing
$model = $user->isPremium() ? 'gpt-4' : 'gpt-3.5-turbo';
$response = $chatClient->complete($model, $messages);
Model Catalog Pinning
gpt-4), verify it's still listed and pin it in your config rather than relying on defaults.API Key Management
ai_open_ai:
client:
api_key: '%env(AI_OPEN_AI_KEY)%'
Rate Limiting
try {
$response = $httpClient->request('POST', $url);
} catch (TransportExceptionInterface | ClientExceptionInterface $e) {
if ($e->getCode() === 429) {
sleep(2);
retry();
}
}
Token Limits
$tokenCount = $this->countTokens($prompt);
if ($tokenCount > 4000) {
throw new \RuntimeException('Prompt too long');
}
Streaming Quirks
DeltaInterface objects, not raw strings$stream = $streamingChatClient->stream('gpt-3.5-turbo', [$message]);
$fullResponse = '';
foreach ($stream as $delta) {
$fullResponse .= $delta->getContent();
}
Batch Requests Are Asynchronous (v0.14.0)
submit() returns immediately with a batch id — it does not return results.custom_id if you need correlation.Realtime Sessions Are Stateful (v0.14.0)
TokenUsage aggressively on realtime sessions.TokenUsage Now Carries the Model (v0.14.0)
getUsage()->getModel() is now populated — use it for accurate per-model cost reporting instead of inferring the model from your request.Enable Debug Mode
ai_open_ai:
client:
debug: true
var/log/dev.logInspect API Errors
try {
$response = $chatClient->complete('invalid-model', [$message]);
} catch (ApiErrorException $e) {
$error = $e->getError();
if ($error->getType() === 'invalid_request_error') {
// Model not found, etc. — often after a model catalog update
}
}
Token Counting
use Symfony\AI\OpenAI\Tokenizer\TokenizerInterface;
$tokenizer = $container->get(TokenizerInterface::class);
$tokenCount = $tokenizer->countTokens($prompt);
Cassette Verification (v0.14.0)
Custom Model Clients
// src/OpenAI/CustomChatClient.php
class CustomChatClient implements ChatCompletionClientInterface
{
public function __construct(
private ChatCompletionClientInterface $decorated
) {}
public function complete(string $model, array $messages, ?array $options = null): ChatCompletionResponseInterface
{
if (str_contains($messages[0]->getContent(), 'hack')) {
throw new \RuntimeException('Blocked prompt');
}
return $this->decorated->complete($model, $messages, $options);
}
}
Register as service:
services:
Symfony\AI\OpenAI\Client\ChatCompletionClientInterface: '@App\OpenAI\CustomChatClient'
Custom Batch Client Decoration
BatchClientInterface to add metrics, idempotency keys, or a DB-backed ledger of submitted batches.Realtime Session Hooks
RealtimeClientInterface to enforce per-user session caps and inject moderation on emitted audio.Response Transformers
// src/OpenAI/ResponseTransformer.php
class ResponseTransformer
{
public function transform(ChatCompletionResponseInterface $response): array
{
$usage = $response->getUsage();
return [
'content' => $response->getContent(),
'model' => $usage?->getModel(),
'cost' => $usage ? $this->costFor($usage) : null,
];
}
}
How can I help you explore Laravel packages today?