Skip to content

feat(executors): Fable/Claude-5 model family, ultracode effort level, free-text model entry + effort for preset models#29

Merged
miguelrisero merged 6 commits into
mainfrom
feat/claude5-models-effort
Jul 8, 2026
Merged

feat(executors): Fable/Claude-5 model family, ultracode effort level, free-text model entry + effort for preset models#29
miguelrisero merged 6 commits into
mainfrom
feat/claude5-models-effort

Conversation

@miguelrisero

@miguelrisero miguelrisero commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Problem

The Claude executor picker was missing the Fable / Claude-5 family, had no way to enter a custom model id, and could not attach an effort level to preset-injected models (e.g. Miguel's claude-fable-5 default surfaced with empty reasoning options). Two real bugs compounded this: (1) model_supports_effort() excluded fable, so CLI-mode launches of claude-fable-5 silently dropped --effort; (2) there was no ultracode effort level, and any UI-only effort id not in the ClaudeEffort enum is silently discarded by apply_overrides().

What changed

Backend (crates/executors):

  • Added fable (alias, "Fable") to the top of the model list in default_discovered_options(), newest-first: Fable, Opus, Opus (1M context), Sonnet, Haiku.
  • Fixed model_supports_effort() to include fable, so effort is no longer dropped for Fable models at interactive CLI launch.
  • Added a real Ultracode variant to ClaudeEffort (serde/strum lowercase → ultracode on the wire), appended to the discovered effort options, with rank 6 + label "Ultracode" in model_selector.rs.
  • Interactive CLI path (installed 2.1.204, which accepts the ultracode alias) passes --effort ultracode verbatim. Headless path maps Ultracode → xhigh via a new ClaudeEffort::headless_arg() because the pinned headless CLI (@2.1.154) hard-rejects ultracode and its dynamic-workflow orchestration is interactive-only.
  • Regenerated shared/types.ts and shared/schemas/claude_code.json (flat string enum preserved — the variant note is a plain // comment so the schema shape is unchanged for the settings agent-config editor).

Frontend:

  • ModelSelectorPopover / ModelList: free-text model entry — when the trimmed search query matches no model, a Use "<query>" row is offered that selects the arbitrary id. The flat (no-provider) picker now always shows the filter/entry box.
  • appendPresetModel(): gated reasoning-options fallback — an injected/custom effort-capable Claude id (opus/sonnet/fable, provider-less) inherits the effort options from the first config model that has them, so Fable presets and custom ids can pick an effort (including ultracode). Provider-scoped models (codex path) are unaffected.
  • Added modelSelector.useCustom i18n key across all locales.

How verified

  • cargo test -p executors — 84 passed. New tests: fable+xhigh red/green for the model_supports_effort fix, ultracode FromStr/AsRef round-trip, Ultracode.headless_arg() == "xhigh", interactive passthrough of ultracode.
  • Discriminating CLI test run: npx -y @anthropic-ai/claude-code@2.1.154 --effort ultracode --versionerror: option '--effort <level>' argument 'ultracode' is invalid. It must be one of: low, medium, high, xhigh, max — confirms the headless→xhigh mapping is mandatory.
  • pnpm run generate-typesultracode present in shared/types.ts and shared/schemas/claude_code.json; schema stays a flat enum (one-line diff).
  • New Vitest spec modelSelector.test.ts (5 passed) covering the gated reasoning-options fallback.
  • pnpm run local-web:check, web-core:check, ui:check pass; ui:lint, local-web:lint, and check-unused-i18n-keys.mjs clean; cargo clippy -p executors clean; pnpm run format run.
  • NOT verified: end-to-end live launch (no /opt access, no paid model calls); fable[1m] validity (see Risks); actual workflow-orchestration behavior of ultracode in a live session.

Risks / notes

  • fable[1m] was intentionally left out: only opus/sonnet [1m] handling is evidenced in the 2.1.204 binary; not adding an unverified entry.
  • default_model / CLI_DEFAULT_MODEL were left as opus per the brief — flipping the repo fallback to fable is a separate conscious call (Miguel's profile already defaults to fable). Possible follow-up.
  • Headless CLI pin (@2.1.154) was NOT bumped (blast radius: loop automation + title_gen). Full model ids like claude-fable-5 pass through on 2.1.154 regardless; only the bare fable alias needs ≥2.1.204, which affects headless spawns only.
  • ClaudeEffort is serialized in profiles.json / stored ExecutorActions — this only ADDS a variant (backward compatible); nothing renamed or removed.
  • Free-text entry lets typos through; a bad id fails only at claude startup inside tmux (visible but easy to miss). normalize_claude_model_id only fixes dotted claude-* ids.

ship-it battery

Automated multi-pass review battery run on this PR; all local checks green before each push. Follow-up commits: 3193fd57, 44650e12, c90b75ca, 987df99a, 14a6046f.

Council round 1 (mixed: Codex ×4 personas + Claude cross-check) — all CAUTION, no P0/BLOCK. Applied: corrected the headless_arg doc comment (2.1.154 hard-rejects unknown --effort, not warn-and-ignore); made model_supports_effort case-insensitive to faithfully mirror the TS modelSupportsEffort (closes a mixed-case silent --effort drop); added headless_arg_matches_as_ref_except_ultracode (pins headless == interactive wire value for every tier except the deliberate ultracode→xhigh delta) and effort_wire_strings_are_stable (golden test enforcing the locked ADD-only never-rename policy at CI). Rejected: tolerant #[serde(other)] deserialization (would reintroduce the silent-drop this PR fixes; downgrade isn't a product requirement) and coupling the generic model_selector ranker to the Claude enum (inverts dependency direction).

/simplify (4 cleanup agents) — applied: derive the picker effort list from ClaudeEffort::VARIANTS (strum, verified to honor serialize_all) instead of a hand-maintained array, so a future tier can't be silently missing; drop the redundant reasoning_label("ultracode") arm (Title-case fallback already yields "Ultracode"). Rejected: collapsing headless_arg to _ => self.as_ref() (keeps compile-time exhaustiveness at the version-skew boundary) and extracting a shared row component (pre-existing triplication, out of diff scope).

Codex + /code-review xhigh — Codex: injected the current override model (not just the profile preset) so a free-text custom effort-capable Claude id gets an effort selector, and gated the appendPresetModel effort fallback behind an explicit flag set only for CLAUDE_CODE. xhigh caught the key defect: the free-text custom-entry row + always-on search box had leaked to every flat (no-provider) executor (Codex, Gemini, Copilot, Droid, Qwen, Cursor), not just Claude — a footgun (arbitrary/typo ids for executors whose downstream wasn't verified; Codex customs would lose reasoning options). Fixed by gating both behind a new allowCustomModel prop set only for Claude; non-Claude flat pickers keep exact prior behavior (MODEL_LIST_PAGE_SIZE restored). Skipped (minor, noted): a custom Claude id isn't written to the recently-used LRU (it still persists as the active selection).

Council round 2 (mixed, updated diff) — 4 APPROVE + 1 CAUTION, no P0/P1/BLOCK. Applied: gated the override-model injection on isClaude too, so non-Claude executors get exactly the pre-PR preset-only injection. Rejected: wiring the new Vitest spec into CI (pre-existing repo-wide condition — vitest is intentionally not a devDep and tsc excludes *.test.ts; ran locally, 7 pass).

CodeRabbit — CLI could not authenticate in this non-interactive environment (environment_unsupported: no API key, no localhost-callback/stdin fallback) on both --plain and --agent. Deterministic env limitation, not a transient rate-limit, so per the fallback a Codex xhigh review was substituted on the final diff → no actionable findings.

Verification: cargo test -p executors (all pass, incl. new tests), cargo clippy -p executors clean, pnpm run check (frontend tsc all green), npx vitest run modelSelector.test.ts (7 pass), pnpm run format applied. No new migrations; generated shared/types.ts + shared/schemas/claude_code.json regenerated (not hand-edited). Locked decisions honored: no default_model flip, no headless pin bump, ADD-only enum.

… free-text model entry + effort for preset models
…ke model_supports_effort case-insensitive

Council round 1 hardening for the Claude effort changes:
- Fix headless_arg doc comment (2.1.154 hard-rejects unknown --effort values,
  not warn-and-ignore) to match verified CLI behavior and the match-arm comment.
- Add headless_arg_matches_as_ref_except_ultracode: pins that headless mode
  emits the same wire value as the interactive path for every tier except the
  deliberate ultracode->xhigh delta, so a future strum serialization change
  can't silently diverge the two paths.
- Add effort_wire_strings_are_stable golden test enforcing the locked ADD-only
  never-rename policy for ClaudeEffort (serialized into profiles.json + stored
  ExecutorActions history) at CI time instead of by comment alone.
- Make model_supports_effort case-insensitive so a free-text/custom id like
  Claude-Fable-5 is gated identically in the UI (TS mirror lowercases) and at
  interactive launch, closing a silent --effort drop; add reciprocal sync
  comment and a boundary test (haiku stays excluded).
…code

/simplify pass on the effort/model changes (behavior-preserving):
- Derive the picker's effort_options from ClaudeEffort::VARIANTS (strum
  VariantNames) instead of a hand-maintained string array, so a future effort
  tier can't be silently missing from the list; pin the derived wire strings in
  a test.
- Drop the redundant reasoning_label "ultracode" arm: the Title-case fallback
  already yields "Ultracode" (like low/medium/high/max); only xhigh needs an
  override.
- ModelSelectorPopover: remove the vestigial showSearch variable (always true
  since MODEL_LIST_PAGE_SIZE was removed) and render the search box directly;
  document the deliberate flat-picker-only scope of the custom-model entry.

Kept the explicit exhaustive headless_arg match rather than collapsing to
`_ => self.as_ref()`: the compile-time exhaustiveness forces every future
variant to make a headless decision at the version-skew boundary (a wildcard
would let a future ultracode-like alias silently reach the pinned headless CLI).
… to Claude

Addresses a Codex xhigh review of the picker changes:
- appendPresetModel now also injects the current override model (which may be a
  free-text custom id the user typed), not just the profile preset, so a custom
  effort-capable Claude id shows an effort selector instead of silently having
  none (and headless launches then carry the chosen --effort).
- Gate the reasoning-options fallback behind an explicit enableEffortFallback
  flag, set only for CLAUDE_CODE. Keeps the shared appendPresetModel helper
  executor-agnostic: previously any provider-less executor whose id happened to
  contain opus/sonnet/fable would inherit effort options it does not support.
- Extend the vitest spec: custom-id inheritance + fallback-disabled cases.
code-review (xhigh) caught that the free-text "Use <query>" entry and the
always-visible search box leaked to every flat (no-provider) executor — Codex,
Gemini, Copilot, Droid, Qwen, Cursor are all flat pickers, not just Claude.
That let a user type an arbitrary model id for executors whose downstream does
not tolerate one (only Claude's was verified), and a bare typed id has no
provider so it produced a malformed provider-less override; Codex custom models
would also silently lose their reasoning options.

- Add an explicit `allowCustomModel` prop to ModelSelectorPopover (default
  false); the container passes it only for CLAUDE_CODE.
- Custom-entry row and the forced search box are now gated on it; other flat
  pickers keep their exact prior behavior (search only once the list exceeds
  MODEL_LIST_PAGE_SIZE, which is restored — the earlier /simplify collapse of
  showSearch assumed a single flat consumer, which was wrong).
- Fold the effort-fallback + custom-entry gating into one `isClaude` flag.
Council round 2: the override-model injection was unconditional (only the
effort-fallback flag was gated on isClaude), so a non-Claude executor with a
stale override id would still get it injected as a provider-less flat model.
Benign (non-Claude pickers can't create free-text ids, so no malformed launch),
but it widened behavior past the Claude-only intent. Gate the override
injection on isClaude too; non-Claude executors now get exactly the pre-PR
preset-only injection.
@miguelrisero
miguelrisero merged commit f1a448f into main Jul 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant