ADR-077: Plain completion joins the named-configuration path
- Status
-
Accepted
- Date
-
2026-07-17
- Authors
-
Netresearch DTT GmbH
Context
The three-tier model (Provider → Model → Configuration,
ADR-001) reaches every chat-shaped and embedding
capability through a *ForConfiguration entry point:
chatWithConfiguration(), streamChatWithConfiguration(),
chatWithToolsForConfiguration() and — since
ADR-055 — embedForConfiguration() all resolve the
adapter from a DB-backed LlmConfiguration and run through the
middleware pipeline, so budgets are enforced and cost is attributed per
configuration.
The high-level CompletionService did not. Its complete() family
resolves only the instance-default configuration (chat() picks the
active default, ADR-034). A consumer that needs several
distinct named text configurations — a summariser, a classifier and a
chatbot in one extension — could not target them by identifier; every
plain completion went to the single default. The low-level
LlmServiceManager::completeWithConfiguration() existed but routes
through the provider's raw complete operation (no system-prompt
shaping, no response-format normalisation, no per-user budget metadata),
so it is not a drop-in for the message-based complete() path.
Decision
Plain completion joins the configuration path.
`Llm` mirrors
chat()'s default-configuration branch — it builds the system/user
ChatMessage value objects, injects the configuration's skills, and
threads the per-user budget and idempotency metadata from the options —
but against the caller's chosen configuration instead of the resolved
default. A pinned provider on the options is irrelevant on the
configuration path and is dropped, exactly as chat() does. The method
takes a typed ChatOptions rather than the low-level
completeWithConfiguration() metadata/override arrays, so budget and
idempotency parity is handled once inside the manager.
The high-level feature service follows.
CompletionServiceInterface gains the named-configuration counterparts
of its whole family: completeForConfiguration(),
completeJsonForConfiguration(), completeMarkdownForConfiguration(),
completeFactualForConfiguration() and
completeCreativeForConfiguration(). Each applies the same option
transforms as its instance-default twin (response-format normalisation,
Markdown system-prompt augmentation, factual/creative presets) before
delegating to the manager — the shared transforms are extracted into
private helpers so the two paths cannot drift.
Method-based, not a ChatOptions field. Targeting a configuration is
expressed as an explicit LlmConfiguration parameter, consistent with
the entire *ForConfiguration family, keeping ChatOptions a pure
scalar readonly DTO.
Consequences
- Completion consumers select a backend-managed configuration by identifier instead of relying on the single instance default; per-configuration budgets and cost attribution apply to plain completion like to every other capability.
- Behaviour parity holds: the JSON/Markdown/factual/creative variants apply identical transforms on the configuration path as on the instance-default path, guaranteed by shared private helpers rather than duplicated logic.
CompletionServiceInterfaceandLlmServiceManagerInterfacegained methods — implementers outside this repo must add them. In-repo the concrete services and the hand-writtenStaticCompletionServicetest double are updated.- The low-level
completeWithConfiguration()(rawcompleteoperation) is unchanged and remains available for callers that deliberately want the non-chat completion endpoint.