* feat: make model profiles runtime-aware for Codex/non-Claude runtimes (closes#2517)
Adds an optional top-level `runtime` config key plus a
`model_profile_overrides[runtime][tier]` map. When `runtime` is set,
profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs
(and reasoning_effort where supported) instead of bare Claude aliases.
Codex defaults from the spec:
opus -> gpt-5.4 reasoning_effort: xhigh
sonnet -> gpt-5.3-codex reasoning_effort: medium
haiku -> gpt-5.4-mini reasoning_effort: medium
Claude defaults mirror MODEL_ALIAS_MAP. Unknown runtimes fall back to
the Claude-alias safe default rather than emit IDs the runtime cannot
accept. reasoning_effort is only emitted into Codex install paths;
never returned from resolveModelInternal and never written to Claude
agent frontmatter.
Backwards compatible: any user without `runtime` set sees identical
behavior — the new branch is gated on `config.runtime != null`.
Precedence (highest to lowest):
1. per-agent model_overrides
2. runtime-aware tier resolution (when `runtime` is set)
3. resolve_model_ids: "omit"
4. Claude-native default
5. inherit (literal passthrough)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#2517): address adversarial review of #2609 (findings 1-16)
Addresses all 16 findings from the adversarial review of PR #2609.
Each finding is enumerated below with its resolution.
CRITICAL
- F1: readGsdRuntimeProfileResolver(targetDir) now probes per-project
.planning/config.json AND ~/.gsd/defaults.json with per-project winning,
so the PR's headline claim ("set runtime in project config and Codex
TOML emit picks it up") actually holds end-to-end.
- F2: resolveTierEntry field-merges user overrides with built-in defaults.
The CONFIGURATION.md string-shorthand example
`{ codex: { opus: "gpt-5-pro" } }`
now keeps reasoning_effort from the built-in entry. Partial-object
overrides like `{ opus: { reasoning_effort: 'low' } }` keep the
built-in model. Both paths regression-tested.
MAJOR
- F3: resolveReasoningEffortInternal gates strictly on the
RUNTIMES_WITH_REASONING_EFFORT allowlist regardless of override
presence. Override + unknown-runtime no longer leaks reasoning_effort.
- F4: runtime:"claude" is now a no-op for resolution (it is the implicit
default). It no longer hijacks resolve_model_ids:"omit". Existing
tests for `runtime:"claude"` returning Claude IDs were rewritten to
reflect the no-op semantics; new test asserts the omit case returns "".
- F5: _readGsdConfigFile in install.js writes a stderr warning on JSON
parse failure instead of silently returning null. Read failure and
parse failure are warned separately. Library require is hoisted to top
of install.js so it is not co-mingled with config-read failure modes.
- F6: install.js requires for core.cjs / model-profiles.cjs are hoisted
to the top of the file with __dirname-based absolute paths so global
npm install works regardless of cwd. Test asserts both lib paths exist
relative to install.js __dirname.
- F7: docs/CONFIGURATION.md `runtime` row no longer lists `opencode` as
a valid runtime — install-path emission for non-Codex runtimes is
explicitly out of scope per #2517 / #2612, and the doc now points at
#2612 for the follow-on work. resolveModelInternal still accepts any
runtime string (back-compat) and falls back safely for unknown values.
- F8: Tests now isolate HOME (and GSD_HOME) to a per-test tmpdir so the
developer's real ~/.gsd/defaults.json cannot bleed into assertions.
Same pattern CodeRabbit caught on PRs #2603 / #2604.
- F9: `runtime` and `model_profile_overrides` documented as flat-only
in core.cjs comments — not routed through `get()` because they are
top-level keys per docs/CONFIGURATION.md and introducing nested
resolution for two new keys was not worth the edge-case surface.
- F10/F13: loadConfig now invokes _warnUnknownProfileOverrides on the
raw parsed config so direct .planning/config.json edits surface
unknown runtime values (e.g. typo `runtime: "codx"`) and unknown
tier values (e.g. `model_profile_overrides.codex.banana`) at read
time. Warnings only — preserves back-compat for runtimes added
later. Per-process warning cache prevents log spam across repeated
loadConfig calls.
MINOR / NIT
- F11: Removed dead `tier || 'sonnet'` defensive shortcut. The local
is now `const alias = tier;` with a comment explaining why `tier`
is guaranteed truthy at that point (every MODEL_PROFILES entry
defines `balanced`, the fallback profile).
- F12: Extracted resolveTierEntry() in core.cjs as the single source
of truth for runtime-aware tier resolution. core.cjs and bin/install.js
both consume it — no duplicated lookup logic between the two files.
- F14: Added regression tests for findings #1, #2, #3, #4, #6, #10, #13
in tests/issue-2517-runtime-aware-profiles.test.cjs. Each must-fix
path has a corresponding test that fails against the pre-fix code
and passes against the post-fix code.
- F15: docs/CONFIGURATION.md `model_profile` row cross-references
#1713 / #1806 next to the `adaptive` enum value.
- F16: RUNTIME_PROFILE_MAP remains in core.cjs as the single source of
truth; install.js imports it through the exported resolveTierEntry
helper rather than carrying its own copy. Doc files (CONFIGURATION.md,
USER-GUIDE.md, settings.md) intentionally still embed the IDs as text
— code comment in core.cjs flags that those doc files must be updated
whenever the constant changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>