---
title: "ADR-118: Verify the specialized services from the backend"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-118@0.35"
source: "Adr/Adr118SpecializedServiceVerification.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-118: Verify the specialized services from the backend

-   *Status:* Accepted
-   *Date:* 2026-07-28
-   *Authors:* Netresearch DTT GmbH

## Context

Translation, image generation and speech are configured through the Extension
Configuration: one nr-vault identifier per credential
(`translators.deepl.apiKeyIdentifier`, `image.fal.apiKeyIdentifier`,
`providers.openai.apiKeyIdentifier` for the DALL·E / Whisper / TTS family).

Nothing in the backend could reach any of them. Every entry point terminates in
a plain chat completion:

-   the Playground drives `AgentRuntimeInterface` with a single text prompt;
-   the "Test" buttons on provider, model and configuration records call
    `$adapter->complete()`;
-   `TaskExecutionService::execute()` never branches on `TaskCategory`,
    `TaskInputType` or `TaskOutputFormat` — it always calls
    `completeWithConfiguration()`, so a translation or image Task cannot be
    defined.

A repository-wide search confirms it: outside their own directories,
`TranslationService` appears only in doc comments, and the image services
not at all. They are implemented, DI-wired and unit-tested, and reachable only
by a consuming extension that injects them.

The practical consequence is that an operator pastes a DeepL identifier into
the Extension Configuration and has no way to learn whether it works. The
failure surfaces later, in a consumer, as a runtime error.

## Decision

**Add backend endpoints that exercise translation and image generation, as
verification surfaces rather than features.**

Three AJAX routes on `SpecializedTestController`, rendered as two cards on
the existing test page:

| Route | Purpose |
| --- | --- |
| `nrllm_test_translate` | Translate a snippet. With no translator named, the LLM path runs (no specialized credential needed); naming one routes to it, which is the case worth testing after configuring its vault identifier. |
| `nrllm_test_translators` | List the registered translators and whether each is configured, so the picker shows the answer before anything runs. |
| `nrllm_test_image` | Generate one image with either the OpenAI or the FAL service. |

**Nothing is persisted.** A translation is returned as text. An image is
returned as whatever the provider produced — a URL from FAL, a data URI from
the OpenAI family — rendered in the browser and gone on reload. Storing it,
moving it into FAL or attaching it to a record is the consuming extension's
job and deliberately out of scope: nr_llm has no storage wiring for generated
images and this ADR does not add one.

**A missing credential is reported as 503, not 500.** That distinction is the
whole point of the endpoints, so `ServiceUnavailableException` is caught
separately and answered with a message naming the Extension Configuration.
A runtime failure of a configured service stays a 500 with the detail in the
log, per the existing error-sanitising convention.

### `ImageGeneratorInterface`

`DallEImageService::generate()` takes an
`ImageGenerationOptions` object as its second parameter,
`FalImageService::generate()` a model identifier. The divergence is
deliberate — the providers model a request differently — and stays.

The new `ImageGeneratorInterface` declares only the part they share,
`generate(string $prompt): ImageGenerationResult`. Both classes already
default every parameter after the prompt, so neither changes behaviour by
implementing it. It lets a caller that wants no provider-specific control treat
the two interchangeably, and it makes them mockable — both are `final`, which
would otherwise leave the controller untestable.

Because both services satisfy the interface, the controller's two image
arguments are bound explicitly in `Services.yaml`; the type alone cannot
disambiguate them.

## Consequences

-   An operator can answer "does this credential work?" without writing consumer
    code, which is what the Extension Configuration fields have been missing
    since they were introduced.
-   The endpoints spend real provider quota on each run. They are admin-only
    ([ADR-037](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-037@0.35) guards every action), single-shot, and input is
    capped at 5000 characters.
-   Speech (Whisper, text-to-speech) stays unreachable. Transcription needs an
    audio upload and synthesis produces a binary response; both raise the same
    storage question this ADR declines to answer for images, with less to gain.
-   `ImageGeneratorInterface` is public surface. A future image provider is
    expected to implement it.
