feat(adapters): add Summarizer backed by local-first llm routing

Implement ports.Summarizer in internal/adapters/summarizer. It routes through a
local Primary endpoint first and an optional BYO Fallback, owning the routing
itself (not delegating to llm.Router) so it can record AIProvider, AIModel, and
FallbackUsed on domain.Summary. Prompt asks for JSON {summary, highlights,
takeaways}; the parser tolerates thinking-model fences/reasoning and rejects an
empty summary.

The summarizer is the single egress point for content toward an AI model, so it
enforces the local-first guarantee from ai_routing.feature: with no BYO
configured (nil fallback) there is no external endpoint, so content reaches the
local stack and nowhere else. Tests assert all four scenarios via a fake client.

Model alias is config (TAPIR_SUMMARIZER_MODEL, host/name) — not hardcoded;
docs/homelab-integration.md notes it stays `confirm` and that thinking models
need an explicit max_tokens or they return empty content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 17:04:29 +02:00
co-authored by Claude Opus 4.8
parent 0aeb3aa99e
commit 6da9d61e63
3 changed files with 322 additions and 4 deletions
+10 -4
View File
@@ -18,10 +18,16 @@ it** — endpoints and aliases drift, and this file is a snapshot (2026-06-02),
Resolve the live key from the vault when wiring; `sk-local-123` is no longer valid. `confirm` partially resolved.
- **Model alias format:** `host/name`, e.g. `koala/qwen3-coder-30b`, `koala/phi4-mini`,
`iguana/devstral`, `iguana/deepseek-r1-14b`. **Not** the `ollama/` prefix form.
- **Which alias for summarization:** NOT yet decided. Tapir summarizes transcript text, so a
capable general/instruct model on koala or iguana is the candidate — pick during the build and
record the choice (an ADR if it's load-bearing). Do not assume a coder alias is right for prose
summarization.
- **Which alias for summarization:** NOT yet decided. `confirm`. Tapir summarizes transcript
text, so a capable general/instruct model on koala or iguana is the candidate — pick during the
build and record the choice (an ADR if it's load-bearing). Do not assume a coder alias is right
for prose summarization. The summarizer adapter does **not** hardcode an alias: it is config,
env `TAPIR_SUMMARIZER_MODEL` (format `host/name`, e.g. `iguana/deepseek-r1-14b`).
- **Thinking models need an explicit `max_tokens`.** qwen3 / deepseek-r1 spend the budget on
reasoning and return **empty content** if `max_tokens` is too low (or unset). The summarizer's
parser treats an empty summary as an error for exactly this reason. When the alias resolves to a
thinking model, add a generous `max_tokens` to the copied `llm.Client` request (it currently
sends none — change Tapir's copy per ADR-004), or pick a non-thinking instruct model.
This maps directly onto the copied `llm` package: `Client` is the OpenAI-compatible caller,
`Router.Primary` points at this gateway with a chosen alias, `Router.Fallback` is the user's BYO.