ADR-055: Embeddings join the configuration path; dimensions metadata
- Status
-
Accepted
- Date
-
2026-07-13
- Authors
-
Netresearch DTT GmbH
Context
The three-tier model (Provider → Model → Configuration,
ADR-001) reaches every chat-shaped capability:
completeWithConfiguration(), chatWithConfiguration(),
streamChatWithConfiguration() and
chatWithToolsForConfiguration() all resolve the adapter from a
DB-backed LlmConfiguration (vault key + model + pricing) and run
through the middleware pipeline, so budgets are enforced and cost is
attributed per configuration.
Embeddings did not. LlmServiceManager::embed() only accepted
EmbeddingOptions with raw provider/model strings, resolved
against ExtensionConfiguration and a model-less transient
configuration. An embedding consumer that persists vectors (a search
index, semantic auto-linking — see the scope boundary in
ADR-050) therefore had to duplicate provider, model
and dimensionality into its own extension configuration, bypassing
per-configuration budgets and cost attribution entirely.
The dimensionality gap made this worse: no record anywhere stated how many dimensions a model's vectors have. A consumer validating a persisted vector index against the configured model had to run a live "calibration probe" — embed a throwaway string and count the floats — which costs a provider call and fails when the provider is unreachable.
Decision
Embeddings join the configuration path.
LlmServiceManager::embedForConfiguration() mirrors
chatWithToolsForConfiguration(): it resolves the adapter via
getAdapterFromConfiguration(), runs through the middleware
pipeline with ProviderOperation::Embedding and the budget
metadata from the options, and guards the embeddings feature the
same way embed() does (UnsupportedFeatureException when the
provider lacks it). Per-call EmbeddingOptions take precedence over
the configuration's stored defaults — an options model overrides
the configuration's model id. Caching mirrors embed(): a positive
cache_ttl places a cache key on the call context (keyed on the
configuration identifier plus the effective model), so two
configurations pointing at different models never share cache entries.
The high-level feature service follows:
EmbeddingService::embedForConfiguration() and
embedBatchForConfiguration() delegate to the manager and populate
beUserUid via the shared auto-populate wiring, exactly like the
existing embed()/embedBatch() paths.
Model records carry dimensions metadata. tx_nrllm_model gains
a dimensions column (integer, 0 = unknown, declared like
context_length), surfaced in the TCA next to the other model
limits and on the Model entity as
getDimensions()/setDimensions(). It is descriptive metadata:
nothing in nr_llm enforces it at call time.
Consequences
- Embedding consumers select a backend-managed configuration instead of duplicating provider + model + dimensions into their own extension configuration; per-configuration budgets and cost attribution apply to embeddings like to every chat-shaped capability.
- A consumer can validate a persisted vector index against the
configured model by comparing its stored dimensionality with
getLlmModel()->getDimensions()— no live calibration probe, no provider round-trip. A value of0means "unknown"; consumers fall back to their previous behaviour then. LlmServiceManagerInterfaceandEmbeddingServiceInterfacegained methods — implementers outside this repo must add them.- nr_llm's embedding capability remains stateless (ADR-050): the configuration path changes how the call is resolved and accounted, not what is persisted. Vector stores stay out of scope.