fix: resolve ProviderModelNotFoundError on non-Claude runtimes (#1156)

Extend resolve_model_ids to accept "omit" value: returns empty string
so non-Claude runtimes (OpenCode, Codex, Gemini, etc.) use their
configured default model instead of unresolvable Claude aliases.

- resolve_model_ids: "omit" short-circuits before alias resolution
- model_overrides still respected (checked first) for explicit IDs
- Installer sets resolve_model_ids: "omit" in ~/.gsd/defaults.json
  for non-Claude runtimes during install
- 4 new tests covering omit behavior and override passthrough
- Fix websearch test mocks for fs.writeSync output change (#1276)

Closes #1156

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-21 00:57:20 -04:00
parent 3f2c0a16e0
commit 02254db611
4 changed files with 63 additions and 8 deletions

View File

@@ -4171,6 +4171,27 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
configureOpencodePermissions(isGlobal);
}
// For non-Claude runtimes, set resolve_model_ids: "omit" in ~/.gsd/defaults.json
// so resolveModelInternal() returns '' instead of Claude aliases (opus/sonnet/haiku)
// that the runtime can't resolve. Users can still use model_overrides for explicit IDs.
// See #1156.
if (runtime !== 'claude') {
const gsdDir = path.join(os.homedir(), '.gsd');
const defaultsPath = path.join(gsdDir, 'defaults.json');
try {
fs.mkdirSync(gsdDir, { recursive: true });
let defaults = {};
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
if (defaults.resolve_model_ids !== 'omit') {
defaults.resolve_model_ids = 'omit';
fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
}
} catch (e) {
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
}
}
let program = 'Claude Code';
if (runtime === 'opencode') program = 'OpenCode';
if (runtime === 'gemini') program = 'Gemini';

View File

@@ -205,7 +205,7 @@ function loadConfig(cwd) {
exa_search: false,
text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus
sub_repos: [],
resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs
resolve_model_ids: false, // false: return alias as-is | true: map to full Claude model ID | "omit": return '' (runtime uses its default)
context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
};
@@ -885,12 +885,20 @@ const MODEL_ALIAS_MAP = {
function resolveModelInternal(cwd, agentType) {
const config = loadConfig(cwd);
// Check per-agent override first
// Check per-agent override first — always respected regardless of resolve_model_ids.
// Users who set fully-qualified model IDs (e.g., "openai/gpt-5.4") get exactly that.
const override = config.model_overrides?.[agentType];
if (override) {
return override;
}
// resolve_model_ids: "omit" — return empty string so the runtime uses its configured
// default model. For non-Claude runtimes (OpenCode, Codex, etc.) that don't recognize
// Claude aliases (opus/sonnet/haiku/inherit). Set automatically during install. See #1156.
if (config.resolve_model_ids === 'omit') {
return '';
}
// Fall back to profile lookup
const profile = String(config.model_profile || 'balanced').toLowerCase();
const agentModels = MODEL_PROFILES[agentType];
@@ -898,8 +906,8 @@ function resolveModelInternal(cwd, agentType) {
if (profile === 'inherit') return 'inherit';
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
// If resolve_model_ids is true, map alias to full model ID
// This prevents 404s when the Task tool passes aliases directly to the API
// resolve_model_ids: true — map alias to full Claude model ID
// Prevents 404s when the Task tool passes aliases directly to the API
if (config.resolve_model_ids) {
return MODEL_ALIAS_MAP[alias] || alias;
}

View File

@@ -1195,15 +1195,16 @@ describe('websearch command', () => {
const { cmdWebsearch } = require('../get-shit-done/bin/lib/commands.cjs');
let origFetch;
let origApiKey;
let origStdoutWrite;
let origWriteSync;
let captured;
beforeEach(() => {
origFetch = global.fetch;
origApiKey = process.env.BRAVE_API_KEY;
origStdoutWrite = process.stdout.write;
origWriteSync = fs.writeSync;
captured = '';
process.stdout.write = (chunk) => { captured += chunk; return true; };
// output() uses fs.writeSync(1, data) since #1276 — mock it to capture output
fs.writeSync = (fd, data) => { if (fd === 1) captured += data; return Buffer.byteLength(String(data)); };
});
afterEach(() => {
@@ -1213,7 +1214,7 @@ describe('websearch command', () => {
} else {
delete process.env.BRAVE_API_KEY;
}
process.stdout.write = origStdoutWrite;
fs.writeSync = origWriteSync;
});
test('returns available=false when BRAVE_API_KEY is unset', async () => {

View File

@@ -281,6 +281,31 @@ describe('resolveModelInternal', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus');
});
});
describe('resolve_model_ids: "omit"', () => {
test('returns empty string for known agents', () => {
writeConfig({ resolve_model_ids: 'omit' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), '');
});
test('returns empty string for unknown agents', () => {
writeConfig({ resolve_model_ids: 'omit' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-nonexistent'), '');
});
test('still respects model_overrides even when omit', () => {
writeConfig({
resolve_model_ids: 'omit',
model_overrides: { 'gsd-planner': 'openai/gpt-5.4' },
});
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'openai/gpt-5.4');
});
test('returns empty string with inherit profile', () => {
writeConfig({ resolve_model_ids: 'omit', model_profile: 'inherit' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), '');
});
});
});
// ─── escapeRegex ───────────────────────────────────────────────────────────────