// Canonical Anthropic model IDs — single source of truth. // Templates, docs, and skills reference these values as strings; // all TypeScript consumers import from here. // Opus defaults to the 1M-context variant ([1m] is Claude Code's // context-extension suffix, valid in settings.json "model" and agent // frontmatter). export const OPUS_MODEL = "claude-opus-5[1m]"; export const SONNET_MODEL = "claude-sonnet-5"; export const HAIKU_MODEL = "claude-haiku-4-5"; // Each value is the model's published max input tokens (its context window), // taken from Anthropic's model documentation on 2026-07-25: Opus 5 and // Sonnet 5 are 1M, Haiku 4.5 is 200k. Not inferred from observed usage. When // adding a model, read its window from the Models API // (GET /v1/models/{id} -> max_input_tokens) rather than assuming a family default. export const MODEL_CONTEXT_WINDOW: Record = { [OPUS_MODEL]: 1_000_000, [SONNET_MODEL]: 1_000_000, [HAIKU_MODEL]: 200_000, }; /** Look up context window size; defaults to 200k for unknown models. */ export function contextWindow(model: string): number { return MODEL_CONTEXT_WINDOW[model] ?? 200_000; } /** Friendly labels for the canonical model ids, for the /chat composer's * native-CC model dropdown (Task 820). A lever id outside this map renders * with its raw id as the label (the composer's append-fallback). */ export const MODEL_DISPLAY_NAMES: Record = { [OPUS_MODEL]: "Opus 5 (1M context)", [SONNET_MODEL]: "Sonnet 5", [HAIKU_MODEL]: "Haiku 4.5", }; /** The dropdown label for a model id: friendly name when canonical, else the * raw id (never misrepresents an off-map persisted lever). */ export function modelDisplayName(id: string): string { return MODEL_DISPLAY_NAMES[id] ?? id; } /** The exact selectable model ids (no aliases). Server-side re-seat validation * (Task 876) uses this so a body-supplied model can never reach the spawn argv * unchecked — a removed model (e.g. claude-fable-5) or a bare alias ('opus') * is rejected before the fork. */ export const SELECTABLE_MODELS: readonly string[] = [OPUS_MODEL, SONNET_MODEL, HAIKU_MODEL]; export function isSelectableModel(id: string): boolean { return SELECTABLE_MODELS.includes(id); } /** Strip Claude Code's `[1m]` context-extension suffix so a selectable id * (`claude-opus-5[1m]`) compares equal to the live API/JSONL `message.model` * (suffix stripped upstream → `claude-opus-5`). Ids without the suffix pass * through. The /chat picker uses this to decide whether a chosen model is a * real change from the running one — a raw `selected !== live` compare is * always true for `[1m]` models. */ export function baseModelId(id: string): string { return id.replace(/\[1m\]$/, ""); } /** Map a model id (often the bare live id without `[1m]`) to its canonical * *selectable* id by base-id match; returns the input unchanged when no * selectable id shares its base. Lets the composer read-only row and the * context-window lookup resolve a live `claude-opus-5` back to * `claude-opus-5[1m]` (display name + 1M window). */ export function canonicalModelId(id: string): string { return SELECTABLE_MODELS.find((m) => baseModelId(m) === baseModelId(id)) ?? id; } // ── Protocol-level Claude Code lever enums ─────────────────────────────────── // Two value sets tied to the Claude Code CLI: the composer-writable permission // modes and the settings.json effort levels. Single source of truth, imported // by the webchat route, the session manager (rc-daemon + fork), the re-seat // route, and the two /chat lever components. Friendly labels stay UI-local; // only the value sets and their validators live here. `dontAsk` is excluded from // both (no surface offers it); `max` is excluded from the settings.json effort // subset because it is invalid as a settings.json `effortLevel` — the new-session // picker still offers it, routed via the `--effort max` argv flag (see // NEW_SESSION_EFFORT_LEVELS below). /** The 5 permission modes the /chat composer and re-seat fork may write. */ export const PERMISSION_MODES = ["default", "acceptEdits", "plan", "auto", "bypassPermissions"] as const; export type PermissionMode = (typeof PERMISSION_MODES)[number]; export function isPermissionMode(value: string): value is PermissionMode { return (PERMISSION_MODES as readonly string[]).includes(value); } /** The 4 settings.json effort levels, low→highest. */ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh"] as const; export type EffortLevel = (typeof EFFORT_LEVELS)[number]; export function isEffortLevel(value: string): value is EffortLevel { return (EFFORT_LEVELS as readonly string[]).includes(value); } // ── Wider value sets that extend the two settings.json subsets ─────────────── // The spawner and the durable account record carry value sets that are supersets // of the composer-writable ones above: the spawn modes add `dontAsk` (the // fail-closed public-spawn mode, never offered by a surface), and the persisted // effort field adds the session-only `auto`/`max`. Both are derived from the // canonical tuples so a renamed mode or new effort level changes one place only. /** The 6 spawn-time permission modes: the 5 composer-writable modes plus the * fail-closed `dontAsk`. The pty-spawner re-exports this under its own names. */ export const SPAWN_PERMISSION_MODES = [...PERMISSION_MODES, "dontAsk"] as const; export type SpawnPermissionMode = (typeof SPAWN_PERMISSION_MODES)[number]; /** The full effort set persisted in account.json: the 4 settings.json levels * plus the session-only `auto`/`max` excluded from that subset. */ export const SESSION_EFFORT_LEVELS = ["auto", ...EFFORT_LEVELS, "max"] as const; export type SessionEffortLevel = (typeof SESSION_EFFORT_LEVELS)[number]; /** The 5 effort levels the /chat new-session picker may write: the 4 settings.json * levels plus the session-only `max`. Distinct from EFFORT_LEVELS (the fork/daemon * settings.json subset — `max` is invalid as a settings.json `effortLevel`) and * from SESSION_EFFORT_LEVELS (which also carries `auto`, never offered by the * picker). A `max` pick reaches the CLI via the `--effort max` argv flag, not the * inline `effortLevel` settings key. */ export const NEW_SESSION_EFFORT_LEVELS = [...EFFORT_LEVELS, "max"] as const; export type NewSessionEffortLevel = (typeof NEW_SESSION_EFFORT_LEVELS)[number]; export function isNewSessionEffortLevel(value: string): value is NewSessionEffortLevel { return (NEW_SESSION_EFFORT_LEVELS as readonly string[]).includes(value); }