Developer guide
This guide covers technical details for developers integrating the LLM extension into their TYPO3 projects.
Core concepts
Architecture overview
The extension follows a layered architecture:
- Providers - Handle direct API communication.
Llm- Orchestrates providers and provides unified API.Service Manager - Feature services - High-level services for specific tasks.
- Domain models - Response objects and value types.
┌─────────────────────────────────────────┐
│ Your Application Code │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Feature Services │
│ (Completion, Embedding, Vision, etc.) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ LlmServiceManager │
│ (Provider selection & routing) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ Providers │
│ (OpenAI, Claude, Gemini, etc.) │
└─────────────────────────────────────────┘
Copied!
Dependency injection
All services are available via dependency injection:
Example: Injecting LLM services
use Netresearch\NrLlm\Service\LlmServiceManager;
use Netresearch\NrLlm\Service\Feature\CompletionService;
use Netresearch\NrLlm\Service\Feature\EmbeddingService;
use Netresearch\NrLlm\Service\Feature\VisionService;
use Netresearch\NrLlm\Service\Feature\TranslationService;
class MyController
{
public function __construct(
private readonly LlmServiceManager $llmManager,
private readonly CompletionService $completionService,
private readonly EmbeddingService $embeddingService,
private readonly VisionService $visionService,
private readonly TranslationService $translationService,
) {}
}
Copied!
Using LlmServiceManager
Basic chat
Example: Basic chat request
$messages = [
['role' => 'system', 'content' => 'You are a helpful assistant.'],
['role' => 'user', 'content' => 'What is TYPO3?'],
];
$response = $this->llmManager->chat($messages);
// Response properties
$content = $response->content; // string
$model = $response->model; // string
$finishReason = $response->finishReason; // string
$usage = $response->usage; // UsageStatistics
// UsageStatistics
$promptTokens = $usage->promptTokens;
$completionTokens = $usage->completionTokens;
$totalTokens = $usage->totalTokens;
Copied!
Chat with options
Example: Chat with configuration options
use Netresearch\NrLlm\Service\Option\ChatOptions;
// Using ChatOptions object
$options = ChatOptions::creative()
->withMaxTokens(2000)
->withSystemPrompt('You are a creative writer.');
$response = $this->llmManager->chat($messages, $options);
// Or using array
$response = $this->llmManager->chat($messages, [
'provider' => 'claude',
'model' => 'claude-opus-4-5-20251101',
'temperature' => 1.2,
'max_tokens' => 2000,
'top_p' => 0.9,
'frequency_penalty' => 0.5,
'presence_penalty' => 0.5,
]);
Copied!
Simple completion
Example: Quick completion from a prompt
// Quick completion from a prompt
$response = $this->llmManager->complete('Explain recursion in programming');
Copied!
Embeddings
Example: Generating embeddings
// Single text
$response = $this->llmManager->embed('Hello, world!');
$vector = $response->getVector(); // array<float>
// Multiple texts
$response = $this->llmManager->embed(['Text 1', 'Text 2', 'Text 3']);
$vectors = $response->embeddings; // array<array<float>>
Copied!
Streaming
Example: Streaming chat responses
$stream = $this->llmManager->streamChat($messages);
foreach ($stream as $chunk) {
echo $chunk;
ob_flush();
flush();
}
Copied!
Tool/function calling
Example: Tool/function calling
$tools = [
[
'type' => 'function',
'function' => [
'name' => 'get_weather',
'description' => 'Get current weather for a location',
'parameters' => [
'type' => 'object',
'properties' => [
'location' => [
'type' => 'string',
'description' => 'City name',
],
'unit' => [
'type' => 'string',
'enum' => ['celsius', 'fahrenheit'],
],
],
'required' => ['location'],
],
],
],
];
$response = $this->llmManager->chatWithTools($messages, $tools);
if ($response->hasToolCalls()) {
foreach ($response->toolCalls as $toolCall) {
$functionName = $toolCall['function']['name'];
$arguments = json_decode($toolCall['function']['arguments'], true);
// Execute your function
$result = match ($functionName) {
'get_weather' => $this->getWeather($arguments['location']),
default => throw new \RuntimeException("Unknown function: {$functionName}"),
};
// Continue conversation with result
$messages[] = [
'role' => 'assistant',
'content' => null,
'tool_calls' => [$toolCall],
];
$messages[] = [
'role' => 'tool',
'tool_call_id' => $toolCall['id'],
'content' => json_encode($result),
];
$response = $this->llmManager->chat($messages);
}
}
Copied!
Response objects
CompletionResponse
Domain/Model/CompletionResponse.php
namespace Netresearch\NrLlm\Domain\Model;
final class CompletionResponse
{
public readonly string $content;
public readonly string $model;
public readonly UsageStatistics $usage;
public readonly string $finishReason;
public readonly string $provider;
public readonly ?array $toolCalls;
public function isComplete(): bool; // finished normally
public function wasTruncated(): bool; // hit max_tokens
public function wasFiltered(): bool; // content filtered
public function hasToolCalls(): bool; // has tool calls
public function getText(): string; // alias for content
}
Copied!
EmbeddingResponse
Domain/Model/EmbeddingResponse.php
namespace Netresearch\NrLlm\Domain\Model;
final class EmbeddingResponse
{
/** @var array<int, array<int, float>> */
public readonly array $embeddings;
public readonly string $model;
public readonly UsageStatistics $usage;
public readonly string $provider;
public function getVector(): array; // First embedding
public static function cosineSimilarity(array $a, array $b): float;
}
Copied!
UsageStatistics
Domain/Model/UsageStatistics.php
namespace Netresearch\NrLlm\Domain\Model;
final readonly class UsageStatistics
{
public int $promptTokens;
public int $completionTokens;
public int $totalTokens;
public ?float $estimatedCost;
}
Copied!
Creating custom providers
Implement a custom provider by extending Abstract:
Example: Custom provider implementation
<?php
namespace MyVendor\MyExtension\Provider;
use Netresearch\NrLlm\Provider\AbstractProvider;
use Netresearch\NrLlm\Provider\Contract\ProviderInterface;
class MyCustomProvider extends AbstractProvider implements ProviderInterface
{
protected string $baseUrl = 'https://api.example.com/v1';
public function getName(): string
{
return 'My Custom Provider';
}
public function getIdentifier(): string
{
return 'custom';
}
public function isConfigured(): bool
{
return !empty($this->apiKey);
}
public function chatCompletion(array $messages, array $options = []): CompletionResponse
{
$payload = $this->buildChatPayload($messages, $options);
$response = $this->sendRequest('chat', $payload);
return new CompletionResponse(
content: $response['choices'][0]['message']['content'],
model: $response['model'],
usage: $this->parseUsage($response['usage']),
finishReason: $response['choices'][0]['finish_reason'],
provider: $this->getIdentifier(),
);
}
// Implement other required methods...
}
Copied!
Register your provider in Services.:
Configuration/Services.yaml
MyVendor\MyExtension\Provider\MyCustomProvider:
arguments:
$httpClient: '@Psr\Http\Client\ClientInterface'
$requestFactory: '@Psr\Http\Message\RequestFactoryInterface'
$streamFactory: '@Psr\Http\Message\StreamFactoryInterface'
$logger: '@Psr\Log\LoggerInterface'
tags:
- name: nr_llm.provider
priority: 50
Copied!
Error handling
The extension throws specific exceptions:
Example: Error handling
use Netresearch\NrLlm\Provider\Exception\ProviderException;
use Netresearch\NrLlm\Provider\Exception\AuthenticationException;
use Netresearch\NrLlm\Provider\Exception\RateLimitException;
use Netresearch\NrLlm\Exception\InvalidArgumentException;
try {
$response = $this->llmManager->chat($messages);
} catch (AuthenticationException $e) {
// Invalid or missing API key
$this->logger->error('Authentication failed: ' . $e->getMessage());
} catch (RateLimitException $e) {
// Rate limit exceeded
$retryAfter = $e->getRetryAfter(); // seconds to wait
$this->logger->warning("Rate limited. Retry after {$retryAfter}s");
} catch (ProviderException $e) {
// General provider error
$this->logger->error('Provider error: ' . $e->getMessage());
} catch (InvalidArgumentException $e) {
// Invalid parameters
$this->logger->error('Invalid argument: ' . $e->getMessage());
}
Copied!
Events
The extension dispatches PSR-14 events:
Example: Event listener implementation
use Netresearch\NrLlm\Event\BeforeRequestEvent;
use Netresearch\NrLlm\Event\AfterResponseEvent;
class MyEventListener
{
public function beforeRequest(BeforeRequestEvent $event): void
{
$messages = $event->getMessages();
$options = $event->getOptions();
$provider = $event->getProvider();
// Modify options
$event->setOptions(array_merge($options, ['my_option' => 'value']));
}
public function afterResponse(AfterResponseEvent $event): void
{
$response = $event->getResponse();
$usage = $response->usage;
// Log usage, track costs, etc.
}
}
Copied!
Register in Services.:
Configuration/Services.yaml
MyVendor\MyExtension\EventListener\MyEventListener:
tags:
- name: event.listener
identifier: 'myextension/before-request'
method: 'beforeRequest'
event: Netresearch\NrLlm\Event\BeforeRequestEvent
Copied!
Best practices
- Use feature services for common tasks instead of raw
Llm.Service Manager - Enable caching for deterministic operations like embeddings.
- Handle errors gracefully with proper try-catch blocks.
- Sanitize input before sending to LLM providers.
- Validate output and treat LLM responses as untrusted.
- Use streaming for long responses to improve UX.
- Set reasonable timeouts based on expected response times.
- Monitor usage to control costs and prevent abuse.