A unified Large Language Model (LLM) provider abstraction layer for TYPO3 v13.4+.
This extension provides a standardized interface to interact with multiple AI providers
(OpenAI, Anthropic Claude, Google Gemini) through a single, consistent API. It includes
specialized services for common AI tasks like text completion, translation, embeddings,
and image analysis.
🚀 Quick start
Get started quickly with installation and basic usage examples.
🔧 Configuration
Configure API keys, providers, and extension settings.
👨💻 Developer guide
Technical documentation for developers integrating LLM capabilities.
🏗️ Architecture
Three-tier configuration architecture and service design.
The TYPO3 LLM extension provides a unified abstraction layer for integrating
Large Language Models (LLMs) into TYPO3 applications. It enables developers to:
Access multiple AI providers through a single, consistent API.
Switch providers transparently without code changes.
Leverage specialized services for common AI tasks.
Cache responses to reduce API costs and improve performance.
Remove any TypoScript includes referencing the extension.
Configuration
The extension uses a database-based configuration architecture with three levels:
Providers, Models, and Configurations. All management is done through the
TYPO3 backend module.
Maximum context window in tokens (e.g., 128000 for GPT-5).
max_output_tokens
max_output_tokens
Type
integer
Default
(model default)
Maximum output tokens (e.g., 16384).
capabilities
capabilities
Type
string (CSV)
Default
chat
Comma-separated list of supported features:
chat - Chat completion.
completion - Text completion.
embeddings - Text-to-vector.
vision - Image analysis.
streaming - Real-time streaming.
tools - Function/tool calling.
cost_input
cost_input
Type
integer
Default
0
Cost per 1M input tokens in cents (for cost tracking).
cost_output
cost_output
Type
integer
Default
0
Cost per 1M output tokens in cents.
is_default
is_default
Type
boolean
Default
false
Mark as default model for this provider.
Fetching models from providers
Use the Fetch Models action to automatically retrieve available models
from the provider's API. This populates the model list with the provider's
current offerings.
LLM configuration
Configurations define specific use cases with model selection and parameters.
Create configurations in Admin Tools > LLM > Configurations.
Required fields
identifier (config)
identifier (config)
Type
string
Required
true
Unique slug for programmatic access (e.g., blog-summarizer).
name (config)
name (config)
Type
string
Required
true
Display name (e.g., Blog Post Summarizer).
model
model
Type
reference
Required
true
Reference to the model to use.
system_prompt
system_prompt
Type
text
Required
true
System message that sets the AI's behavior and context.
Optional fields
temperature
temperature
Type
float
Default
0.7
Creativity level from 0.0 (deterministic) to 2.0 (creative).
max_tokens (config)
max_tokens (config)
Type
integer
Default
(model default)
Maximum response length in tokens.
top_p
top_p
Type
float
Default
1.0
Nucleus sampling parameter (0.0 - 1.0).
frequency_penalty
frequency_penalty
Type
float
Default
0.0
Reduces word repetition (-2.0 to 2.0).
presence_penalty
presence_penalty
Type
float
Default
0.0
Encourages topic diversity (-2.0 to 2.0).
use_case_type
use_case_type
Type
string
Default
chat
The type of task:
chat - Conversational interactions.
completion - Text completion.
embedding - Vector generation.
translation - Language translation.
Using configurations
Retrieve configurations programmatically:
Example: Using configurations in a controller
useNetresearch\NrLlm\Domain\Repository\LlmConfigurationRepository;
useNetresearch\NrLlm\Provider\ProviderAdapterRegistry;
classMyController{
publicfunction__construct(
private readonly LlmConfigurationRepository $configRepository,
private readonly ProviderAdapterRegistry $adapterRegistry,
){}
publicfunctionprocessAction(): void{
// Get configuration by identifier
$config = $this->configRepository->findByIdentifier('blog-summarizer');
// Get the model and provider
$model = $config->getModel();
$provider = $model->getProvider();
// Create adapter and make requests
$adapter = $this->adapterRegistry->createAdapterFromModel($model);
$response = $adapter->chatCompletion($messages, $config->toOptions());
}
}
Copied!
TypoScript settings
Runtime settings can be configured via TypoScript:
namespaceNetresearch\NrLlm\Domain\Model;
finalclassCompletionResponse{
public readonly string $content;
public readonly string $model;
public readonly UsageStatistics $usage;
public readonly string $finishReason;
public readonly string $provider;
public readonly ?array $toolCalls;
publicfunctionisComplete(): bool; // finished normallypublicfunctionwasTruncated(): bool; // hit max_tokenspublicfunctionwasFiltered(): bool; // content filteredpublicfunctionhasToolCalls(): bool; // has tool callspublicfunctiongetText(): string; // alias for content
}
Copied!
EmbeddingResponse
Domain/Model/EmbeddingResponse.php
namespaceNetresearch\NrLlm\Domain\Model;
finalclassEmbeddingResponse{
/** @var array<int, array<int, float>> */public readonly array $embeddings;
public readonly string $model;
public readonly UsageStatistics $usage;
public readonly string $provider;
publicfunctiongetVector(): array; // First embeddingpublicstaticfunctioncosineSimilarity(array $a, array $b): float;
}
Copied!
UsageStatistics
Domain/Model/UsageStatistics.php
namespaceNetresearch\NrLlm\Domain\Model;
final readonly classUsageStatistics{
public int $promptTokens;
public int $completionTokens;
public int $totalTokens;
public ?float $estimatedCost;
}
Copied!
Creating custom providers
Implement a custom provider by extending AbstractProvider:
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 LlmServiceManager.
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.
Feature services
High-level AI services for TYPO3 with prompt engineering and response parsing.
The feature services layer provides domain-specific AI capabilities for TYPO3 extensions.
Each service wraps the core LlmServiceManager with specialized prompts, response parsing,
and configuration optimized for specific use cases.
// Standard completion
$response = $completionService->complete($prompt);
// JSON output
$data = $completionService->completeJson('List 5 colors as a JSON array');
// Markdown output
$markdown = $completionService->completeMarkdown('Write docs for this API');
// Factual (low creativity, high consistency)
$response = $completionService->completeFactual('What is the capital of France?');
// Creative (high creativity)
$response = $completionService->completeCreative('Write a haiku about coding');
The extension includes a comprehensive test suite:
Test Type
Count
Purpose
Unit tests
384
Individual class and method testing.
Integration tests
39
Service interaction and provider testing.
E2E tests
11
Full workflow testing with real APIs.
Functional tests
39
TYPO3 framework integration.
Property tests
25
Fuzzy/property-based testing.
Running tests
Prerequisites
Install development dependencies
# Install development dependencies
composer install --dev
Copied!
Unit tests
Run unit tests
# Recommended: Use runTests.sh (Docker-based, consistent environment)
Build/Scripts/runTests.sh -s unit
# With specific PHP version
Build/Scripts/runTests.sh -s unit -p 8.3
# Alternative: Via Composer script
composer ci:test:php:unit
Copied!
Integration tests
Run integration tests
# Run integration tests (requires mock server or API keys)
composer ci:test:php:integration
# With real API (set environment variables first)
OPENAI_API_KEY=sk-... Build/Scripts/runTests.sh -s unit
Copied!
Functional tests
Run functional tests
# Run TYPO3 functional tests
Build/Scripts/runTests.sh -s functional
# Alternative: Via Composer script
composer ci:test:php:functional
Copied!
All tests
Run complete test suite
# Run all test suites via runTests.sh
Build/Scripts/runTests.sh -s unit
Build/Scripts/runTests.sh -s functional
# Run code quality checks
Build/Scripts/runTests.sh -s cgl
Build/Scripts/runTests.sh -s phpstan
We needed to support multiple LLM providers (OpenAI, Anthropic Claude, Google Gemini)
while maintaining a consistent API for consumers. Each provider has different:
AbstractProvider base class with shared functionality.
LlmServiceManager as the unified entry point.
Consequences
Positive:
●● Consumers use single API regardless of provider.
●● Easy to add new providers.
● Capability checking via interface detection.
●● Provider switching requires no code changes.
Negative:
✕ Lowest common denominator for shared features.
◑ Provider-specific features require direct provider access.
◑ Additional abstraction layer complexity.
Net Score: +5.5 (Strong positive impact - abstraction enables flexibility and maintainability)
Alternatives considered
Single monolithic class: Rejected due to maintenance complexity.
Strategy pattern only: Insufficient for capability detection.
Factory pattern: Used in combination with interfaces.
ADR-002: Feature Services Architecture
Status
Accepted (2024-02)
Context
Common LLM tasks (translation, image analysis, embeddings) require:
Specialized prompts and configurations
Pre/post-processing logic
Caching strategies
Quality control measures
Decision
Create dedicated Feature Services for high-level operations:
CompletionService: Text generation with format control.
EmbeddingService: Vector operations with caching.
VisionService: Image analysis with specialized prompts.
TranslationService: Language translation with quality scoring.
Each service:
Uses LlmServiceManager internally.
Provides domain-specific methods.
Handles caching and optimization.
Returns typed response objects.
Consequences
Positive:
●● Clear separation of concerns.
● Reusable, tested implementations.
●● Consistent behavior across use cases.
● Built-in best practices (caching, prompts).
Negative:
◑ Additional classes to maintain.
◑ Potential duplication with manager methods.
◑ Learning curve for service selection.
Net Score: +6.5 (Strong positive impact - services provide high-level abstractions with best practices)
ADR-003: Typed Response Objects
Status
Accepted (2024-01)
Context
Provider APIs return different response structures. We needed to:
Provide consistent response format to consumers.
Enable IDE autocompletion and type checking.
Include relevant metadata (usage, model, finish reason).
Decision
Use immutable value objects for responses:
Example: CompletionResponse value object
finalclassCompletionResponse{
publicfunction__construct(
public readonly string $content,
public readonly string $model,
public readonly UsageStatistics $usage,
public readonly string $finishReason,
public readonly string $provider,
public readonly ?array $toolCalls = null,
){}
}
Copied!
Key characteristics:
final classes prevent inheritance issues.
readonly properties ensure immutability.
Constructor promotion for concise definition.
Nullable for optional data.
Consequences
Positive:
●● Strong typing with IDE support.
● Immutable objects are thread-safe.
●● Clear API contract.
● Easy testing and mocking.
Negative:
◑ Cannot extend responses.
✕ Breaking changes require new properties.
◑ Slight memory overhead vs arrays.
Net Score: +5.5 (Strong positive impact - type safety and immutability outweigh flexibility limitations)
ADR-004: PSR-14 Event System
Status
Accepted (2024-02)
Context
Consumers need extension points for:
Logging and monitoring.
Request modification.
Response processing.
Cost tracking and rate limiting.
Decision
Use TYPO3's PSR-14 event system with events:
BeforeRequestEvent: Modify requests before sending.
AfterResponseEvent: Process responses after receiving.
Events are dispatched by LlmServiceManager and provide:
Full context (messages, options, provider).
Mutable options (before request).
Response data (after response).
Timing information.
Consequences
Positive:
●● Follows TYPO3 conventions.
●● Decoupled extension mechanism.
● Multiple listeners without modification.
● Testable event handlers.
Negative:
◑ Event overhead on every request.
◑ Listener ordering considerations.
◑ Debugging event flow complexity.
Net Score: +6.5 (Strong positive impact - standard TYPO3 integration with decoupled extensibility)
ADR-005: TYPO3 Caching Framework Integration
Status
Accepted (2024-03)
Context
LLM API calls are:
Expensive (cost per token).
Relatively slow (network latency).
Often deterministic (embeddings, some completions).
Decision
Integrate with TYPO3's caching framework:
Cache identifier: nrllm_responses.
Configurable backend (default: database).
Cache keys based on: provider + model + input hash.
TTL: 3600s default (configurable).
Caching strategy:
Always cache: Embeddings (deterministic).
Optional cache: Completions with temperature=0.
Never cache: Streaming, tool calls, high temperature.
Consequences
Positive:
●● Reduced API costs.
●● Faster responses for cached content.
● Follows TYPO3 patterns.
◐ Configurable per deployment.
Negative:
✕ Cache invalidation complexity.
◑ Storage requirements.
✕ Stale responses if TTL too long.
Net Score: +4.5 (Positive impact - significant cost/performance gains with manageable cache complexity)
This ADR documents the original encryption approach which has been replaced.
API keys are now stored using the
netresearch/nr-vault
extension
which provides enterprise-grade secrets management with envelope encryption,
audit logging, and access control.
Context
The nr_llm extension stores API keys for various LLM providers (OpenAI, Anthropic, etc.)
in the database. These credentials are sensitive and require protection.
Problem statement
TYPO3's TCA type=password field has two modes:
Hashed mode (default): Uses bcrypt/argon2 - irreversible, suitable for user passwords
Unhashed mode (hashed => false): Stores plaintext - required for API keys that must be retrieved
API keys must be retrievable to authenticate with external services, so hashing is not an option.
However, storing them in plaintext exposes them to:
Database dumps/backups
SQL injection attacks
Unauthorized database access
Accidental exposure in logs
Requirements
API keys must be retrievable (not hashed).
Keys must be encrypted at rest in the database.
Encryption must be transparent to the application.
Solution must work without external dependencies (self-contained).
Must support key rotation.
Backwards compatible with existing plaintext values.
Decision
Implement application-level encryption using sodium_crypto_secretbox (XSalsa20-Poly1305)
with key derivation from TYPO3's encryptionKey.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Backend Form │
│ (user enters API key) │
└─────────────────────────────┬───────────────────────────────────┘
│ plaintext
▼
┌─────────────────────────────────────────────────────────────────┐
│ Provider::setApiKey() │
│ ProviderEncryptionService::encrypt() │
│ │
│ 1. Generate random nonce (24 bytes) │
│ 2. Derive key from TYPO3 encryptionKey via SHA-256 │
│ 3. Encrypt with XSalsa20-Poly1305 │
│ 4. Prefix with "enc:" marker │
│ 5. Base64 encode for storage │
└─────────────────────────────┬───────────────────────────────────┘
│ "enc:base64(nonce+ciphertext+tag)"
▼
┌─────────────────────────────────────────────────────────────────┐
│ Database │
│ tx_nrllm_provider.api_key │
└─────────────────────────────────────────────────────────────────┘
The nr_llm extension needs to manage LLM configurations for various use cases (chat, translation, embeddings, etc.). Initially, configurations were stored in a single table mixing connection settings, model parameters, and use-case-specific prompts.
Problem statement
A single-table approach creates several issues:
API Key Duplication: Same API key repeated across multiple configurations.
Model Redundancy: Model capabilities and pricing duplicated.
Inflexible Connections: Cannot have multiple API keys for same provider (prod/dev).
Mixed Concerns: Connection details, model specs, and prompts intermingled.
Maintenance Burden: Changing an API key requires updating multiple records.
Real-world scenarios not supported
Scenario
Single-Table Problem
Separate prod/dev OpenAI accounts
Must duplicate all configurations
Self-hosted Ollama + cloud fallback
Cannot model multiple endpoints
Cost tracking per API key
No clear key-to-usage mapping
Model catalog with shared pricing
Model specs repeated everywhere
Team-specific API keys
No multi-tenancy support
Decision
Implement a three-level hierarchical architecture separating concerns: