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:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user