---
title: "ADR-003: Provider Credentials Delegated to nr-llm"
manual: "nr_repurpose"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-repurpose:adr-003@main"
source: "Adr/Adr003ProviderCredentialsViaNrLlm.rst"
rendered: "2026-09-30T16:39:51+00:00"
---

# ADR-003: Provider Credentials Delegated to nr-llm {#adr-003}

-   *Status:* Accepted
-   *Date:* 2026-06-09
-   *Authors:* Netresearch DTT GmbH

## Context {#adr-003-context}

Every AI capability nr_repurpose uses — chat/vision completion (analysis,
scripts, copy), text-to-speech (the podcast), and image generation (the diagram
and story backgrounds) — ultimately authenticates with the same OpenAI account.
The naïve approach is to put the OpenAI API key in extension configuration and
hand it to each service. That violates the Netresearch rule that *API keys MUST
be referenced by identifier, never stored as plaintext*, and it would scatter
the secret across configuration and process memory on the worker host.

nr_repurpose also should not own provider code at all: nr-llm already abstracts
OpenAI, enforces per-user budgets, and (since nr-llm `0.10.0`, see nr-llm
ADR-030) authenticates both its database-backed providers *and* its specialized
services (TTS, images) from its own credential store.

## Decision {#adr-003-decision}

Own no provider code and no key. Give the credential to nr-llm and refer to it
only by the identifier nr-llm issues.

1.  **One secret, one identifier.** The key is handed to nr-llm, which stores it
    and yields an identifier (`nr_repurpose_openai` in the bundled setup).
    nr-llm's OpenAI provider is configured with
    `providers.openai.apiKeyIdentifier = nr_repurpose_openai` and
    `defaultProvider = openai`.
1.  **All access goes through nr-llm.** Completions use nr-llm's
    `CompletionService`; TTS and images use nr-llm's specialized services,
    wrapped by thin local adapters (`OpenAiSpeechSynthesizer`,
    `DallEImageGenerator`) behind the extension's own
    `SpeechSynthesizerInterface` / `ImageGeneratorInterface`. Since
    nr-llm `0.10.0` every one of these authenticates by the same identifier —
    there is no plaintext `providers.openai.apiKey` path.
1.  **The secret never surfaces here.** nr_repurpose holds no key, logs no key,
    and has no provider HTTP code. *How* nr-llm protects the credential — today
    an encrypted store behind an audited HTTP client, [`netresearch/nr-vault`](https://packagist.org/packages/netresearch/nr-vault) —
    is nr-llm's implementation detail and nr-llm's dependency. This extension
    neither requires that package nor names its version range: doing so would
    pin a constraint no code here is written against.

## Consequences {#adr-003-consequences}

-   No plaintext OpenAI key exists anywhere in nr_repurpose's configuration, code,
    or runtime memory; upstream calls are audited centrally by nr-llm.
-   Installation gains a mandatory step: hand the key to nr-llm and point its
    Provider record and its extension configuration at the resulting identifier
    (see [Hand the provider key to nr-llm](https://docs.typo3.org/permalink/netresearch/nr-repurpose:installation-openai-key@main) and [nr-llm wiring: providers, models, Configurations](https://docs.typo3.org/permalink/netresearch/nr-repurpose:configuration-nr-llm@main)). nr-llm's
    backend setup wizard takes the key and issues the identifier; scripted
    installs that cannot drive a wizard fill nr-llm's store from the command line
    instead, which is what the bundled DDEV setup does.
-   nr_repurpose inherits nr-llm's budget enforcement for free on completion
    calls; the specialized TTS/image calls are gated manually against nr-llm's
    `BudgetService` because nr-llm does not run them through its budget
    middleware (see [Two AI cost paths](https://docs.typo3.org/permalink/netresearch/nr-repurpose:architecture-generation-budget@main)).
-   The extension is bound to nr-llm `^0.25` alone; what backs nr-llm's
    credential store, and in which versions, is nr-llm's contract to state and to
    widen. Adopting a different account or provider is a configuration change in
    nr-llm, not a code change here.

## Note (2026-09-29) {#adr-003-note-2026-09-29}

Point 3 of the decision and the last consequence no longer describe the
extension. Since 2026-08-11 nr_repurpose requires
[`netresearch/nr-vault`](https://packagist.org/packages/netresearch/nr-vault) directly (now `^1.1` in `composer.json`,
`1.1.0-1.99.99` in `ext_emconf.php`):
`GenerationOrchestrator` imports nr-vault's
`TechnicalActorContextInterface` and, with
[technicalBeUserUid](https://docs.typo3.org/permalink/netresearch/nr-repurpose:confval-technicalbeuseruid@main) set to a positive backend
user uid, runs a job inside its `runAs()` scope, so that nr-vault checks the
secret's access grants against that backend user instead of denying the
worker, which runs without one. A value of 0 or below sets no actor, and the
job runs outside that scope as before. The extension still holds no key and
has no provider code; the credential stays in nr-llm's store. nr-llm is
required as `^0.35 || ^0.36 || ^0.37 || ^0.38`, not `^0.25`.
