ADR-099: Fail-closed HTTP egress for the specialized services
- Status
-
Accepted
- Date
-
2026-07-21
- Authors
-
Netresearch DTT GmbH
Context
ADR-097 routed every specialized dispatch through the pipeline
via run, so an image / speech / translation call now gets the same
telemetry row, correlation id and circuit breaker as a chat call. But the
wrapping is a convention: a new service method — or a refactor — that builds a
request and sends it without calling run would compile, pass, and
silently spend against the provider with no telemetry, no circuit breaker and no
usage. Nothing enforced the convention.
Decision
The HTTP egress fails closed. Abstract tracks a private
$within flag, set only while a run dispatch is
executing (saved/restored around the terminal, so nesting is safe).
assert throws a \Logic when the flag is false,
and it is called at every HTTP-egress point — execute and the two
binary/multipart senders in Text and
Whisper — before their try block, so the guard is
not swallowed by the Throwable -> Service mapping.
A \Logic (not a service exception) is deliberate: dispatching
outside a lifecycle is a programmer error, not a runtime fault, and must not be
retried or mapped to a transient failure.
Enabling the guard required routing the last unwrapped provider calls through
run. DeepL's detect is a billable translate call and
now runs as Provider — closing a real telemetry gap.
Its get and get are free metadata lookups; they run as
the new Provider so they are observable and circuit-breaker
guarded like any provider HTTP call, but labelled honestly rather than as a
translation.
Consequences
- A specialized service method that reaches the provider without
runnow throws immediately instead of spending unobserved. The guard is the single invariant behind ADR-097's promise.Lifecycle () Providergained aOperation Metadatacase for provider status / metadata calls that are not themselves an AI generation.- DeepL
detect/Language get/Usage getnow emit a telemetry row and are subject to the provider circuit breaker. They record no usage and enforce no budget (they are not billable generations, apart from the translate callGlossaries detectalready made), so cost accounting is unchanged.Language - Test doubles that exercise the low-level HTTP helpers must dispatch inside a
lifecycle — the
Testabledelegate wraps its call inSpecialized Service run, mirroring production. A deliberately unwrapped delegate asserts the guard throws.Lifecycle () - Still deferred: folding the per-service usage recording into a tagged pipeline extractor read by the usage middleware.