/** * The models that may draft a listing, and what each one costs. * * One catalogue rather than two lists. The Admin settings dropdown needs the * models, `costMicros` needs their rates, and the price shown beside a model in * Admin has to be the price it is actually billed at — which it cannot be if * the list and the rates are maintained separately. * * Rates are dollars per million tokens, confirmed against the pricing page on * 2026-08-31 rather than recalled. Worth checking again when a model is added: * an increase to $3/$15 had been scheduled for 2026-09-01 and was cancelled, * with Sonnet's $2/$10 made permanent. */ export interface DraftingModel { id: string; /** Shown in the Admin dropdown. */ label: string; /** Dollars per million input tokens. */ inputRate: number; /** Dollars per million output tokens. */ outputRate: number; } export const DRAFTING_MODELS: readonly DraftingModel[] = [ { id: 'claude-sonnet-5', label: 'Claude Sonnet 5', inputRate: 2, outputRate: 10 }, { id: 'claude-opus-5', label: 'Claude Opus 5', inputRate: 5, outputRate: 25 }, { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', inputRate: 1, outputRate: 5 } ]; /** * Sonnet, not Opus. The task is writing a description from a photograph rather * than reasoning, and this runs once per submission on a route a stranger with * a link can trigger. Opus costs two and a half times as much per item. */ export const DEFAULT_DRAFTING_MODEL = 'claude-sonnet-5'; export function isDraftingModel(id: string): boolean { return DRAFTING_MODELS.some((model) => model.id === id); } /** * Deliberately not zero. An unrecognised model pricing at nothing would make a * budget read as unspent however much was really spent, which is the one * failure a spend guard must not have. Set to the most expensive rate here, so * an unknown model errs towards over- rather than under-reporting. */ const FALLBACK_RATE = { inputRate: 5, outputRate: 25 }; /** * Whole micros, so a cost never carries a floating-point fraction into the * database. Rates are per million tokens and a micro is a millionth of a * dollar, so the two cancel and the arithmetic is just tokens times rate. */ export function costMicros(model: string, inputTokens: number, outputTokens: number): number { const rate = DRAFTING_MODELS.find((m) => m.id === model) ?? FALLBACK_RATE; return Math.round(inputTokens * rate.inputRate + outputTokens * rate.outputRate); }