/** * Anthropic API model id → the Officer id that most nearly names it. * * Two id spaces meet here. A transcript records what the API served (`claude-opus-5`); Officer's picker * offers what the CLI accepts (`claude-code/opus`, `claude-code/opus[1m]`). This translates one to the * other, and it is **deliberately lossy in one direction only** — that is the whole reason * `chat_session_models` exists: * * - `opus` and `opus[1m]` both serve `claude-opus-5`, so the 1M variant cannot be recovered. * - `opusplan` serves whichever of opus/sonnet answered that turn, so it cannot be recognised at all. * - `mythos` has no Officer entry (Project Glasswing only), so it resolves to null rather than to a * neighbouring family — offering a model this machine may not reach is worse than offering none. * * So this is the FALLBACK for a session that has not taken a turn since the table landed, and the seed for * the backfill. A real turn overwrites its guess with the exact alias the user picked. Never use it to * decide what to run — only to decide what to display. * * Matching is by FAMILY PREFIX rather than an exhaustive id list, because the API ids are not uniform: * `claude-haiku-4-5-20251001` carries a date suffix while `claude-fable-5` does not, and new versions ship * without warning. A prefix match keeps a model released tomorrow resolving to the right family instead of * to null. */ /** Claude Code's own marker for an entry it generated itself. Not a model, and must never be stored. */ export const SYNTHETIC_MODEL = ''; const FAMILIES: { prefix: string; officerId: string }[] = [ { prefix: 'claude-fable', officerId: 'claude-code/fable' }, { prefix: 'claude-opus', officerId: 'claude-code/opus' }, { prefix: 'claude-sonnet', officerId: 'claude-code/sonnet' }, { prefix: 'claude-haiku', officerId: 'claude-code/haiku' }, ]; /** * The Officer model id for an API model id, or null when there is no honest answer. * * Null for ``, for mythos, for an OpenCode model, and for anything unrecognised. Callers treat * null as "we do not know", which renders as no model rather than as a guess. */ export function officerModelForApiModel(apiModel: string | null | undefined): string | null { if (!apiModel || apiModel === SYNTHETIC_MODEL) return null; const id = apiModel.trim().toLowerCase(); if (!id) return null; const family = FAMILIES.find((f) => id.startsWith(f.prefix)); return family?.officerId ?? null; } /** Whether a transcript entry's model is a real model rather than Claude Code's synthetic marker. */ export const isRealModel = (apiModel: string | null | undefined): boolean => !!apiModel && apiModel !== SYNTHETIC_MODEL;