ADR-059: Decompose LlmServiceManager into focused collaborators
- Status
-
Accepted
- Date
-
2026-07-14
- Authors
-
Netresearch DTT GmbH
Context
Llm had grown to 987 lines. It is the extension's central
entry point — a Singleton implementing
Llm, which ADR-028 classifies as
a Category-1 public API. A responsibility scan found nine distinct groups in one
class:
- Skill-injection glue (delegates to
Skill).Injection Service - Default-configuration resolution.
- Loading the
nr_llmextension configuration. - The keyed, ExtensionConfiguration-backed provider registry (register / look up / list / configure providers by string identifier).
- Generic dispatch (
chat,complete,embed,vision,streamChat,chatWithTools). - Configuration-backed dispatch (the
*WithConfigurationmethods). - The database-backed adapter factory facade.
- Pipeline plumbing (
runThroughPipeline, budget metadata, transient configuration synthesis). - Message shaping (system-prompt injection and message normalisation).
Two of these groups additionally carried duplicated code: the two embedding entry points each held an inline copy of the cache-metadata block, and six generic entry points repeated the same options / provider-key extraction preamble.
Decision
Decompose the manager in stages. This ADR records stage 1, which extracts
the self-contained groups while leaving the dispatch logic in place. The manager
remains final and keeps implementing the unchanged
Llm; every former public method is retained as a
thin delegation, so the Category-1 contract, the class name, registerProvider
and the three non-interface public methods (getAdapterFromModel,
getAdapterFromConfiguration, getAdapterRegistry) are unchanged.
Extracted collaborators (all private, autowired via the Classes/* resource
block in Services.):
Keyed— groups 3 + 4. Holds the mutable provider map and the loaded extension configuration, so it is itself aProvider Registry Singleton.Interface ProviderCompilerPassstill adds itsregisterProvidermethod calls to the manager service, which forward here.Configuration— group 2.Resolver readonly; resolves the backend-managed default configuration through the repository.Message— group 9.Shaper readonly, stateless message normalisation and system-prompt injection.Embed— deduplicates the two inline embed cache-metadata blocks. The blocks are not identical: the ad-hocCache Key Builder embedpath keys by provider identifier with the payload() {input, options}and anrllm_provider_<id>tag, whileembedkeys by configuration identifier plus the effective model (options override or the configuration's model id) with aFor Configuration () nrllm_configuration_<id>tag, so two configurations pointing at different models never share entries. The builder therefore shares only the structure (the positive-ttl guard and the{cacheKey, cacheTtl, cacheTags}shape with the commonnrllm_embeddingstag); each caller supplies its own namespace, key payload and scope tag. This difference is intentional, not a defect.
The repeated options / provider-key preamble becomes a private
splitProviderKey() on the manager (the six callers all remain on the manager
in stage 1).
The manager's constructor changes accordingly — it drops
Extension, Logger,
Cache and the Llm (now
owned by the collaborators) and gains the four collaborators. The constructor is
not part of the interface contract; production wiring is autowired.
Consequences
- Behaviour is unchanged. The interface signatures, the provider-resolution
error messages and their exception codes, and the two embed cache-keying
strategies are all preserved. The existing manager / integration / e2e tests
continue to exercise the behaviour through the facade; they construct the
manager through a test factory (
LlmServiceManagerTestFactory) that accepts the previous leaf-dependency shape and wires the new collaborators, so the call sites did not each have to change shape. The extracted classes additionally get their own unit tests. - New private services keep the documented public-service set stable
(ADR-028); the
PublicServicesPolicyTestcount is unchanged. - The manager drops from 987 to roughly 790 lines.
Stage 2 (not in this change)
Groups 5 and 6 (generic and configuration-backed dispatch) plus the pipeline plumbing (group 8) remain on the manager. Extracting per-operation dispatchers (completion, embedding, streaming, tool-calling) is deferred to a follow-up so this change stays reviewable and behaviour-preserving. It remains worthwhile: after stage 1 the manager is still dominated by the dispatch methods, and those are the natural seam for the privacy-model entry point work (ADR-026 payload constraint). Stage 2 should be taken up when that work needs the seam.