3.6 KiB
3.6 KiB
Per-task model override via MCP + cheapest-model prompt guidance
Date: 2026-06-09
Goal
Let Claude pick the model for each task it generates (planning subtasks, improvement follow-ups, external task creation) directly at creation time via MCP, and instruct Claude — in the relevant prompts — to choose the cheapest model that can do the job well.
Background
TaskEntity.Model(nullable) already exists and is resolved task → list-config → global default inTaskRunner.ResolveConfigAsync, then passed to the CLI as--modelbyClaudeArgsBuilder.- Today the model can only be set after creation via
set_task_config(ConfigMcpTools.SetTaskConfig). The creation tools (CreateChildTask,SuggestImprovement,AddTask) accept no model, so assigning one is a two-call dance. ModelRegistry.Aliases = ["sonnet","opus","haiku"]; no cost ordering or validation helper exists.
No schema change is required — only plumbing a model argument through the
creation paths plus prompt edits.
Decisions
- Validation: strict alias-only.
modelmust be one of haiku/sonnet/opus (case-insensitive); blank/null means "inherit" (no override); anything else throws an MCP error so Claude self-corrects immediately rather than the task failing later at CLI runtime. AddSubtaskis out of scope: it creates aSubtaskEntity(a checklist step), which is never independently executed — a model there is a no-op.- Improvement-child prompt: the child's model is fixed at filing time and it cannot re-pick, so only a one-line "this is an intentionally small/cheap unit — stay minimal" reminder is added. The real model-choice instruction lives in the main system prompt's SuggestImprovement guidance.
Cost ordering & heuristic (single source: ModelRegistry.ByCostAscending)
haiku < sonnet < opus
- haiku — trivial/mechanical: doc tweaks, simple renames, small localized edits.
- sonnet — normal coding work (default).
- opus — complex architecture, cross-cutting changes, hard debugging.
Changes
-
ClaudeDo.Data/Models/ModelRegistry.csByCostAscending = ["haiku","sonnet","opus"].string? NormalizeAlias(string? model)— trim; null/blank → null; case-insensitive match → canonical lowercase alias; else throwArgumentExceptionwith the allowed list.
-
TaskRepository.CreateChildAsync— add optionalstring? model = null; setchild.Model = ModelRegistry.NormalizeAlias(model). Single choke-point for both child-creation MCP tools. -
MCP creation tools (add
modelparam, document in[Description]):PlanningMcpService.CreateChildTask→ forward toCreateChildAsync.TaskRunMcpService.SuggestImprovement→ forward toCreateChildAsync.ExternalMcpService.AddTask→NormalizeAliasthen setentity.Model.
-
Prompts (
PromptFiles.cs)PlanningSystemDefault— instruct the planner to pass eachCreateChildTaskthe cheapest capable model (with the ordering/heuristic).SystemDefault(Out-of-scope improvements) — when filing viaSuggestImprovement, pass the cheapest capablemodel.ImprovementChildDefault— one-line minimality reminder.
-
Tests (no real CLI):
NormalizeAlias: valid aliases (any case), blank/null → null, unknown → throws.CreateChildTask/SuggestImprovement/AddTaskpersist the model; invalid model is rejected.
Out of scope
- No DB migration. No locale changes (prompts and MCP descriptions are not localized). No UI changes (existing per-task model display already covers it).