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:
@@ -1,11 +1,29 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// unset removes an env key for the duration of the test, restoring it after.
|
||||
// Needed to observe a default for a key read with LookupEnv (where present-empty
|
||||
// means "explicitly disabled", not "use default").
|
||||
func unset(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if old, ok := os.LookupEnv(key); ok {
|
||||
t.Cleanup(func() {
|
||||
if err := os.Setenv(key, old); err != nil {
|
||||
t.Fatalf("restore %s: %v", key, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("unset %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// setEnv sets env vars for the test and clears them afterward, so cases don't
|
||||
// leak into one another. t.Setenv handles restoration.
|
||||
func setEnv(t *testing.T, kv map[string]string) {
|
||||
@@ -53,6 +71,46 @@ func TestLoad_AppliesDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_SummarizerChainDefaults(t *testing.T) {
|
||||
setEnv(t, map[string]string{
|
||||
"TAPIR_SUMMARIZER_MODEL": "",
|
||||
"TAPIR_SUMMARY_MAX_TOKENS": "",
|
||||
"TAPIR_MAX_TRANSCRIPT_CHARS": "",
|
||||
})
|
||||
unset(t, "TAPIR_FALLBACK_MODEL")
|
||||
unset(t, "TAPIR_CLOUD_FALLBACK_MODEL")
|
||||
|
||||
c, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if c.FallbackModel != defaultFallbackModel {
|
||||
t.Errorf("FallbackModel = %q, want %q", c.FallbackModel, defaultFallbackModel)
|
||||
}
|
||||
if c.CloudFallbackModel != defaultCloudFallbackModel {
|
||||
t.Errorf("CloudFallbackModel = %q, want %q", c.CloudFallbackModel, defaultCloudFallbackModel)
|
||||
}
|
||||
if c.SummaryMaxTokens != defaultSummaryMaxTokens {
|
||||
t.Errorf("SummaryMaxTokens = %d, want %d", c.SummaryMaxTokens, defaultSummaryMaxTokens)
|
||||
}
|
||||
if c.MaxTranscriptChars != defaultMaxTranscriptChars {
|
||||
t.Errorf("MaxTranscriptChars = %d, want %d", c.MaxTranscriptChars, defaultMaxTranscriptChars)
|
||||
}
|
||||
}
|
||||
|
||||
// An explicitly empty cloud-fallback env disables external routing — the lever a
|
||||
// client deployment pulls so content never leaves the local stack.
|
||||
func TestLoad_EmptyCloudFallbackDisables(t *testing.T) {
|
||||
t.Setenv("TAPIR_CLOUD_FALLBACK_MODEL", "")
|
||||
c, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if c.CloudFallbackModel != "" {
|
||||
t.Errorf("CloudFallbackModel = %q, want empty (disabled)", c.CloudFallbackModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_OnboardSummarizeCount(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, env string
|
||||
|
||||
Reference in New Issue
Block a user