fix(#2068): dynamic routing escalates the model per --attempt (#2334)

* fix(#2068): dynamic routing escalates the model per --attempt, not just effort

cmdResolveExecution resolved the model via resolveModelInternal (which ignores
dynamic_routing), so retries escalated effort but the model stayed pinned to the
default tier. Resolve the model via resolveModelForTier when --attempt is given,
gated identically to the effort resolution so model and effort stay symmetric —
an omitted --attempt keeps the classic profile model (unchanged for everyone,
incl. dynamic_routing users who don't pass --attempt). Escalation is capped at
max_escalations. Falls back to resolveModelInternal when dynamic_routing is off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#2068): backfill PR number 2334 into changeset

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-16 15:25:19 -04:00
committed by GitHub
parent 1794acb255
commit a30fb75b51
3 changed files with 185 additions and 2 deletions

View File

@@ -30,7 +30,7 @@ import roadmapParserMod = require('./roadmap-parser.cjs');
const { extractCurrentMilestone, stripShippedMilestones: _stripShippedMilestones, getMilestoneInfo, getMilestonePhaseFilter, getRoadmapPhaseInternal } = roadmapParserMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import modelResolverMod = require('./model-resolver.cjs');
const { resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
const { resolveModelInternal, resolveModelForTier, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, resolveGranularityInternal, assertValidGranularityOverride } = modelResolverMod;
import { renderEffortForRuntime, RUNTIMES_WITH_FAST_MODE } from './model-catalog.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import planningWorkspace = require('./planning-workspace.cjs');
@@ -488,7 +488,16 @@ function cmdResolveExecution(cwd: string, agentType: string | undefined, raw: bo
opts = opts || {};
const config = loadConfig(cwd);
const profile = (config['model_profile'] as string) || 'balanced';
const model = resolveModelInternal(cwd, agentType!);
// #2068: resolve the model per-attempt so dynamic_routing escalates the MODEL
// (heavy tier) alongside effort. Gated on an explicit --attempt exactly like the
// effort resolution below, so the two fields stay symmetric: with no --attempt
// the model comes from the classic profile path (unchanged for everyone,
// including dynamic_routing-enabled users who don't pass --attempt), and only an
// explicit attempt routes through the tier ladder. resolveModelForTier itself
// still falls back to resolveModelInternal when dynamic_routing is off.
const model = (opts.attempt !== undefined && opts.attempt !== null)
? resolveModelForTier(cwd, agentType!, opts.attempt)
: resolveModelInternal(cwd, agentType!);
const effortOpts: Record<string, unknown> = {};
if (typeof opts.effortOverride === 'string') effortOpts['override'] = opts.effortOverride;