From 3246810876489a19b9c2b99cc0d11280c0d14247 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 26 Apr 2026 13:33:54 -0400 Subject: [PATCH] feat: extend RUNTIME_PROFILE_MAP to gemini/qwen/opencode/copilot + settings-advanced UI (#2754) Closes #2612 - Add gemini, qwen, opencode, and copilot entries to RUNTIME_PROFILE_MAP in core.cjs - Group B runtimes (kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity) intentionally have no built-in map and fall through to the existing unknown-runtime fallback - Add 40 new tests to tests/issue-2517-runtime-aware-profiles.test.cjs covering each new runtime's three tiers, Group B fall-through, and partial override merge semantics - Add Section 7 "Runtime Model Tiers" to settings-advanced.md with interactive UI to view and override built-in tier defaults per runtime - Update docs/CONFIGURATION.md built-in tier table to include all four new runtimes Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/bin/lib/core.cjs | 20 ++ get-shit-done/workflows/settings-advanced.md | 194 +++++++++++++-- ...issue-2517-runtime-aware-profiles.test.cjs | 224 ++++++++++++++++++ 3 files changed, 412 insertions(+), 26 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8d4fd9738..fa637294e 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -1572,6 +1572,26 @@ const RUNTIME_PROFILE_MAP = { sonnet: { model: 'gpt-5.3-codex', reasoning_effort: 'medium' }, haiku: { model: 'gpt-5.4-mini', reasoning_effort: 'medium' }, }, + gemini: { + opus: { model: 'gemini-3-pro' }, + sonnet: { model: 'gemini-3-flash' }, + haiku: { model: 'gemini-2.5-flash-lite' }, + }, + qwen: { + opus: { model: 'qwen3-max-2026-01-23' }, + sonnet: { model: 'qwen3-coder-plus' }, + haiku: { model: 'qwen3-coder-next' }, + }, + opencode: { + opus: { model: 'anthropic/claude-opus-4-7' }, + sonnet: { model: 'anthropic/claude-sonnet-4-6' }, + haiku: { model: 'anthropic/claude-haiku-4-5' }, + }, + copilot: { + opus: { model: 'claude-opus-4-7' }, + sonnet: { model: 'claude-sonnet-4-6' }, + haiku: { model: 'claude-haiku-4-5' }, + }, }; const RUNTIMES_WITH_REASONING_EFFORT = new Set(['codex']); diff --git a/get-shit-done/workflows/settings-advanced.md b/get-shit-done/workflows/settings-advanced.md index 61c242ed2..c29450abc 100644 --- a/get-shit-done/workflows/settings-advanced.md +++ b/get-shit-done/workflows/settings-advanced.md @@ -1,12 +1,12 @@ Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts, inline plan threshold, cross-AI execution, base branch, branch templates, response language, -context window, gitignored search, and graphify build timeout. +context window, gitignored search, graphify build timeout, and runtime model tier overrides. This is a companion to `/gsd-settings` — the common-case prompt there covers model profile, research/plan_check/verifier toggles, branching strategy, UI/AI phase gates, and worktree isolation. This advanced command covers everything else that is user-settable, grouped into -six sections so each prompt batch stays cognitively scoped. Every answer pre-selects the +seven sections so each prompt batch stays cognitively scoped. Every answer pre-selects the current value; numeric-input answers that are non-numeric are rejected and re-prompted. @@ -74,6 +74,12 @@ Runtime / Output: - `search_gitignored` (default: `false`) - `graphify.build_timeout` (default: `300`) +Runtime Model Tiers: +- `runtime` (default: `null` — reads as `"claude"`) +- `model_profile_overrides..opus` (default: built-in for the runtime, or absent) +- `model_profile_overrides..sonnet` (default: built-in for the runtime, or absent) +- `model_profile_overrides..haiku` (default: built-in for the runtime, or absent) + Each field's **current value is pre-selected** in the prompt rendering below. When the current value is absent from the config, render the documented default as the pre-selected option so the user sees what the effective value is. @@ -322,6 +328,121 @@ AskUserQuestion([ ]) ``` +### Section 7 — Runtime Model Tiers + +This section lets the user inspect and override the built-in model IDs GSD resolves for each +profile tier (`opus` / `sonnet` / `haiku`) on their configured runtime. + +**Step A — Show current runtime and built-in defaults:** + +Read `runtime` from the config (or treat as `"claude"` if absent). Look up the built-in +tier map from the table below. For each tier, also read the current override from +`model_profile_overrides..` if present. + +Built-in tier defaults by runtime: + +| Runtime | `opus` | `sonnet` | `haiku` | +|------------|-------------------------------|---------------------------------|-------------------------------| +| `claude` | `claude-opus-4-7` | `claude-sonnet-4-6` | `claude-haiku-4-5` | +| `codex` | `gpt-5.4` | `gpt-5.3-codex` | `gpt-5.4-mini` | +| `gemini` | `gemini-3-pro` | `gemini-3-flash` | `gemini-2.5-flash-lite` | +| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` | +| `opencode` | `anthropic/claude-opus-4-7` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | +| `copilot` | `claude-opus-4-7` | `claude-sonnet-4-6` | `claude-haiku-4-5` | +| Group B (`kilo`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | | + +Display a table to the user showing the effective configuration: + +```text +Runtime model tiers — runtime: + +| Tier | Built-in default | Current override (if any) | +|--------|-----------------------------------|-----------------------------------| +| opus | | | +| sonnet | | | +| haiku | | | +``` + +For Group B runtimes (those without a built-in default), show `(no built-in default — your runtime handles model selection)` in the built-in column. + +**Step B — Let the user choose a runtime (optional):** + +```text +AskUserQuestion([ + { + question: "Which runtime do you want to configure tier overrides for? (current: )", + header: "Runtime Selection", + multiSelect: false, + options: [ + { label: "Keep current ()", description: "Configure overrides for the current runtime." }, + { label: "claude", description: "Claude Code / Anthropic CLI." }, + { label: "codex", description: "OpenAI Codex CLI." }, + { label: "gemini", description: "Gemini CLI." }, + { label: "qwen", description: "Qwen CLI." }, + { label: "opencode", description: "OpenCode (uses anthropic/ prefix)." }, + { label: "copilot", description: "GitHub Copilot." }, + { label: "Other (Group B or custom)", description: "kilo, cline, cursor, windsurf, augment, trae, codebuddy, antigravity, or a custom runtime string. Overrides are honored even though no built-in map exists." } + ] + } +]) +``` + +If "Other" is selected, prompt the user to enter the runtime name as a free-text string. +If the selected runtime differs from the stored `runtime` key, update `runtime` via +`gsd-sdk query config-set runtime ` before proceeding to Step C. + +**Step C — Configure tier overrides for the selected runtime:** + +```text +AskUserQuestion([ + { + question: "Override for opus tier? Built-in: Current: ", + header: "Opus Override", + multiSelect: false, + options: [ + { label: "Keep current", description: "Leave unchanged (uses built-in default if no override)." }, + { label: "Clear override", description: "Remove any existing override; fall back to built-in." }, + { label: "Enter model ID", description: "Type the exact model ID string to use for opus-tier agents on this runtime." } + ] + }, + { + question: "Override for sonnet tier? Built-in: Current: ", + header: "Sonnet Override", + multiSelect: false, + options: [ + { label: "Keep current", description: "Leave unchanged." }, + { label: "Clear override", description: "Remove any existing override; fall back to built-in." }, + { label: "Enter model ID", description: "Type the exact model ID string to use for sonnet-tier agents on this runtime." } + ] + }, + { + question: "Override for haiku tier? Built-in: Current: ", + header: "Haiku Override", + multiSelect: false, + options: [ + { label: "Keep current", description: "Leave unchanged." }, + { label: "Clear override", description: "Remove any existing override; fall back to built-in." }, + { label: "Enter model ID", description: "Type the exact model ID string to use for haiku-tier agents on this runtime." } + ] + } +]) +``` + +**Step D — Apply the changes:** + +For each tier where the user chose "Enter model ID": +```bash +gsd-sdk query config-set model_profile_overrides.. "" +``` + +For each tier where the user chose "Clear override", remove the key by setting it to null: +```bash +gsd-sdk query config-set model_profile_overrides.. null +``` + +"Keep current" selections are skipped entirely. Never write a key the user did not explicitly +change. + @@ -338,11 +459,15 @@ gsd-sdk query config-set workflow.plan_bounce_passes 5 gsd-sdk query config-set workflow.subagent_timeout 900 gsd-sdk query config-set git.base_branch main gsd-sdk query config-set context_window 1000000 +# Runtime model tier examples: +gsd-sdk query config-set runtime gemini +gsd-sdk query config-set model_profile_overrides.gemini.opus gemini-3-ultra +gsd-sdk query config-set model_profile_overrides.gemini.haiku null ``` Conceptual shape after merge (unchanged top-level keys like `model_profile`, `granularity`, `mode`, `brave_search`, `agent_skills.*`, `hooks.context_warnings`, and -anything not listed in Sections 1–6 MUST survive the update): +anything not listed in Sections 1–7 MUST survive the update): ```json { @@ -374,6 +499,16 @@ anything not listed in Sections 1–6 MUST survive the update): "graphify": { ...existing_graphify, "build_timeout": + }, + "runtime": , + "model_profile_overrides": { + ...existing_model_profile_overrides, + "": { + ...existing_runtime_overrides, + "opus": , + "sonnet": , + "haiku": + } } } ``` @@ -391,27 +526,31 @@ Display: GSD ► ADVANCED SETTINGS UPDATED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -| Setting | Value | -|--------------------------------|-------| -| workflow.plan_bounce | {on/off} | -| workflow.plan_bounce_passes | {n} | -| workflow.plan_bounce_script | {path/null} | -| workflow.subagent_timeout | {seconds} | -| workflow.inline_plan_threshold | {n} | -| workflow.node_repair | {on/off} | -| workflow.node_repair_budget | {n} | -| workflow.auto_prune_state | {on/off} | -| workflow.max_discuss_passes | {n} | -| workflow.cross_ai_execution | {on/off} | -| workflow.cross_ai_command | {cmd/null} | -| workflow.cross_ai_timeout | {seconds} | -| git.base_branch | {branch} | -| git.phase_branch_template | {template} | -| git.milestone_branch_template | {template} | -| response_language | {lang/null} | -| context_window | {tokens} | -| search_gitignored | {on/off} | -| graphify.build_timeout | {seconds} | +| Setting | Value | +|--------------------------------------------|-------| +| workflow.plan_bounce | {on/off} | +| workflow.plan_bounce_passes | {n} | +| workflow.plan_bounce_script | {path/null} | +| workflow.subagent_timeout | {seconds} | +| workflow.inline_plan_threshold | {n} | +| workflow.node_repair | {on/off} | +| workflow.node_repair_budget | {n} | +| workflow.auto_prune_state | {on/off} | +| workflow.max_discuss_passes | {n} | +| workflow.cross_ai_execution | {on/off} | +| workflow.cross_ai_command | {cmd/null} | +| workflow.cross_ai_timeout | {seconds} | +| git.base_branch | {branch} | +| git.phase_branch_template | {template} | +| git.milestone_branch_template | {template} | +| response_language | {lang/null} | +| context_window | {tokens} | +| search_gitignored | {on/off} | +| graphify.build_timeout | {seconds} | +| runtime | {runtime/null} | +| model_profile_overrides..opus | {model/built-in/null} | +| model_profile_overrides..sonnet | {model/built-in/null} | +| model_profile_overrides..haiku | {model/built-in/null} | These settings apply to future /gsd-plan-phase, /gsd-execute-phase, /gsd-discuss-phase, and /gsd-ship runs. @@ -425,11 +564,14 @@ UI/AI phase gates), use /gsd-settings. - [ ] Current config read from resolved `$GSD_CONFIG_PATH` -- [ ] Six sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime) +- [ ] Seven sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime/Output, Runtime Model Tiers) - [ ] Every field pre-selected to its current value (or documented default if absent) - [ ] Numeric inputs validated — non-numeric rejected and re-prompted - [ ] Branch-template inputs validated — non-default must contain a placeholder - [ ] Null-allowed fields accept an empty input as a clear - [ ] Writes routed through `gsd-sdk query config-set` so unrelated keys are preserved -- [ ] Confirmation table rendered listing all 19 fields +- [ ] Section 7 shows current runtime and built-in tier table +- [ ] Group B runtimes display "(no built-in default — your runtime handles model selection)" +- [ ] Override set/clear/keep paths all work correctly for each tier +- [ ] Confirmation table rendered listing all 23 fields (19 + runtime + 3 tier overrides) diff --git a/tests/issue-2517-runtime-aware-profiles.test.cjs b/tests/issue-2517-runtime-aware-profiles.test.cjs index e5a4e7691..073305c18 100644 --- a/tests/issue-2517-runtime-aware-profiles.test.cjs +++ b/tests/issue-2517-runtime-aware-profiles.test.cjs @@ -590,3 +590,227 @@ describe('issue #2517: RUNTIME_PROFILE_MAP single source of truth (finding #16)' assert.deepStrictEqual(claudeOpus, { model: 'claude-opus-4-7' }); }); }); + +// ─── Issue #2612: gemini runtime tier resolution ───────────────────────────── +describe('issue #2612: runtime "gemini" — Gemini tier resolution', () => { + let tmpDir; + beforeEach(() => { isolateHome(); tmpDir = createTempProject(); _resetRuntimeWarningCacheForTests(); }); + afterEach(() => { cleanup(tmpDir); restoreHome(); }); + + test('opus tier -> gemini-3-pro', () => { + writeConfig(tmpDir, { runtime: 'gemini', model_profile: 'quality' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'gemini-3-pro'); + }); + + test('sonnet tier -> gemini-3-flash', () => { + writeConfig(tmpDir, { runtime: 'gemini', model_profile: 'balanced' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'gemini-3-flash'); + }); + + test('haiku tier -> gemini-2.5-flash-lite', () => { + writeConfig(tmpDir, { runtime: 'gemini', model_profile: 'budget' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'gemini-2.5-flash-lite'); + }); + + test('reasoning_effort is null for gemini (no reasoning_effort in spec)', () => { + writeConfig(tmpDir, { runtime: 'gemini', model_profile: 'quality' }); + assert.strictEqual(resolveReasoningEffortInternal(tmpDir, 'gsd-planner'), null); + }); +}); + +// ─── Issue #2612: qwen runtime tier resolution ─────────────────────────────── +describe('issue #2612: runtime "qwen" — Qwen tier resolution', () => { + let tmpDir; + beforeEach(() => { isolateHome(); tmpDir = createTempProject(); _resetRuntimeWarningCacheForTests(); }); + afterEach(() => { cleanup(tmpDir); restoreHome(); }); + + test('opus tier -> qwen3-max-2026-01-23', () => { + writeConfig(tmpDir, { runtime: 'qwen', model_profile: 'quality' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'qwen3-max-2026-01-23'); + }); + + test('sonnet tier -> qwen3-coder-plus', () => { + writeConfig(tmpDir, { runtime: 'qwen', model_profile: 'balanced' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'qwen3-coder-plus'); + }); + + test('haiku tier -> qwen3-coder-next', () => { + writeConfig(tmpDir, { runtime: 'qwen', model_profile: 'budget' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'qwen3-coder-next'); + }); + + test('reasoning_effort is null for qwen (no reasoning_effort in spec)', () => { + writeConfig(tmpDir, { runtime: 'qwen', model_profile: 'quality' }); + assert.strictEqual(resolveReasoningEffortInternal(tmpDir, 'gsd-planner'), null); + }); +}); + +// ─── Issue #2612: opencode runtime tier resolution ─────────────────────────── +describe('issue #2612: runtime "opencode" — OpenCode tier resolution', () => { + let tmpDir; + beforeEach(() => { isolateHome(); tmpDir = createTempProject(); _resetRuntimeWarningCacheForTests(); }); + afterEach(() => { cleanup(tmpDir); restoreHome(); }); + + test('opus tier -> anthropic/claude-opus-4-7', () => { + writeConfig(tmpDir, { runtime: 'opencode', model_profile: 'quality' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'anthropic/claude-opus-4-7'); + }); + + test('sonnet tier -> anthropic/claude-sonnet-4-6', () => { + writeConfig(tmpDir, { runtime: 'opencode', model_profile: 'balanced' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-4-6'); + }); + + test('haiku tier -> anthropic/claude-haiku-4-5', () => { + writeConfig(tmpDir, { runtime: 'opencode', model_profile: 'budget' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'anthropic/claude-haiku-4-5'); + }); + + test('reasoning_effort is null for opencode (no reasoning_effort in spec)', () => { + writeConfig(tmpDir, { runtime: 'opencode', model_profile: 'quality' }); + assert.strictEqual(resolveReasoningEffortInternal(tmpDir, 'gsd-planner'), null); + }); +}); + +// ─── Issue #2612: copilot runtime tier resolution ──────────────────────────── +describe('issue #2612: runtime "copilot" — Copilot tier resolution', () => { + let tmpDir; + beforeEach(() => { isolateHome(); tmpDir = createTempProject(); _resetRuntimeWarningCacheForTests(); }); + afterEach(() => { cleanup(tmpDir); restoreHome(); }); + + test('opus tier -> claude-opus-4-7', () => { + writeConfig(tmpDir, { runtime: 'copilot', model_profile: 'quality' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-opus-4-7'); + }); + + test('sonnet tier -> claude-sonnet-4-6', () => { + writeConfig(tmpDir, { runtime: 'copilot', model_profile: 'balanced' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'claude-sonnet-4-6'); + }); + + test('haiku tier -> claude-haiku-4-5', () => { + writeConfig(tmpDir, { runtime: 'copilot', model_profile: 'budget' }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'claude-haiku-4-5'); + }); + + test('reasoning_effort is null for copilot (no reasoning_effort in spec)', () => { + writeConfig(tmpDir, { runtime: 'copilot', model_profile: 'quality' }); + assert.strictEqual(resolveReasoningEffortInternal(tmpDir, 'gsd-planner'), null); + }); +}); + +// ─── Issue #2612: Group B runtimes fall through (no built-in map) ──────────── +describe('issue #2612: Group B runtimes — no built-in map, use unknown-runtime fallback', () => { + test('cursor is not in RUNTIME_PROFILE_MAP (uses unknown-runtime fallback)', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.cursor, undefined); + }); + + test('kilo is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.kilo, undefined); + }); + + test('windsurf is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.windsurf, undefined); + }); + + test('cline is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.cline, undefined); + }); + + test('augment is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.augment, undefined); + }); + + test('trae is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.trae, undefined); + }); + + test('codebuddy is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.codebuddy, undefined); + }); + + test('antigravity is not in RUNTIME_PROFILE_MAP', () => { + assert.strictEqual(RUNTIME_PROFILE_MAP.antigravity, undefined); + }); + + test('cursor runtime falls back to Claude alias (not a Gemini/Qwen/etc ID)', () => { + const { createTempProject, cleanup } = require('./helpers.cjs'); + isolateHome(); + const tmpDir = createTempProject(); + _resetRuntimeWarningCacheForTests(); + try { + writeConfig(tmpDir, { runtime: 'cursor', model_profile: 'quality' }); + // Should fall back to Claude alias, not emit a provider-specific ID + const resolved = resolveModelInternal(tmpDir, 'gsd-planner'); + assert.strictEqual(resolved, 'opus'); + } finally { + cleanup(tmpDir); + restoreHome(); + } + }); +}); + +// ─── Issue #2612: Partial override merge for new runtimes ──────────────────── +describe('issue #2612: partial override merge for new Group A runtimes', () => { + let tmpDir; + beforeEach(() => { isolateHome(); tmpDir = createTempProject(); _resetRuntimeWarningCacheForTests(); }); + afterEach(() => { cleanup(tmpDir); restoreHome(); }); + + test('gemini.opus override wins; sonnet and haiku use built-in defaults', () => { + writeConfig(tmpDir, { + runtime: 'gemini', + model_profile: 'quality', + model_profile_overrides: { + gemini: { opus: 'gemini-3-ultra' }, + }, + }); + // opus is overridden + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'gemini-3-ultra'); + // sonnet not overridden — built-in default (quality -> sonnet for gsd-codebase-mapper) + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'gemini-3-flash'); + }); + + test('qwen.opus override wins; sonnet and haiku use built-in defaults', () => { + writeConfig(tmpDir, { + runtime: 'qwen', + model_profile: 'quality', + model_profile_overrides: { + qwen: { opus: 'qwen3-max-custom' }, + }, + }); + // opus is overridden + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'qwen3-max-custom'); + // sonnet not overridden — quality -> sonnet for gsd-codebase-mapper + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'qwen3-coder-plus'); + }); + + test('opencode.sonnet override wins; opus and haiku still use built-in defaults', () => { + writeConfig(tmpDir, { + runtime: 'opencode', + model_profile: 'balanced', + model_profile_overrides: { + opencode: { sonnet: 'anthropic/claude-sonnet-4-7' }, + }, + }); + // gsd-planner balanced -> opus -> built-in default + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'anthropic/claude-opus-4-7'); + // gsd-roadmapper balanced -> sonnet -> overridden + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-4-7'); + // gsd-codebase-mapper balanced -> haiku -> built-in default (haiku not overridden) + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'anthropic/claude-haiku-4-5'); + }); + + test('copilot.haiku override wins; opus and sonnet still use built-in defaults', () => { + writeConfig(tmpDir, { + runtime: 'copilot', + model_profile: 'budget', + model_profile_overrides: { + copilot: { haiku: 'claude-haiku-4-6' }, + }, + }); + // gsd-codebase-mapper budget -> haiku -> overridden + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'claude-haiku-4-6'); + // gsd-planner budget -> sonnet -> built-in default + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-sonnet-4-6'); + }); +});