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);
|
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';
|
let program = 'Claude Code';
|
||||||
if (runtime === 'opencode') program = 'OpenCode';
|
if (runtime === 'opencode') program = 'OpenCode';
|
||||||
if (runtime === 'gemini') program = 'Gemini';
|
if (runtime === 'gemini') program = 'Gemini';
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ function loadConfig(cwd) {
|
|||||||
exa_search: false,
|
exa_search: false,
|
||||||
text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus
|
text_mode: false, // when true, use plain-text numbered lists instead of AskUserQuestion menus
|
||||||
sub_repos: [],
|
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
|
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)
|
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
|
||||||
};
|
};
|
||||||
@@ -885,12 +885,20 @@ const MODEL_ALIAS_MAP = {
|
|||||||
function resolveModelInternal(cwd, agentType) {
|
function resolveModelInternal(cwd, agentType) {
|
||||||
const config = loadConfig(cwd);
|
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];
|
const override = config.model_overrides?.[agentType];
|
||||||
if (override) {
|
if (override) {
|
||||||
return 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
|
// Fall back to profile lookup
|
||||||
const profile = String(config.model_profile || 'balanced').toLowerCase();
|
const profile = String(config.model_profile || 'balanced').toLowerCase();
|
||||||
const agentModels = MODEL_PROFILES[agentType];
|
const agentModels = MODEL_PROFILES[agentType];
|
||||||
@@ -898,8 +906,8 @@ function resolveModelInternal(cwd, agentType) {
|
|||||||
if (profile === 'inherit') return 'inherit';
|
if (profile === 'inherit') return 'inherit';
|
||||||
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||||
|
|
||||||
// If resolve_model_ids is true, map alias to full model ID
|
// resolve_model_ids: true — map alias to full Claude model ID
|
||||||
// This prevents 404s when the Task tool passes aliases directly to the API
|
// Prevents 404s when the Task tool passes aliases directly to the API
|
||||||
if (config.resolve_model_ids) {
|
if (config.resolve_model_ids) {
|
||||||
return MODEL_ALIAS_MAP[alias] || alias;
|
return MODEL_ALIAS_MAP[alias] || alias;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1195,15 +1195,16 @@ describe('websearch command', () => {
|
|||||||
const { cmdWebsearch } = require('../get-shit-done/bin/lib/commands.cjs');
|
const { cmdWebsearch } = require('../get-shit-done/bin/lib/commands.cjs');
|
||||||
let origFetch;
|
let origFetch;
|
||||||
let origApiKey;
|
let origApiKey;
|
||||||
let origStdoutWrite;
|
let origWriteSync;
|
||||||
let captured;
|
let captured;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
origFetch = global.fetch;
|
origFetch = global.fetch;
|
||||||
origApiKey = process.env.BRAVE_API_KEY;
|
origApiKey = process.env.BRAVE_API_KEY;
|
||||||
origStdoutWrite = process.stdout.write;
|
origWriteSync = fs.writeSync;
|
||||||
captured = '';
|
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(() => {
|
afterEach(() => {
|
||||||
@@ -1213,7 +1214,7 @@ describe('websearch command', () => {
|
|||||||
} else {
|
} else {
|
||||||
delete process.env.BRAVE_API_KEY;
|
delete process.env.BRAVE_API_KEY;
|
||||||
}
|
}
|
||||||
process.stdout.write = origStdoutWrite;
|
fs.writeSync = origWriteSync;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns available=false when BRAVE_API_KEY is unset', async () => {
|
test('returns available=false when BRAVE_API_KEY is unset', async () => {
|
||||||
|
|||||||
@@ -281,6 +281,31 @@ describe('resolveModelInternal', () => {
|
|||||||
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'opus');
|
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 ───────────────────────────────────────────────────────────────
|
// ─── escapeRegex ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user