---
title: "Build your extension on nr-llm"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:integration-guide@0.35"
source: "Developer/IntegrationGuide.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# Build your extension on nr-llm

This guide walks you through adding AI capabilities to a TYPO3 extension using
nr-llm as a dependency. By the end, your extension will have working AI features
without any provider-specific code.

-   [Why build on nr-llm?](https://docs.typo3.org/permalink/netresearch/nr-llm:why-build-on-nr-llm@0.35)
-   [Step 1: Add the dependency](https://docs.typo3.org/permalink/netresearch/nr-llm:step-1-add-the-dependency@0.35)
-   [Step 2: Inject the service](https://docs.typo3.org/permalink/netresearch/nr-llm:step-2-inject-the-service@0.35)
-   [Step 3: Use feature services for specialized tasks](https://docs.typo3.org/permalink/netresearch/nr-llm:step-3-use-feature-services-for-specialized-tasks@0.35)
-   [Step 4: Handle errors gracefully](https://docs.typo3.org/permalink/netresearch/nr-llm:step-4-handle-errors-gracefully@0.35)
-   [Step 5: Use database configurations (optional)](https://docs.typo3.org/permalink/netresearch/nr-llm:step-5-use-database-configurations-optional@0.35)
-   [Step 6: Declare the configurations you need (optional)](https://docs.typo3.org/permalink/netresearch/nr-llm:step-6-declare-the-configurations-you-need-optional@0.35)
-   [Testing your integration](https://docs.typo3.org/permalink/netresearch/nr-llm:testing-your-integration@0.35)
-   [Integration checklist](https://docs.typo3.org/permalink/netresearch/nr-llm:integration-checklist@0.35)

## Why build on nr-llm?

When your extension calls an LLM API directly, it takes on responsibility for:

-   HTTP client setup, authentication, and error handling per provider
-   Secure API key storage (not in
    `ext_conf_template.txt` or `$GLOBALS`)
-   Response caching to control costs
-   Streaming implementation for real-time UX
-   A configuration UI for administrators

nr-llm handles all of this. Your extension focuses on *what* to ask the AI, not
*how* to reach it.

## Step 1: Add the dependency

**Install nr-llm**

```bash
composer require netresearch/nr-llm
```

Add the dependency to your `ext_emconf.php`:

**ext_emconf.php**

```php
'constraints' => [
    'depends' => [
        'typo3' => '13.4.0-14.99.99',
        'nr_llm' => '0.4.0-0.99.99',
    ],
],
```

## Step 2: Inject the service

All nr-llm services are available via TYPO3's dependency injection. Pick the
service that matches your use case:

**Classes/Service/MyAiService.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Service;

use Netresearch\NrLlm\Service\LlmServiceManagerInterface;

final readonly class MyAiService
{
    public function __construct(
        private LlmServiceManagerInterface $llm,
    ) {}

    public function summarize(string $text): string
    {
        $response = $this->llm->complete(
            "Summarize the following text in 2-3 sentences:\n\n" . $text,
        );

        return $response->content;
    }
}
```

No `Services.yaml` configuration needed — TYPO3's autowiring handles it.

## Step 3: Use feature services for specialized tasks

For common AI tasks, use the specialized feature services instead of raw chat:

**Translation example**

```php
use Netresearch\NrLlm\Service\Feature\TranslationService;

final readonly class ContentTranslator
{
    public function __construct(
        private TranslationService $translator,
    ) {}

    public function translateToGerman(string $text): string
    {
        $result = $this->translator->translate($text, 'de');
        return $result->translation;
    }
}
```

**Image analysis example**

```php
use Netresearch\NrLlm\Service\Feature\VisionService;

final readonly class ImageMetadataGenerator
{
    public function __construct(
        private VisionService $vision,
    ) {}

    public function generateAltText(string $imageUrl): string
    {
        return $this->vision->generateAltText($imageUrl);
    }
}
```

**Embedding / similarity example**

```php
use Netresearch\NrLlm\Service\Feature\EmbeddingService;

final readonly class ContentRecommender
{
    public function __construct(
        private EmbeddingService $embeddings,
    ) {}

    /**
     * @param list<array{id: int, text: string, vector: list<float>}> $candidates
     * @return list<int> Top 5 most similar content IDs
     */
    public function findSimilar(string $query, array $candidates): array
    {
        $queryVector = $this->embeddings->embed($query);
        $results = $this->embeddings->findMostSimilar(
            $queryVector,
            array_column($candidates, 'vector'),
            topK: 5,
        );

        return array_map(
            fn(int $index) => $candidates[$index]['id'],
            array_keys($results),
        );
    }
}
```

## Step 4: Handle errors gracefully

nr-llm throws typed exceptions so you can provide meaningful feedback:

**Error handling with typed exceptions**

```php
use Netresearch\NrLlm\Provider\Exception\ProviderConfigurationException;
use Netresearch\NrLlm\Provider\Exception\ProviderConnectionException;
use Netresearch\NrLlm\Provider\Exception\ProviderResponseException;

try {
    $response = $this->llm->complete($prompt);
} catch (ProviderConfigurationException) {
    // No provider configured — guide the admin
    return 'AI features require LLM configuration. '
         . 'An administrator can set this up in AI > Setup.';
} catch (ProviderConnectionException) {
    // Network issue — suggest retry
    return 'Could not reach the AI provider. Please try again.';
} catch (ProviderResponseException $e) {
    // Provider returned an error (rate limit, invalid input, etc.)
    $this->logger->warning('LLM provider error', ['exception' => $e]);
    return 'The AI service returned an error. Please try again later.';
}
```

## Step 5: Use database configurations (optional)

For advanced use cases, reference named configurations that admins create in the
backend module. Resolve them through
`NetresearchNrLlmServiceConfigurationResolver::getActiveByIdentifier()`
— it works in user-less contexts (CLI, Symfony Messenger consumers,
anonymous frontend requests) and applies the guards a raw repository
lookup skips ([ADR-070](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-070@0.35)):

**Using named database configurations**

```php
use Netresearch\NrLlm\Service\ConfigurationResolver;
use Netresearch\NrLlm\Service\LlmServiceManagerInterface;

final readonly class BlogSummarizer
{
    public function __construct(
        private ConfigurationResolver $configurationResolver,
        private LlmServiceManagerInterface $llm,
    ) {}

    public function summarize(string $article): string
    {
        // Uses the "blog-summarizer" configuration created by the admin
        // (specific model, temperature, system prompt, etc.)
        $config = $this->configurationResolver->getActiveByIdentifier('blog-summarizer');

        $response = $this->llm->chat(
            [['role' => 'user', 'content' => "Summarize:\n\n" . $article]],
            $config->toChatOptions(),
        );

        return $response->content;
    }
}
```

The method throws typed exceptions (all implementing
`NrLlmExceptionInterface`): `ConfigurationNotFoundException`
when no record with the identifier exists,
`ConfigurationInactiveException` when it exists but is deactivated,
and `AccessDeniedException` when it is restricted to backend groups
(user-less callers cannot prove group membership; see
[ADR-070](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-070@0.35)).

> [!WARNING]
> Do not resolve configurations via
> `LlmConfigurationRepository::findOneByIdentifier()` directly: it
> ignores the record's **isActive** flag and its backend-group access
> restrictions, so a deactivated or restricted configuration would keep
> serving your calls.

## Step 6: Declare the configurations you need (optional)

Instead of documenting "please create a configuration named
`myext.chat`" in prose, declare it as a **configuration preset**: implement
`ConfigurationPresetProviderInterface` (auto-tagged
`nr_llm.configuration_preset`) and express your requirements as
`ModelSelectionCriteria` — never a concrete provider, model, or API
key. nr_llm lists your presets as pending and a backend admin imports
them with one confirmation as criteria-mode configuration records.

See [Configuration presets](https://docs.typo3.org/permalink/netresearch/nr-llm:developer-configuration-presets@0.35) for
the declaration example and import flow.

## Testing your integration

Mock the nr-llm interfaces in your unit tests:

**Tests/Unit/Service/MyAiServiceTest.php**

```php
use Netresearch\NrLlm\Domain\Model\CompletionResponse;
use Netresearch\NrLlm\Domain\Model\UsageStatistics;
use Netresearch\NrLlm\Service\LlmServiceManagerInterface;
use PHPUnit\Framework\TestCase;

final class MyAiServiceTest extends TestCase
{
    public function testSummarizeReturnsCompletionContent(): void
    {
        $llm = $this->createStub(LlmServiceManagerInterface::class);
        $llm->method('complete')->willReturn(
            new CompletionResponse(
                content: 'A short summary.',
                model: 'gpt-5.3-instant',
                usage: new UsageStatistics(50, 20, 70),
                finishReason: 'stop',
                provider: 'openai',
            ),
        );

        $service = new MyAiService($llm);
        self::assertSame('A short summary.', $service->summarize('Long text...'));
    }
}
```

## Integration checklist

1.  **composer.json** — Added `netresearch/nr-llm` to `require`
1.  **ext_emconf.php** — Added `nr_llm` to `depends` constraints
1.  **Services** — Inject `LlmServiceManagerInterface`
    or feature services via DI
1.  **Error handling** — Catch typed exceptions and show user-friendly messages
1.  **Testing** — Mock `LlmServiceManagerInterface` in unit tests
1.  **Documentation** — Tell your users they need to
    configure a provider in AI > Setup
