get-shit-done:修复 `resolve-model` 在 Claude 运行时忽略 `resolve_model_ids: true` 的完整解析链剖析
本篇基于 changeset 文档 3643-resolve-model-claude-runtime.md 展开,讲解 get-shit-done(GSD)中模型 ID 解析器 gsd-sdk query resolve-model 的一个缺陷及其修复:当 runtime: "claude" 且 resolve_model_ids: true 时,解析器此前只返回 opus / sonnet / haiku 这类 tier 别名,而不是消费方真正需要的完整 Claude 模型 ID(如 claude-sonnet-4-6)。读完后你将掌握:resolve-model 的完整优先级链(model_overrides → 阶段类型档位 → 运行时解析 → resolve_model_ids 处理 → profile 查询)、Claude 作为“隐式默认运行时”造成的解析分支漏洞成因,以及共享模型目录(model catalog)如何成为所有运行时派生模型 ID 的唯一事实来源。
问题背景:tier 别名与完整模型 ID 的两层语义
GSD 内部用统一的 tier 别名(opus / sonnet / haiku)描述“这一档应该用多强的模型”,而具体运行时再把它映射为可提交给 API 的模型 ID。resolve-model 命令的作用就是把某个 agent 名按当前配置解析出最终模型标识,调用形式见 CLI-TOOLS.md:
# Get model for agent based on current profile
node gsd-tools.cjs resolve-model <agent-name>
# Raw output returns the selected model ID/tier.
# JSON output also includes profile and, when the active runtime supports it,
# reasoning_effort.
其中 resolve_model_ids 是控制这层映射的开关,语义定义在 CONFIGURATION.md:非 Claude 运行时安装时自动写入 resolve_model_ids: "omit",让所有 agent 使用运行时自身配置的默认模型;而 Claude 侧显式开启 true 时,应返回完整 Claude ID。本 bug(#3643)就是 SDK 移植路径(TS 版)在 Claude 运行时下没有兑现 true 的承诺。
缺陷成因:Claude 在运行时解析中的“提前退出”
SDK 的解析器实现位于 config-query.ts。其中 resolveRuntimeTier 负责“非 Claude 运行时”的档位到模型映射:
// sdk/src/query/config-query.ts(L195-L199)
function resolveRuntimeTier(config: Record<string, unknown>, tier: string): RuntimeTierEntry | null {
if (!isRuntimeTierName(tier)) return null;
const runtime = typeof config.runtime === 'string' ? config.runtime : '';
if (!runtime || runtime === 'claude') return null;
...
这里的设计意图是:Claude 是隐式/默认运行时,tier 别名(sonnet 等)本就是 Claude API 原生可识别的模型标识,所以 Claude 不需要运行时解析这一步,函数直接返回 null 退出。问题在于:退出后代码沿“profile 查询”路径落到文件末尾的 alias 返回,而 TS 移植版在返回 alias 之前没有检查 resolve_model_ids。changeset 原文描述的正是这条链路:
previously the resolver bailed out of
resolveRuntimeTierfor Claude (because Claude is the implicit/default runtime inconfig-query.ts:149) and fell through to the alias-return path ... without consulting the catalog. Consumers asking for resolved model IDs received the tier alias (opus/sonnet/haiku) instead of the full Claude model ID.
于是同一个配置在 CJS 路径与 SDK 路径上出现行为漂移:CJS 版本(core.cjs L1348-L1350)有对应的兜底分支:
// 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;
}
changeset 指出的核心事实是:这个 CJS 分支在 TS 移植时丢失了。
修复方案:让 Claude 也走共享模型目录
修复在 resolveModel 处理器的 alias 返回之前新增了一个 Claude 专属分支(config-query.ts L293-L313):
// #3643: runtime:claude bails out of resolveRuntimeTier (line 149) because
// Claude is the implicit/default runtime, but consumers that asked for
// resolved model IDs still need the full ID (e.g. "claude-sonnet-4-6"), not
// the tier alias. Mirror the CJS branch at get-shit-done/bin/lib/core.cjs
// (`if (config.resolve_model_ids) return MODEL_ALIAS_MAP[alias] || alias;`)
// by consulting the catalog's claude runtime defaults for the resolved tier.
const runtime = typeof (config as Record<string, unknown>).runtime === 'string'
? ((config as Record<string, unknown>).runtime as string)
: '';
// Empty/missing runtime is implicit Claude (per the resolveRuntimeTier bail-out
// at line ~149); without this branch the resolved-IDs path silently fell
// through to the alias return for projects that never set `runtime` explicitly.
const isClaudeRuntime = runtime === '' || runtime === 'claude';
if (resolveModelIds === true && isClaudeRuntime && isRuntimeTierName(tier)) {
const claudeDefault = resolveRuntimeTierDefault('claude', tier);
if (claudeDefault?.model) {
return { data: { model: claudeDefault.model, profile } };
}
}
return { data: { model: alias, profile } };
三个实现要点值得注意:
-
runtime === ''与runtime === 'claude'等价处理。源码注释明确说明:空的/缺失的runtime就是隐式 Claude(与resolveRuntimeTier的提前退出条件一致),如果不把空字符串也计入,那些从未显式设置runtime的项目会继续静默漏到 alias 返回路径上。 -
调用共享目录而不是私有映射表。分支通过
resolveRuntimeTierDefault('claude', tier)取 Claude 默认 ID,该函数定义于 model-catalog.ts L60-L62,读的是 model-catalog.json 的runtimeTierDefaults.claude表:"claude": { "opus": { "model": "claude-opus-4-7" }, "sonnet": { "model": "claude-sonnet-4-6" }, "haiku": { "model": "claude-haiku-4-5" } }changeset 强调这一设计决策的价值:“both runtimes derive Claude IDs from the same source of truth”——Claude 原生路径与 opencode/copilot 等经由目录映射 Claude 的路径(同文件中
opencode、copilot条目分别映射到anthropic/claude-sonnet-4-6等带 provider 前缀的 ID)从此共用同一份 Claude 档位表,避免 CJS 侧MODEL_ALIAS_MAP与 SDK 侧目录各自维护而再次漂移。 -
只在
tier是合法档位别名时才查目录(isRuntimeTierName(tier)),且查不到时优雅降级回 alias 返回,不抛错。
完整优先级链:修复后的解析顺序
结合 core.cjs L1270-L1354 的注释结构与 SDK 实现,resolve-model 的完整决策顺序(修复后 CJS/TS 双路径对齐)为:
| 步骤 | 条件 | 返回 |
|---|---|---|
| 1 | model_overrides[agentType] 存在 |
覆盖值原样返回(最高优先级) |
| 2 | 计算 tier | config.models[phaseType](阶段类型档位,#3023)优先,否则按 model_profile 查 MODEL_PROFILES[agent] |
| 3 | 运行时解析 | runtime 显式设为非 Claude 值且 tier 非 inherit 时走 resolveRuntimeTier(内置默认 + model_profile_overrides[runtime] 合并,非推理型运行时剥离 reasoning_effort) |
| 4 | resolve_model_ids: "omit" |
返回空字符串,让运行时用自己的默认模型(安装器为 Codex 等自动设置) |
| 5 | resolve_model_ids: true 且 runtime 为(隐式或显式)Claude(本次修复) |
查共享目录 runtimeTierDefaults.claude[tier],返回完整 Claude ID |
| 6 | 其余情况 | 返回 tier 别名(opus/sonnet/haiku) |
changeset 明确保证前几级优先级不受影响:“model_overrides, the phase-type tier override (config.models[phaseType]), and resolve_model_ids: "omit" all retain their existing precedence.” 也就是说 omit 仍然先于步骤 5 生效(见 SDK 实现中 resolveModelIds === 'omit' 分支位于新增 Claude 分支之前,config-query.ts L289-L291)。
一个可直接复现的行为对照示例:
// .planning/config.json
{
"model_profile": "balanced",
"runtime": "claude",
"resolve_model_ids": true
}
gsd-sdk query resolve-model gsd-executor 修复前返回 { model: "sonnet", profile: "balanced" },修复后返回 { model: "claude-sonnet-4-6", profile: "balanced" }(gsd-executor 在 balanced profile 下对应 sonnet 档位,见 model-catalog.json 的 agents 表)。
测试验证:RED→GREEN 的回归防护
sdk/src/query/config-query.test.ts 中针对 #3643 新增了一组断言,覆盖修复行为与两条回归护栏:
- balanced/quality/budget 三档全覆盖:
runtime: 'claude' + resolve_model_ids: true下,gsd-executor(balanced)返回claude-sonnet-4-6,gsd-planner(quality)返回claude-opus-4-7,gsd-verifier(budget)返回claude-haiku-4-5; - 阶段类型档位优先:
model_profile: 'budget'但models: { execution: 'opus' }时,gsd-executor返回claude-opus-4-7,证明步骤 2 的 phase-type 覆盖在 Claude 解析路径上依然生效; - 回归护栏一:
runtime: 'claude'但未设置resolve_model_ids时,仍返回别名sonnet——未显式开启解析的项目零行为变化; - 回归护栏二:
resolve_model_ids: "omit"在 Claude 运行时下仍然胜过别名映射,返回空模型参数。
测试注释直接标注了症状与根因位置:"aliases (opus/sonnet/haiku) leaked through to consumers that asked for resolved model IDs because resolveRuntimeTier bails for runtime:claude and the alias-return fall-through ignored resolve_model_ids. CJS branch at get-shit-done/bin/lib/core.cjs:1348-1350 has the missing guard."
小结与适用边界
这个 changeset 修复的本质是跨运行时移植时的分支遗漏:CJS 中“别名 → 完整 ID”的兜底逻辑在 TS 移植时被省略,而 Claude 作为隐式默认运行时恰好是 resolveRuntimeTier 的提前退出点,两条逻辑叠加使得缺陷只在“SDK 路径 + Claude 运行时 + 显式 resolve_model_ids: true”这一组合下暴露。修复方式不是复刻一张 MODEL_ALIAS_MAP,而是让 Claude 分支复用 model-catalog.json 的 runtimeTierDefaults 表,使 CJS 与 TS、原生 Claude 与第三方运行时统一从同一份数据源派生模型 ID。需要注意的适用前提:该修复作用于 gsd-sdk query resolve-model 的 SDK 查询路径(对应 CLI 侧 gsd-tools.cjs resolve-model 的既有行为),完整 ID 具体取值随 model-catalog.json 中 Claude 档位表更新而变化,仓库中当前为 claude-opus-4-7 / claude-sonnet-4-6 / claude-haiku-4-5。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00