Per board directive on INB-20:
- live pipeline: INBOXOS_LLM_MODEL=INBOXOS_LLM_MODEL_EXTRACT=gemma4:31b-cloud (single model, no escalation)
- historical pipeline: INBOXOS_LLM_MODEL_CHAIN='gemma4:e4b,gemma4:12b,gemma4:31b-cloud' (primary, smart retry, 2nd smart retry)
- LLMConfig now has a model_chain field parsed from INBOXOS_LLM_MODEL_CHAIN; ollama_run_command_with_chain + llm_run_command_with_chain implement the chain as the retry policy (no same-model tenacity, so a flaky cheap model does not multiply wall-clock latency)
- precedence flip in ollama_run_command: per-step env-var model wins over prompt frontmatter, so an operator can pin a pipeline-level model without editing prompt files
- .opencode/commands/inboxos:*.md frontmatter updated to gemma4:e4b (matches the historical primary; the live env var overrides to 31b-cloud at runtime)
Offline tests: 14/14 live, 8/8 historical unit (incl. new test_process_escalation_recovers_on_second_model) pass. Live Ollama smoke: gemma4:31b-cloud correctly classifies a test email via the live pipeline path; gemma4:e4b → escalation chain routes through to gemma4:31b-cloud when needed.
Refs: INB-20
Co-Authored-By: Paperclip <noreply@paperclip.ing>
c5b7cf8 wired INBOXOS_LLM_MODEL_EXTRACT (default gemma4:12b) and
INBOXOS_LLM_MAX_TOKENS (default 4096) through the live pipeline,
and the workflow now passes both env vars. But
ollama_run_command prefers the prompt's `model:` frontmatter over
the per-step config (see _load_prompt in common.py), so the
extract prompts were still routing to lfm2.5:8b in live run 684
(the modelID log line confirmed it).
Update all four inboxos:extract-*.md frontmatters to
ollama/gemma4:12b so the prompt contract matches the runtime
configuration. Classify stays on ollama/lfm2.5:8b — the cheap
default is correct for the short label-only classify schema.
This is a prompt-as-code change (rule #7 in the CTO charter), so
it goes through git rather than being embedded in a workflow.
Co-Authored-By: Paperclip <noreply@paperclip.ing>