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');
+ });
+});