feat(summarizer): resilient endpoint chain with local→cloud fallback (ADR-022)
The first friendly-pilot live run produced zero summaries: koala/phi4-mini hit three silent failure modes — 8k context overflow on long transcripts (HTTP 400), intermittent malformed JSON (highlights as a bare string), and no fallback wired at all (summarizer.New(primary, nil)). Keep phi4-mini as the fast primary and add resilience around it: - Ordered endpoint chain (summarizer.NewChain): phi4-mini → koala/phi4-14b (local) → berget/mistral-small (worst-case external). All reached through the one LiteLLM gateway by alias. - A parse failure now advances the chain like a transport error — the old Primary→Fallback shape returned the parse error without trying anyone else. - Tolerant parse: highlights/takeaways coerce string→[]string, absorbing the common small-model quirk without spending a fallback round-trip. - Transcript truncation (TAPIR_MAX_TRANSCRIPT_CHARS=18000) prevents the overflow rather than recovering from it; validated to fit phi4-mini's 8k window. - Bounded completion budget (TAPIR_SUMMARY_MAX_TOKENS=1500) — the old 8192 budget itself contributed to the overflow. Local-first guarantee preserved by ordering: external endpoint is tried only after every local one fails. TAPIR_CLOUD_FALLBACK_MODEL="" disables it entirely for client/NDA deployments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -803,6 +803,58 @@ never exposed in the UI.
|
||||
|
||||
---
|
||||
|
||||
## ADR-022 — Summarizer is a resilient endpoint chain, not a single model
|
||||
|
||||
**Status:** Accepted (2026-06-10). **Extends ADR-004** (the copied `llm` Primary→Fallback
|
||||
routing). Triggered by the first friendly-pilot live run, where a connected user got **zero**
|
||||
summaries after 12h.
|
||||
|
||||
**Context.** Stage-0 ran a single summarizer model (`koala/phi4-mini`) with no fallback wired
|
||||
(`summarizer.New(primary, nil)`). The live run exposed three independent failure modes, each of
|
||||
which silently produced no summary:
|
||||
|
||||
1. **Context overflow.** `phi4-mini` has an 8k context. Real transcripts (one was 11,602 tokens)
|
||||
exceed it and the gateway returns HTTP 400 — and the request also sent `max_tokens=8192`, so
|
||||
even a short transcript plus the completion budget could overflow the window.
|
||||
2. **Malformed model output.** `phi4-mini` intermittently emits `highlights` as a bare string
|
||||
instead of an array, producing `cannot unmarshal string into []string`. The old code returned
|
||||
the parse error **without** trying any other model — a 200-with-bad-JSON short-circuited.
|
||||
3. **No fallback existed at all** — any primary failure was terminal for that video.
|
||||
|
||||
`phi4-mini` is kept as primary deliberately: it is fast and, on transcripts that fit, correct.
|
||||
The fix is resilience around it, not replacing it.
|
||||
|
||||
**Decision.**
|
||||
1. **Ordered endpoint chain (`summarizer.NewChain`).** Endpoints are tried in order; the first to
|
||||
return a *parseable* summary wins. Default chain:
|
||||
`koala/phi4-mini` (primary, local) → `koala/phi4-14b` (fallback, local) →
|
||||
`berget/mistral-small` (worst-case, external). All three are reached through the **one** LiteLLM
|
||||
gateway by alias — the gateway already fronts both llama-swap and berget — so a fallback is a
|
||||
different alias, not a second client config.
|
||||
2. **A parse failure advances the chain, same as a transport error.** "Reliably summarized" means
|
||||
*parseable summary returned*, not *HTTP 200*. This is the behaviour the old Primary→Fallback
|
||||
shape missed.
|
||||
3. **Tolerant parse.** `highlights`/`takeaways` coerce from a bare string (or a mixed scalar
|
||||
array) to `[]string`, so the most common small-model quirk is absorbed **without** spending a
|
||||
fallback round-trip — keeping the fast path fast.
|
||||
4. **Transcript truncation (`TAPIR_MAX_TRANSCRIPT_CHARS`, default 18000).** Input is bounded
|
||||
up-front to fit a small-context primary, so overflow is prevented rather than recovered-from.
|
||||
5. **Bounded completion budget (`TAPIR_SUMMARY_MAX_TOKENS`, default 1500).** A summary needs few
|
||||
hundred tokens; the old 8192 budget itself contributed to 8k-window overflow.
|
||||
|
||||
**Local-first guarantee preserved.** The chain ordering *is* the guarantee: locals are tried
|
||||
first, so content reaches the external endpoint only after every local endpoint has failed.
|
||||
`TAPIR_CLOUD_FALLBACK_MODEL=""` removes the external endpoint entirely — the lever a
|
||||
**client/NDA deployment** pulls so content never leaves the local stack. With no external endpoint
|
||||
configured the `ai_routing.feature` "content only local" scenarios hold unchanged.
|
||||
|
||||
**Reversibility.** Pure wiring + config. Setting `TAPIR_FALLBACK_MODEL` and
|
||||
`TAPIR_CLOUD_FALLBACK_MODEL` empty collapses the chain back to single-primary behaviour; the
|
||||
tolerant parse and truncation are strict supersets of the old behaviour (a previously-parseable
|
||||
reply still parses; a transcript within budget is unchanged).
|
||||
|
||||
---
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
Approaches considered during the 2026-06-02 planning + grill session and **deliberately not
|
||||
|
||||
Reference in New Issue
Block a user