import { DEFAULT_ROSTERS } from "../config/defaults.js"; import type { PrivacyProfile, UltraConfig } from "../types.js"; /** * A resolved roster: the ordered model tiers a profile declares, plus the subset that * may not decide anything that matters. Every model check in UltraPi resolves to one * of these, so opening the roster stays fail-closed — a model the user did not declare * is refused exactly where a non-profile model used to be. */ export interface ModelRoster { profile: PrivacyProfile; tiers: readonly string[]; nonCritical: readonly string[]; } export const MAX_ROSTER_TIERS = 16; function profileLabel(profile: PrivacyProfile): string { return `${profile[0]!.toUpperCase()}${profile.slice(1)} profile`; } export function defaultRoster(profile: PrivacyProfile): ModelRoster { const declared = DEFAULT_ROSTERS[profile]; return { profile, tiers: [...declared.tiers], nonCritical: [...(declared.nonCritical ?? [])] }; } export function rosterFor(config: Pick): ModelRoster { const declared = config.models; return declared ? { profile: config.profile, tiers: declared.tiers, nonCritical: declared.nonCritical ?? [] } : defaultRoster(config.profile); } /** Position in the declared tiers, which is the model's capability rank. -1 when undeclared. */ export function modelRank(roster: ModelRoster, model: string | undefined): number { return model === undefined ? -1 : roster.tiers.indexOf(model); } export function isRosterModel(roster: ModelRoster, model: string): boolean { return modelRank(roster, model) >= 0; } export function mayDecide(roster: ModelRoster, model: string): boolean { return isRosterModel(roster, model) && !roster.nonCritical.includes(model); } export function assertRosterModel(roster: ModelRoster, model: string, critical = false): void { if (!isRosterModel(roster, model)) throw new Error(`${profileLabel(roster.profile)} blocks model ${model}: declare it in models.tiers to allow it`); if (critical && !mayDecide(roster, model)) throw new Error(`${profileLabel(roster.profile)} lists ${model} in models.nonCritical, so it cannot make critical decisions`); } export function assertModelAllowed(config: Pick, model: string, critical = false): void { assertRosterModel(rosterFor(config), model, critical); } /** * Models to try, in order, when `model` stops answering: same-rank-or-stronger peers first * so a rotation never silently weakens the run, then weaker declared tiers as a last resort. * `criticalOnly` drops models the roster bars from deciding. */ export interface RosterRoles { cheap: string; stable: string; strong: string; deciding: string } /** * The four positions UltraPi assigns work to, derived from a declared roster: the cheapest * model, the everyday decision model, the strongest, and the cheapest one allowed to decide. * Every role a config names is one of these, so an installer can fit any tier list — one * model or eight — without the caller knowing which provider it came from. */ export function rolesForRoster(tiers: readonly string[], nonCritical: readonly string[] = []): RosterRoles { const cheap = tiers[0]!; const deciders = tiers.filter((tier) => !nonCritical.includes(tier)); const deciding = deciders[0] ?? cheap; const strong = deciders.at(-1) ?? tiers.at(-1)!; const preferred = tiers[Math.min(1, tiers.length - 1)]!; const stable = nonCritical.includes(preferred) ? deciders.find((tier) => tiers.indexOf(tier) >= tiers.indexOf(preferred)) ?? deciding : preferred; return { cheap, stable, strong, deciding }; } export function rotationCandidates(roster: ModelRoster, model: string, criticalOnly = false): readonly string[] { const rank = modelRank(roster, model); const eligible = roster.tiers.filter((candidate) => candidate !== model && (!criticalOnly || mayDecide(roster, candidate))); const stronger = eligible.filter((candidate) => modelRank(roster, candidate) > rank); const weaker = eligible.filter((candidate) => modelRank(roster, candidate) < rank).reverse(); return [...stronger, ...weaker]; }