ADR-003: Provider Credentials Delegated to nr-llm
- Status
-
Accepted
- Date
-
2026-06-09
- Authors
-
Netresearch DTT GmbH
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., see nr-llm
ADR-030) authenticates both its database-backed providers and its specialized
services (TTS, images) from its own credential store.
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.
- One secret, one identifier. The key is handed to nr-llm, which stores it
and yields an identifier (
nr_in the bundled setup). nr-llm's OpenAI provider is configured withrepurpose_ openai providers.andopenai. api Key Identifier = nr_ repurpose_ openai default.Provider = openai - All access goes through nr-llm. Completions use nr-llm's
Completion; TTS and images use nr-llm's specialized services, wrapped by thin local adapters (Service Open,Ai Speech Synthesizer Dall) behind the extension's ownEImage Generator Speech/Synthesizer Interface Image. Since nr-llmGenerator Interface 0.every one of these authenticates by the same identifier — there is no plaintext10. 0 providers.path.openai. api Key - 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 — 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
- 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 and nr-llm wiring: providers, models, Configurations). 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
Budgetbecause nr-llm does not run them through its budget middleware (see Two AI cost paths).Service - The extension is bound to nr-llm
^0.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.25
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
directly (now ^1. in composer.,
1. in ext_):
Generation imports nr-vault's
Technical and, with
technicalBeUserUid set to a positive backend
user uid, runs a job inside its run 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., not ^0..