ADR 090 · Accepted
One extension until 1.0, with documented split seams
Context
nr_llm has grown well beyond a provider abstraction. It now bundles several subsystems that are, in principle, independently useful:
the core three-tier abstraction (Provider → Model → Configuration), the middleware pipeline (fallback, budget, usage, cache) and the completion / embedding / vision feature services;
specialized services — DeepL / LLM translation, DALL·E / FAL image generation, Whisper / TTS speech (Classes/Specialized/);
the tool / agent system — the builtin tool set, RAG site-search retrieval, the tool loop, human-in-the-loop approval and agent-run persistence (Classes/Service/Tool/, ADR-042 ff., ADR-084);
the guardrail / redaction safety pipeline (Classes/Service/Guardrail/, ADR-085 ff.);
the backend UI — Tool Playground, analytics dashboard, setup wizard and skills management (Classes/Controller/Backend/).
Different consumers want different subsets: an agency embedding the provider core in its own product does not need the backend dashboards; a site that only translates does not need the agent stack or its attack surface. That makes a split into focused extensions (nr_llm core plus optional feature packages) an attractive long-term shape.
It is, however, the wrong move now. The extension is under rapid pre-1.0 development: the internal contracts between these subsystems still change often. Splitting today would freeze those contracts into public, cross-extension APIs prematurely, and impose coordinated multi-repo releases and an extension-version compatibility matrix — friction that would slow exactly the iteration that is still happening.
Decision
Ship ``nr_llm`` as a single extension until the 1.0 release, and revisit the split with or before 1.0 — once the internal seams have stabilized.
Until then, keep the architecture split-ready rather than split: preserve clean module boundaries so a future extraction is a packaging change, not a re-architecture. The phpat architecture tests (Tests/Architecture/) already enforce the vertical layering — Controller → Service → Provider / Domain (e.g. controllers use the tool registry rather than concrete adapters, services do not depend on controllers or concrete provider adapters, domain models stay free of repositories/HTTP). They do not yet police the horizontal seams between the feature modules (specialized ↔ tools ↔ guardrail ↔ backend); those are currently kept clean by review. Extending phpat to assert the horizontal module boundaries is itself part of the split-readiness work, and new cross-module coupling that would block an extraction is treated as a defect.
Anticipated split seams (candidate extensions):
Package |
Depends on |
Scope |
|---|---|---|
nr_llm (core) |
— |
Provider → Model → Configuration, middleware pipeline, the completion / embedding / vision feature services, LlmServiceManager. The mandatory base of every other package. |
nr_llm_specialized |
core |
Translation (DeepL / LLM), image (DALL·E / FAL), speech (Whisper / TTS), and their per-modality provider adapters. |
nr_llm_tools |
core |
The builtin tool set, RAG site-search retrieval, the tool loop, human-in-the-loop approval and agent-run persistence. |
nr_llm_guardrail |
core |
The guardrail / secret-redaction safety pipeline. May instead remain in core, since "secure by default" is a core promise. |
nr_llm_backend |
core (+ installed feature packages) |
Tool Playground, analytics dashboard, setup wizard, skills management — the backend modules and their widgets. |
A subsystem is a candidate for extraction only once all of the following hold:
its contract with core has been stable across several releases (few or no breaking changes);
a concrete consumer benefits from installing it separately (smaller footprint or reduced attack surface); and
the 1.0 public-API freeze is planned or in progress.
Consequences
Now: one repository, one release pipeline, one version — fast iteration. The cost is a larger install footprint for consumers who want only a subset, and a broader default attack surface (mitigated by the tool availability gating and guardrail defaults).
Split-ready discipline: module boundaries must stay clean. The phpat architecture tests guard the vertical layering automatically; the horizontal seams (core reaching into the backend UI, or one feature module reaching into another) are today a review responsibility. Adding phpat rules for the module seams — so a wrong-way dependency fails CI rather than only review — is a concrete, cheap step toward split-readiness.
At 1.0: re-evaluate against the criteria above. If the seams have held, the split is largely a composer.json / ext_emconf.php repackaging plus moving files along the documented boundaries; if a consumer need is real before 1.0, a single package (most likely nr_llm_specialized or nr_llm_tools) can be extracted early without committing to the full split.
Alternatives considered
Split now. Rejected: premature. It would freeze still-churning internal contracts as public APIs and add coordinated-release friction during the phase where iteration speed matters most.
Never split; stay monolithic. Rejected: the subsystems are genuinely separable and real consumers want subsets. Committing to a permanent monolith would, over time, invite the cross-module coupling this ADR exists to prevent.