feat(orchestrator): decouple summarizer model from design#286
Conversation
Per-iteration gate summaries shared `models.design`, but the two phases take different transports. DESIGN runs through `claude -p`, which can carry the `[1m]` 1M-context model tag. Gate summarization runs through the LLM API (OpenAI-compatible proxy), which may not serve that tag. Add `models.summarizer` (default `claude-opus-4-6`), expose it in the campaign schema, and have `run_iteration` resolve the summarizer model independently. DESIGN is unchanged.
Review — Small fix + docs neededGood fix for a real problem (proxy can't serve The issueThe The fix — do what the reporter doesThe reporter in resolved = _resolve_model(campaign, "report", model)
dispatcher = LLMDispatcher(work_dir=work_dir, campaign=campaign, model=resolved)
dispatcher.dispatch("extractor", "report", ...)It creates its own fresh The summarizer should do the same. Instead of changing the model on the shared # Line 384 — leave this alone (DESIGN/EXECUTE fallback)
llm_dispatcher = LLMDispatcher(work_dir=work_dir, campaign=campaign, model=_model_for("design"))
# New — dedicated instance for gate summaries, same pattern as reporter
summary_dispatcher = LLMDispatcher(work_dir=work_dir, campaign=campaign, model=_model_for("summarizer"))Then pass Both reporter and summarizer use Documentation neededPlease add a section to the campaign configuration docs (or README) that explains the model configuration for users: 1. What models exist and their defaults:
2. How to override in campaign.yaml: models:
design: "claude-opus-4-6[1m]" # CLI — supports [1m] context tags
execute_analyze: "claude-sonnet-4-6"
summarizer: "claude-opus-4-6" # API proxy — must be proxy-served
report: "claude-sonnet-4-6" # API proxy — must be proxy-served3. The key distinction users need to understand:
This helps users understand why models are configured separately and prevents the exact proxy mismatch bug this PR fixes. Summary
With those two additions, good to merge. |
Summary
models.design, but the two phases use different transports: DESIGN goes throughclaude -p(which can carry the[1m]1M-context tag) while gate summaries go through the LLM API via an OpenAI-compatible proxy that may not serve the same tag.models.summarizerknob (defaultclaude-opus-4-6), expose it incampaign.schema.yaml, and haverun_iterationresolve the summarizer model via_model_for("summarizer")instead of"design".summarizerrole plumbing indispatch.py,campaign.py, andllm_dispatch.pyalready expected this — the previousmodels.designlookup was a leftover from when the two phases shared a model.Files changed
orchestrator/defaults.yaml— newmodels.summarizerdefault.orchestrator/schemas/campaign.schema.yaml— schema entry so users can override per-campaign.orchestrator/iteration.py— one-line swap:_model_for("design")→_model_for("summarizer")for the summarizer'sLLMDispatcher.Ripples considered
No
graphify-out/graph.jsonis present in the repo, so no automated ripple detection. Manual sweep acrossorchestrator/anddocs/formodels.summarizer/models.design/summarizershows no other call sites that need updating.Test plan
models.summarizer: confirm gate summaries run withclaude-opus-4-6(the new default).models.summarizer: <some-proxy-served-tag>incampaign.yaml: confirm gate summaries use that override and DESIGN keeps its ownmodels.design.models.designis a[1m]variant the proxy doesn't serve: confirm DESIGN still works and summaries are no longer broken by the proxy mismatch.