Forward-port of the #1133 hotfix (commit 22f237a5, 1.4.5 hotfix line) onto next. The hotfix was authored against src/core.cts (v1.4.4); on next the resolver logic lives in src/model-resolver.cts (ADR-457 extraction, #888), so the patch is re-applied there rather than cherry-picked. resolveModelInternal step 2.5 now honors model_policy on the claude runtime: the policy-resolved full model ID is mapped back to a Claude Code agent alias via CLAUDE_POLICY_ID_TO_ALIAS (reverse of MODEL_ALIAS_MAP + claude-fable-5 -> fable). Bare aliases (opus/sonnet/haiku/fable) pass through; an ID with no Claude alias warns once to stderr (deduped by agentType::policyModel::tier) and falls back to the configured tier alias. Non-claude runtimes return full IDs verbatim (unchanged). resolveModelForTier is intentionally unchanged. The warn-dedupe cache lives in model-resolver.cts; core.cts composes the exported _resetRuntimeWarningCacheForTests to clear both that cache and the config-loader warning cache (config-loader cannot import model-resolver -- circular dependency). Ports the 6 #1133 tests (rewriting the old claude-no-op test that asserted the bug) plus one added test covering the MODEL_ALIAS_MAP reverse-map path. Forward-port of #1133 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,7 @@ const {
|
||||
resolveEffortInternal,
|
||||
resolveFastModeInternal,
|
||||
resolveEffortForTier,
|
||||
_resetModelPolicyWarningCacheForTests,
|
||||
} = modelResolverModule;
|
||||
|
||||
// ─── Path helpers ────────────────────────────────────────────────────────────
|
||||
@@ -416,7 +417,10 @@ export = {
|
||||
resolveTierEntry,
|
||||
resolveModelPolicy,
|
||||
KNOWN_PROVIDERS,
|
||||
_resetRuntimeWarningCacheForTests,
|
||||
_resetRuntimeWarningCacheForTests: (): void => {
|
||||
_resetRuntimeWarningCacheForTests();
|
||||
_resetModelPolicyWarningCacheForTests();
|
||||
},
|
||||
pathExistsInternal,
|
||||
gitWorktreeInfoInternal,
|
||||
generateSlugInternal,
|
||||
|
||||
@@ -76,6 +76,36 @@ function _resolveRuntimeTier(config: Record<string, unknown>, tier: string): Tie
|
||||
});
|
||||
}
|
||||
|
||||
// Reverse of the Claude tier-default IDs, plus the Fable alias which Claude
|
||||
// Code's Agent tool accepts but which is not a GSD model-profile tier (#1133).
|
||||
const CLAUDE_POLICY_ID_TO_ALIAS: Record<string, string> = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(MODEL_ALIAS_MAP)
|
||||
.filter((e): e is [string, string] => typeof e[1] === 'string')
|
||||
.map(([aliasName, id]) => [id, aliasName]),
|
||||
),
|
||||
'claude-fable-5': 'fable',
|
||||
};
|
||||
const CLAUDE_AGENT_ALIASES = new Set(['opus', 'sonnet', 'haiku', 'fable']);
|
||||
|
||||
// Dedupe stderr warnings so repeated agent resolutions don't spam (#1133).
|
||||
const _modelPolicyUnmappableWarned = new Set<string>();
|
||||
function warnModelPolicyUnmappable(agentType: string, policyModel: string, tier: string): void {
|
||||
const key = `${agentType}::${policyModel}::${tier}`;
|
||||
if (_modelPolicyUnmappableWarned.has(key)) return;
|
||||
_modelPolicyUnmappableWarned.add(key);
|
||||
// MUST go to stderr — resolve-model's JSON result is parsed from stdout.
|
||||
process.stderr.write(
|
||||
`gsd: warning — model_policy resolved "${policyModel}" for ${agentType}, ` +
|
||||
`but it has no Claude agent alias; using "${tier}" instead.\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// Test-only: reset the model_policy warn-dedupe cache between cases (#1133).
|
||||
function _resetModelPolicyWarningCacheForTests(): void {
|
||||
_modelPolicyUnmappableWarned.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* #49 — Provider-neutral model policy preset resolution.
|
||||
*/
|
||||
@@ -153,14 +183,29 @@ function resolveModelInternal(cwd: string, agentType: string): string {
|
||||
? 'inherit'
|
||||
: (agentModels ? (agentModels[profile] || agentModels['balanced']) : null));
|
||||
|
||||
// 2.5. model_policy preset (#49)
|
||||
// 2.5. model_policy preset (#49, #1133)
|
||||
const configRuntime = config['runtime'] as string | null | undefined;
|
||||
if (configRuntime && configRuntime !== 'claude' && tier && tier !== 'inherit') {
|
||||
if (tier && tier !== 'inherit') {
|
||||
const onClaude = !configRuntime || configRuntime === 'claude';
|
||||
const effectiveRuntime = configRuntime || 'claude';
|
||||
const mergedPolicy = config['model_policy']
|
||||
? { ...(config['model_policy'] as Record<string, unknown>), runtime: configRuntime }
|
||||
? { ...(config['model_policy'] as Record<string, unknown>), runtime: effectiveRuntime }
|
||||
: null;
|
||||
const policyModel = resolveModelPolicy(mergedPolicy, tier);
|
||||
if (policyModel) return policyModel;
|
||||
if (policyModel) {
|
||||
// Non-Claude runtimes take full model IDs verbatim (unchanged behavior).
|
||||
if (!onClaude) return policyModel;
|
||||
// Claude Code's Agent tool takes tier aliases (opus/sonnet/haiku/fable),
|
||||
// not full model IDs — map the policy-resolved ID back to an alias (#1133).
|
||||
const aliasForId = CLAUDE_POLICY_ID_TO_ALIAS[policyModel];
|
||||
if (aliasForId) return aliasForId;
|
||||
// The policy value may already be a bare Claude agent alias (e.g. "fable").
|
||||
if (CLAUDE_AGENT_ALIASES.has(policyModel)) return policyModel;
|
||||
// No Claude alias for this ID (e.g. a pinned minor version like
|
||||
// claude-opus-4-5). Warn once and fall through to the tier alias rather
|
||||
// than returning an ID Claude Code cannot spawn.
|
||||
warnModelPolicyUnmappable(agentType, policyModel, tier);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Runtime-aware resolution (#2517)
|
||||
@@ -463,6 +508,7 @@ export = {
|
||||
resolveTierEntry,
|
||||
resolveModelPolicy,
|
||||
resolveModelInternal,
|
||||
_resetModelPolicyWarningCacheForTests,
|
||||
VALID_GRANULARITIES,
|
||||
resolveGranularityInternal,
|
||||
assertValidGranularityOverride,
|
||||
|
||||
Reference in New Issue
Block a user