# Model Fallback Contract

> Contract last revised in **v10.6.0** (Fable 5 restored as top tier). The version tag here tracks the last substantive change to this contract, not the pipeline release.

Personas route to the top available intelligence tier they declare in
`preferredModel`. That tier can be quota-limited or temporarily unavailable.
This contract defines when and how the orchestrator falls back, deterministically,
without ever editing persona files at runtime.

> **Fable 5 available again (2026-07).** `claude-fable-5` is the top tier once
> more. Architect (`ios/android/backend-architect`), Reviewer-1 (`code-reviewer`),
> and triage personas declare `preferredModel: fable` - the deepest-reasoning
> roles. **If Fable 5 is unavailable, the first fallback is opus (`claude-opus-5`).**
> Security and other `preferredModel: opus` personas keep opus as their top tier.
> The `premiumTierUntil` date gate stays in the contract as a generic mechanism
> for any plan-window-limited premium tier.

## Tier ladder

```
fable -> opus -> sonnet -> haiku
```

The orchestrator walks **one step down per trigger** from whichever tier a
persona prefers: a `fable` persona degrades `fable -> opus` first, then
`opus -> sonnet`, then `sonnet -> haiku`; an `opus` persona starts at
`opus -> sonnet`. So a Fable 5 outage transparently promotes Opus 5 into the
architect/reviewer roles with no file edits.

### Rung names are the contract; model IDs are not

Personas and dispatch read the **rung** (`fable`, `opus`, `sonnet`, `haiku`). The wire
model ID each rung resolves to lives in exactly two places: `scripts/cost-table.json`
(`prices.<rung>.modelId`) and the per-host reviewer table in
`phases/phase-4-review.md`. A generation move edits those; it never renames a rung and
never touches a persona file.

Current resolution: `fable` -> `claude-fable-5`, `opus` -> `claude-opus-5`,
`sonnet` -> `claude-sonnet-5`, `haiku` -> `claude-haiku-4-5`.

**Rate limits do not follow the rung.** Claude Opus 5 draws on a pool separate from
the combined Opus 4.x pool, so a generation move does not inherit the previous
generation's headroom. Budget-ceiling downgrades below are unaffected - they read the
cost ledger, not the provider's quota - but a 429 on the opus rung after a generation
move is a limits question, not a fallback-contract question.

### Behavioural re-tuning the generation move implies

The pipeline's reviewer and dev prompts were tuned against the previous generation.
Three shifts on the current opus rung are worth knowing when reading a run that looks
different rather than broken, and none of them are bugs in this contract:

- **Longer user-facing output.** Effort is not the lever; prompt-level conciseness is.
  Phase 7 report length and reviewer prose are where this shows.
- **Self-verification without being asked.** Explicit "double-check your work"
  scaffolding now causes over-verification rather than preventing under-verification.
  Phase 4's deterministic gates already carry that load.
- **Readier subagent delegation.** The previous generation under-reached and needed
  encouragement; this one does not. Phase 1's parallel scan and Phase 4's reviewer
  panel are already bounded by explicit counts, which is the right shape - keep them
  bounded rather than adding "delegate more" guidance.

One step down per trigger, walking the ladder until a tier dispatches or the
floor (`haiku`) is reached. `haiku` is the last-resort floor: a run that reaches
it is heavily degraded but still makes progress instead of hard-halting because
two tiers happened to be unavailable at once. A dispatch error at the floor is a
genuine outage  -  it falls through to the normal phase-error path (pause ->
resume), since there is nothing left to step down to. The mechanism is the existing per-dispatch override:
set `PHASE_MODEL_OVERRIDE=<fallback>` before dispatch (the orchestrator exports
`CLAUDE_CODE_SUBAGENT_MODEL` on Claude Code, or passes `--model` on Copilot
CLI). Persona frontmatter stays the source of truth for the *preferred* model;
the override is per-dispatch and leaves files untouched.

## Prefs knob

`prefs.global.modelFallback` (template default below; absent knob = `enabled: true`
with no date gate; an absent `floorModel` defaults to `haiku`, so installs that
predate this field still get the second downgrade step):

```json
"modelFallback": {
  "enabled": true,
  "premiumTierUntil": null,
  "fallbackModel": "sonnet",
  "floorModel": "haiku",
  "onDispatchError": true
}
```

| Field | Meaning |
|---|---|
| `enabled` | Master switch. `false` = always dispatch the persona's `preferredModel`, fail loudly on error. |
| `premiumTierUntil` | ISO date (`YYYY-MM-DD`) or `null`. When set and today is **after** this date, every `preferredModel` dispatch is downgraded to `fallbackModel` unless the user re-confirms (see Date gate). Use when the top tier is included in a plan only until a known date. |
| `fallbackModel` | Target of the first downgrade step. Default `sonnet` (the next tier below opus). |
| `floorModel` | Last-resort tier when `fallbackModel` also fails to dispatch. Default `haiku`. Set to the same value as `fallbackModel` (or `null`) to disable the second step and halt after one downgrade. |
| `onDispatchError` | When `true`, a failed top-tier dispatch (model unavailable / quota / 4xx on model id) retries once on `fallbackModel`, and a failed `fallbackModel` dispatch retries once on `floorModel`, instead of aborting the phase. |

## Triggers (checked in this order)

1. **Date gate (Phase 0 Step 0, once per run).** If `premiumTierUntil` is set and
   in the past, print one line:
   `WARN: premium tier plan window ended <date>; preferredModel personas will dispatch on <fallbackModel>. Set prefs.global.modelFallback.premiumTierUntil to null to keep the top tier on usage credits.`
   Then export the override for every `preferredModel` persona this run. Interactive modes
   may instead ask once (`Keep top tier (usage credits) / Fall back to <model>`);
   autopilot never asks - it falls back and logs.
2. **Dispatch error (per dispatch).** On a model-unavailable / quota error from a
   top-tier dispatch and `onDispatchError: true`: retry the same persona once with
   `PHASE_MODEL_OVERRIDE=<fallbackModel>`. If that retry also fails with a dispatch
   error and `floorModel` is set and differs from `fallbackModel`, retry once more
   with `PHASE_MODEL_OVERRIDE=<floorModel>`. A failure at the floor (or when no
   floor is configured) falls through to the normal phase-error path (pause ->
   resume). Never silent-skip the persona. Each downgrade emits its own
   `model_fallback` metric line so a two-step degrade is visible in Phase 7.
3. **Cost budget ceiling (existing gate).** When `cost-budget-check.mjs` exits 11
   (exceeded) mid-run, the run already pauses per the cost-budget contract; on
   user-approved continue, `preferredModel` personas downgrade to `fallbackModel` for the
   remainder of the run.

## Logging

Every fallback emits one `log-metric.sh` line (`metric: model_fallback`,
attributes: `persona`, `from`, `to`, `trigger: date-gate|dispatch-error|budget`)
and one agent-log line so Phase 7 reports show which phases ran degraded.
The cost ledger prices the dispatch at the model actually used (the tracker's
per-phase `model` field already carries the override).

## Non-goals

- No automatic re-upgrade mid-run (a run that fell back stays fallen back -
  deterministic over clever).
- No edits to `pipeline/agents/*.md` at runtime; frontmatter is install-time
  configuration only.
- Copilot CLI reviewer set is out of scope: Copilot CLI pins its own three
  reviewer models (GPT-5.4 + Opus + Sonnet - Fable 5 is not offered there) and
  does not use this persona ladder. Only Claude Code dispatches Reviewer-1 on
  Fable.

## Codex CLI

Codex offers no Anthropic models, so the tier names map onto OpenAI models plus a
reasoning effort - effort carries the depth distinction that the model id carries
on Claude Code. The map is applied at install time by
`install/_codex-agents.mjs`, which writes `model` + `model_reasoning_effort` into
each `~/.codex/agents/<persona>.toml`:

| Tier | Codex model | Effort |
|---|---|---|
| `fable` | `gpt-5.6` | `xhigh` |
| `opus` | `gpt-5.6` | `high` |
| `sonnet` | `gpt-5.4` | `medium` |
| `haiku` | `gpt-5.6-terra` | `low` |

Ladder on Codex: `gpt-5.6 @ xhigh -> gpt-5.6 @ high -> gpt-5.4 -> gpt-5.6-terra`.
The first step down lowers effort rather than switching model, which is the
cheapest useful degradation when the top tier is rate-limited rather than
unavailable.

**Per-dispatch override on Codex** goes through `spawn_agent`, and it MUST pass
`fork_turns: "none"` (or a positive integer). A full-history fork inherits the
parent model and reasoning effort and silently discards the override, so a
fallback that omits it appears to apply while changing nothing.

The three model ids above live in exactly three places - this table,
`pipeline/scripts/cost-table.json`, and the Phase 4 reviewer matrix - so an OpenAI
rename is a three-file change.
