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\ProviderConfigurationException;
use Netresearch\NrLlm\Provider\Exception\ProviderConnectionException;
use Netresearch\NrLlm\Provider\Exception\ProviderResponseException;
use Netresearch\NrLlm\Provider\Exception\UnsupportedFeatureException;
use Netresearch\NrLlm\Exception\InvalidArgumentException;
try {
$response = $this->llmManager->chat($messages);
} catch (ProviderConfigurationException $e) {
// Invalid or missing provider configuration
$this->logger->error('Configuration error: ' . $e->getMessage());
} catch (ProviderConnectionException $e) {
// Connection to provider failed
$this->logger->error('Connection failed: ' . $e->getMessage());
} catch (ProviderResponseException $e) {
// Provider returned an error response
$this->logger->error('Provider response error: ' . $e->getMessage());
} catch (UnsupportedFeatureException $e) {
// Requested feature not supported by provider
$this->logger->warning('Unsupported feature: ' . $e->getMessage());
} 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
Note
PSR-14 events (BeforeRequestEvent, AfterResponseEvent) are planned
for a future release. The event classes do not exist yet in the current
codebase. Once implemented, they will allow listeners to intercept and modify
requests before they are sent to providers, and to inspect responses after
they are received.
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.