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.

← All ADRs