* fix(3620): point the docs at files that actually exist docproof found 34 stale references; the reporter hand-read all 34 and reported the 8 that are real, explaining why the other 26 are deliberate (files the documents themselves label legacy or "superseded by", and one pre-Diataxis link label whose target still resolves). Those 26 are left alone — re-touching them would contradict the issue's own analysis. Every claim was re-verified against git ls-files at HEAD before editing. docs/INVENTORY.md said its roster is anchored by six drift-control tests. Five are gone (commands-doc-parity, agents-doc-parity, cli-modules-doc-parity, hooks-doc-parity in 5d8a8c4d; command-count-sync infbf30792), so the sentence now names the one that exists. Whether one test is sufficient coverage is a maintainer question the issue explicitly declined to answer, so no new drift tests are proposed here. The four translations were a revision further behind, each naming a seventh test deleted inae8bb707that the English file had already dropped. All four now match. Renamed targets corrected in CONTEXT.md, VERSIONING.md, docs/CONFIGURATION.md and the update workflow. The new test names carry no issue-NNN- prefix, which is what lint-regression-test-names requires, so they are the correct targets. docs/TESTING-SUITES.md is the one that could cost somebody time: it INSTRUCTED contributors to add an acknowledgment to the legacy drift-ack file, which CONTRIBUTING.md says to never use. Rewritten from the real workflow — per-PR fragments under the drift-acks directory, and a spent base-side ack is re-armed by rewording that fragment's reason in place, never by adding a duplicate, since two sources naming one path is a hard error. docs/skills/discovery-contract.md's heading named a query module deleted in11918dcc. The section was REMOVED rather than retargeted: its documented behavior (skip the deprecated root) is not what the surviving code does — skill-manifest includes that root marked deprecated:true — so retargeting would have documented something false. Found and fixed inline, same class: VERSIONING.md described an SDK bundling step the release workflow does not have (zero such mentions in that file); CONFIGURATION.md and four translations named a dead model-catalog triple collapsed by ADR-457. Dead config removed: the changeset lint's user-facing prefix list still carried two retired sdk entries. git ls-files -- 'sdk/*' returns nothing. No test pins that array. Left deliberately: the comment explaining the retired catalog path, the install regression test that reconstructs the old broken layout to prove it fails, and the generated test-timings cache. Each is a legitimate mention of a dead path, not drift. Note lint-removed-but-needed cannot catch this class: it diffs baseRef...HEAD, so it only sees files deleted in the change under review. These were orphaned by PRs that predate the lint. A repo-wide existence audit would need a suppression mechanism for the 26 deliberate mentions above; that is a feature, not part of this fix. Fixes #3620 * chore(3620): backfill changeset PR number (#3658) --------- Co-authored-by: sim <sim@local>
11 KiB
How to configure model profiles
Choose the right model tier strategy for your project, then tune individual agents or entire phase types without writing a large override block. This guide starts with the simplest lever and works up to dynamic routing.
The four profiles (plus adaptive and inherit)
Set model_profile in .planning/config.json or via /gsd-config --profile <name>:
| Profile | Planner | Executor | Researchers | Verifier | Use when |
|---|---|---|---|---|---|
quality |
Opus | Opus | Opus | Sonnet | Production-quality work where cost is secondary |
balanced |
Opus | Sonnet | Sonnet | Sonnet | Normal development — the default |
budget |
Sonnet | Sonnet | Haiku | Haiku | Rapid prototyping, cost-sensitive contexts |
adaptive |
Opus | Sonnet | Sonnet | Sonnet | Resolves the same way as the other tiers under runtime-aware profiles; use when switching between runtimes frequently |
inherit |
(session model) | (session model) | (session model) | (session model) | Non-Anthropic providers (OpenRouter, local models) — all agents follow your current session model |
The table above shows a representative subset. All 33 shipped agents have explicit per-profile tier assignments in gsd-core/bin/shared/model-catalog.json. For the full table see Model Profiles in the configuration reference.
Quick switch via command:
/gsd-config --profile balanced # Normal development
/gsd-config --profile budget # Prototyping or high-cost phases
/gsd-config --profile quality # Production release
/gsd-config --profile inherit # OpenRouter, local models
Or edit .planning/config.json directly:
{
"model_profile": "balanced"
}
Per-agent overrides (model_overrides)
If a single agent needs a different tier without changing the whole profile, use model_overrides:
{
"model_profile": "balanced",
"model_overrides": {
"gsd-executor": "opus",
"gsd-codebase-mapper": "haiku"
}
}
Valid values: opus, sonnet, haiku, inherit, or any fully-qualified model ID (e.g. "openai/o3", "google/gemini-2.5-pro").
model_overrides can be set per-project in .planning/config.json or globally in ~/.gsd/defaults.json. Per-project entries win on conflict; non-conflicting global entries are preserved.
Important for Codex and OpenCode: Those runtimes embed the model into each agent's static config at install time rather than choosing it per spawn, so after editing model_overrides you must re-run the installer for the change to take effect:
npx @opengsd/gsd-core@latest --codex --global # or --opencode, --kilo, etc.
GSD will also warn you if you forget: workflow entry commands (gsd init plan-phase, gsd init execute-phase, etc.) detect when .planning/config.json or ~/.gsd/defaults.json is newer than your installed agent files and print a one-line stderr reminder naming the changed file and the re-install command. The check is read-only and runs only on codex and opencode; Claude Code resolves models at spawn time and is unaffected. (#1688)
Per-phase-type models (models)
If you want to say "Opus for planning, Sonnet for everything else" without learning all 33 agent names, use the models block. It maps six phase types to tier aliases:
{
"model_profile": "balanced",
"models": {
"planning": "opus",
"discuss": "opus",
"research": "sonnet",
"execution": "opus",
"verification": "sonnet",
"completion": "sonnet"
}
}
Phase types and their agents:
| Phase type | Agents covered |
|---|---|
planning |
gsd-planner, gsd-roadmapper, gsd-pattern-mapper |
research |
gsd-phase-researcher, gsd-project-researcher, gsd-research-synthesizer, gsd-codebase-mapper, gsd-ui-researcher |
execution |
gsd-executor, gsd-debugger, gsd-doc-writer |
verification |
gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier, gsd-code-reviewer |
discuss |
gsd-assumptions-analyzer |
completion |
Reserved — no subagent today; accepted by schema for forward compatibility |
The models block accepts tier aliases only (opus, sonnet, haiku, inherit). For a fully-qualified model ID, use model_overrides per agent instead.
Combining models with a per-agent exception:
{
"model_profile": "balanced",
"models": {
"research": "sonnet"
},
"model_overrides": {
"gsd-codebase-mapper": "haiku"
}
}
All five research agents resolve to sonnet except gsd-codebase-mapper, which is pinned to haiku.
Dynamic routing — start cheap, escalate on failure
If you want to pay for cheaper tiers by default and only escalate when an agent fails a quality gate, enable dynamic_routing:
{
"dynamic_routing": {
"enabled": true,
"tier_models": {
"light": "haiku",
"standard": "sonnet",
"heavy": "opus"
},
"escalate_on_failure": true,
"max_escalations": 1
}
}
Each agent has a default tier (light, standard, or heavy). On the first attempt, GSD picks tier_models[default_tier]. If the orchestrator detects a soft failure (verification inconclusive, plan-check flagged, etc.), it re-spawns the agent one tier up. max_escalations caps the total retries.
Agents that already sit at heavy cannot escalate further.
Turning off escalation while keeping dynamic resolution:
{
"dynamic_routing": {
"enabled": true,
"escalate_on_failure": false
}
}
Every attempt uses tier_models[default_tier] regardless of outcome — useful when you want explicit tier-to-model mapping without the escalation behaviour.
dynamic_routing is disabled by default. Omitting the block or setting enabled: false preserves static resolution.
Keep going when a provider throttles you
The tier ladder above escalates within one provider. When the provider itself is the thing
that ran out of quota, a heavier tier on the same account is still throttled. Add
provider_escalation — an ordered list of fallback model IDs — to keep the phase moving
instead of stopping for a manual restart:
{
"dynamic_routing": {
"enabled": true,
"tier_models": { "light": "haiku", "standard": "sonnet", "heavy": "opus" },
"provider_escalation": ["gpt-5", "nvidia/llama-3.3"],
"max_escalations": 2
}
}
When an executor dies on a rate limit, GSD classifies the error body, switches to the next
model in the list, logs the swap (sonnet → gpt-5), and waits out any Retry-After the
provider sent. The walk is capped at min(max_escalations, provider_escalation.length).
Once the list is spent, GSD names every model it tried and hands you the normal recovery
prompt — it never silently retries the exhausted one.
This is most useful on providers without a guaranteed SLA (Nvidia NIM, OpenRouter, and
other third-party OpenCode models), where a throttle mid-phase is routine. It only fires on
quota / rate-limit failures; other failures keep the tier ladder. Leaving
provider_escalation unset preserves the manual wait-for-reset behaviour exactly.
Using GSD on non-Anthropic runtimes
If you installed GSD for Codex, OpenCode, Antigravity CLI, or Kilo, the installer already set resolve_model_ids: "omit" in your config. This tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model. No manual setup is needed for the basic case.
Codex does not do tier routing — pin explicitly instead
Codex agents inherit whatever model your Codex session is using. GSD writes no model line into
~/.codex/agents/<agent>.toml, so setting model_profile has no effect on Codex.
This is deliberate (ADR-2313). A ChatGPT-account Codex
session exposes only its own model, so a pinned tier model fails the request outright —
400 invalid_request_error: "The 'sonnet' model is not supported when using Codex with a ChatGPT account" — and the agent never spawns.
To pin a model on Codex, name a real Codex model id per agent:
{
"runtime": "codex",
"model_overrides": {
"gsd-planner": "gpt-5.6-sol",
"gsd-executor": "gpt-5.6-terra"
}
}
Then re-run the installer, as with any model_overrides edit on Codex (see above).
Two rules apply to what you can put there:
- It must be a real Codex model id. A GSD tier alias (
opus,sonnet,haiku,fable) or aclaude-*id is dropped with a warning rather than written, because Codex rejects them. - Your account must actually expose it. GSD cannot check this — if you pin
gpt-5.6-solon an account that does not have it, you get the same 400. When in doubt, omit the pin and let the session model apply.
model_reasoning_effort follows the model: with no pin, GSD writes no effort line either, so the
Codex UI drives both rather than one following GSD and the other following your session.
Upgrading from v1.10 or earlier? Codex installs used to embed a per-tier model (
opus→gpt-5.6-sol,sonnet→gpt-5.6-terra,haiku→gpt-5.6-luna). If you were on an API-key account where those resolved successfully, add themodel_overridesblock above to keep them. The installer prints a one-time notice when it drops a pin. If you were on a ChatGPT account, this is the change that stops the 400s — nothing to do.
If you want per-agent model IDs on any non-Claude runtime:
{
"resolve_model_ids": "omit",
"model_overrides": {
"gsd-planner": "o3",
"gsd-executor": "o4-mini",
"gsd-debugger": "o3"
}
}
For the full runtime-aware profiles reference and the model_policy surface (provider-neutral presets added in v1.42), see Configuration reference — Model Profiles.
Resolution precedence (highest to lowest)
When multiple layers apply, the resolver picks the highest-priority entry:
1. model_overrides[<agent>] — per-agent; full IDs; targeted exception
2. dynamic_routing.tier_models[<tier>] — when enabled; escalates on soft failure
3. models[<phase_type>] — coarse phase-level tier
4. model_profile (per-agent column) — global tier strategy
5. Runtime default — when nothing else applies
Choosing the right lever
| You want | Use |
|---|---|
| One tier strategy for all agents | model_profile |
| Coarse phase-level tuning ("Opus for planning") | models.<phase_type> |
| Per-agent precision ("force Haiku on the codebase mapper") | model_overrides[<agent>] |
| A fully-qualified model ID for a specific agent | model_overrides[<agent>]: "openai/gpt-5" |
| Start cheap, escalate only on failure | dynamic_routing |
| All agents follow the session model (non-Anthropic provider) | model_profile: "inherit" |