RFC-106: Tiered fallback for DGX-backed pipeline stages¶
- Status: Implemented (all migration steps landed under #1198)
- Tracking issue: #1198
- Authors: Marko
- Stakeholders: Core Pipeline, Providers, DGX Infra, Cost/Resilience
- Related RFCs:
docs/rfc/RFC-046-materialization-architecture.md(profiles/registry as source of truth)- Related work:
docs/guides/eval-reports/EVAL_MOSS_BAKEOFF_2026_07.md(MOSS promoted as DGX transcription; #1174)#926(the original lazy DGX-diarization → local fallback)#1177(MOSS service) ·#952(faster-whisper on :8000)
Abstract¶
DGX-backed stages should degrade through an ordered chain of providers, not a single fallback. This RFC defines a provider-agnostic tiered fallback: each stage (transcription, diarization, summary/GI/KG) resolves to a primary provider plus an ordered list of fallbacks, tried in order on failure, applied uniformly in one place instead of hand-wired inside individual providers.
Problem statement¶
Fallback today is per-provider and inconsistent:
tailnet_dgx_whisperself-wraps a single fallback (transcription_fallback_provider, defaultopenai).tailnet_dgxdiarization self-wraps its own lazy fallback (#926).- The
mossprovider has no fallback at all — so promoting MOSS as the DGX transcription default (#1174) silently dropped the fallback;transcription_fallback_provideris dead config for it. - The LLM stages (summary / GI / KG on Ollama :11434) have no DGX-down fallback — they fail if the box is unreachable.
And a single fallback level cannot distinguish the two failure modes a DGX profile actually faces:
| Failure | What's reachable | Correct fallback |
|---|---|---|
| One service crashes / its port is not in the tailnet ACL | rest of DGX is up | the other DGX provider for that stage |
| Whole DGX down / unreachable | nothing on the DGX | cloud (or local-mac) |
A profile that falls straight from MOSS to cloud pays for (and leaks to) the cloud even when faster-whisper on the same box would have served it. A profile that falls only to DGX-whisper fails outright when the box is down.
Proposed design¶
One ordered chain per stage, resolved once¶
Replace the singular <stage>_fallback_provider with an ordered list
<stage>_fallback_providers: list[str] — the chain tried after the primary. A generic
FallbackChainProvider wraps the primary + the chain and is constructed once in each stage's
factory (transcription.factory, diarization.factory, the LLM stage factory), so every provider
gets identical behaviour and the per-provider self-wrapping in tailnet_dgx_whisper /
tailnet_dgx diarization is retired.
transcription (DGX prod): moss → tailnet_dgx_whisper → openai
diarization (DGX prod): tailnet_dgx (pyannote) → deepgram
summary/gi/kg (DGX prod): ollama(dgx) → gemini | openai
Failure classification (do not cascade on bad input)¶
The chain advances only on infrastructure failures — connection refused, timeout, health-check
down, HTTP 5xx. A content failure (malformed audio, guardrail violation, a deterministic 4xx)
must not cascade: retrying the next tier wastes money and hides a real bug. Reuse the existing
is_provider_audio_payload_limit_error / guardrail classifications; add a small
is_infra_failure(exc) predicate the chain switches on.
Health-aware skipping¶
Mirror tailnet_dgx_whisper's existing preflight: if a tier exposes a health endpoint and it
reports down, skip straight to the next tier instead of eating the request timeout. Registry
StageOption.endpoint already carries the health surface.
The registry owns the chain; materialize emits it (source of truth)¶
The failover ladder is registry-governed data, exactly like the primary provider — not a runtime default. This keeps the "profiles are views, the registry is the source of truth" invariant (RFC-046) intact for fallback too.
ProfilePresetgains a per-stage<stage>_fallback: list[str]— an ordered list ofStageOptionids after the primary. Example (a DGX prod preset):
transcription="moss_transcribe_diarize",
transcription_fallback=["tailnet_dgx_speaches_thread_b", "openai_whisper_1"],
diarization="tailnet_dgx_diarization_community1",
diarization_fallback=["deepgram_diarization_nova3"],
-
The resolver maps that list of ids to their
StageOption.providervalues and emits<stage>_fallback_providers: list[str]into the materialized profile. The chain is therefore present in the YAML — no runtime inference, andmake profiles-checkguards it like every other registry-governed field. -
Because each tier is a
StageOption, the chain is eval-backed: every entry carries its ownheadline_metric,research_ref,realtime_multiple,resident_memory_gb, andtier(primary/fallback). The ladder is a list of measured options, not opaque strings. -
Config: add
<stage>_fallback_providers: list[str]. The legacy singular<stage>_fallback_providermaps to a one-element chain (full back-compat; no profile churn required to keep today's behaviour). Cloud presets keep their single cloud fallback (a one-element chain). -
allow_cloud_fallback(per profile): when false, the resolver refuses to emit a cloudStageOptioninto any chain — the ladder ends at the last on-prem tier and fails closed. This is what makes theall-DGX / no-cloudprofiles safe to give a fallback at all.
Migration — landed under #1198¶
- ✅ Registry emits the chains (increment 1):
ProfilePreset.<stage>_fallback, resolver emits<stage>_fallback_providers,REGISTRY_GOVERNED_FIELDS+ config fields,profiles-checkguard. - ✅ Transcription (increment 2a):
FallbackChainTranscriptionProvider+is_infra_failure;tailnet_dgx_whisperself-wrap retired → pure DGX tier that raises. Closes the #1174 MOSS gap. - ✅ Diarization (increment 2b):
FallbackChainDiarizationProvider;tailnet_dgxdiarization self-wrap (local pyannote) retired onto the chain. - ✅ LLM/summary (increment 2c):
FallbackAwareSummarizationProviderextended to an ordered chain, sourced fromsummary_fallback_providers; covers ollama and vLLM primaries. - ✅ Fail-closed (increment 3):
ProfilePreset.allow_cloud_fallback; the resolver strips cloud tiers when False; airgapped presets declare it.
Free-before-paid (operator directive). Both stages prefer on-prem tiers before paid cloud:
transcription: moss → dgx-whisper → local whisper → openai;
diarization: dgx pyannote → local pyannote → deepgram. is_infra_failure cascades on
timeouts/5xx/connection-blips/guardrail-garbage and stops on content-deterministic failures
(payload-limit, non-429 4xx). The LLM chain keeps RFC-089's cascade-on-any contract.
Non-goals¶
- Not a load balancer or a circuit breaker with backoff windows — just ordered try-next-on-infra- failure. (A breaker can layer on later.)
- Not cross-stage orchestration (e.g. "if transcription fell to cloud, also move summary") — each stage's chain is independent.
- Not changing which providers win a bake-off — the chains encode existing eval verdicts.
Alternatives considered¶
- Keep per-provider self-wrapping, just add it to
moss. Fastest, but perpetuates the inconsistency (every new DGX provider re-implements fallback) and stays single-level — cannot express the DGX-whisper-then-cloud tier. Rejected as the durable answer; acceptable only as an interim stopgap if the chain work slips. - Pipeline-level try/except around each stage call. Spreads fallback policy across the workflow instead of the provider layer; harder to test and to keep consistent with the registry.
Open questions — resolved¶
- Diarization cloud tier:
deepgram, but only after a free local in-process pyannote tier (dgx pyannote → local pyannote → deepgram) so a caller that can run pyannote never pays. - LLM-stage cloud tier:
gemini(thecloud_balancedsummary tier) for the DGX prod profiles. all-DGX / no-cloudfail-closed: yes — implemented asProfilePreset.allow_cloud_fallback(default True). When False the resolver drops cloud tiers from every chain; the airgapped presets set it.