import { readFile } from "node:fs/promises"; export interface LiveWorkerModel { readonly provider: string; readonly id: string; readonly name: string; readonly cost: number; readonly contextWindow: number; readonly maxTokens: number; readonly reasoning: boolean; readonly thinkingLevel?: string; } interface ModelSelectionInput { readonly difficulty: "easy" | "medium" | "hard" | undefined; readonly available: readonly LiveWorkerModel[]; readonly scoped?: readonly LiveWorkerModel[]; readonly current: LiveWorkerModel; } export interface WorkerModelSelection { readonly model: LiveWorkerModel; readonly reason: "difficulty-tier" | "current-model-fallback"; } export function selectWorkerModel(input: ModelSelectionInput): WorkerModelSelection { const candidates = input.scoped && input.scoped.length > 0 ? input.scoped : input.available; if (!input.difficulty || candidates.length === 0) { return { model: input.current, reason: "current-model-fallback" }; } const ordered = [...candidates].sort( input.difficulty === "easy" ? compareEconomyTier : compareCapabilityTier, ); const index = input.difficulty === "hard" ? ordered.length - 1 : input.difficulty === "medium" ? Math.floor((ordered.length - 1) / 2) : 0; return { model: ordered[index]!, reason: "difficulty-tier" }; } export function floorAuditorModel( selected: LiveWorkerModel, implementer: LiveWorkerModel, ): LiveWorkerModel { return compareCapabilityTier(selected, implementer) < 0 ? implementer : selected; } interface ModelGuidanceInput { readonly available: readonly LiveWorkerModel[]; readonly current: LiveWorkerModel; readonly globalNotesPath: string; readonly projectNotesPath: string; readonly projectTrusted: boolean; } export interface WorkerModelGuidance { readonly text: string; readonly notices: readonly string[]; } export async function buildWorkerModelGuidance( input: ModelGuidanceInput, ): Promise { const malformed: string[] = []; const globalRules = await readNoteRules(input.globalNotesPath, malformed); const projectRules = input.projectTrusted ? await readNoteRules(input.projectNotesPath, malformed) : []; const rows = input.available.map((model) => { const key = modelKey(model); const globalNote = lastMatchingNote(globalRules, key); const projectNote = lastMatchingNote(projectRules, key); const note = projectNote ?? globalNote; return `- ${key}: ${model.name}; context=${model.contextWindow}; max_output=${model.maxTokens}; reasoning=${model.reasoning ? "yes" : "no"}; cost=${model.cost}${note ? `; note=${note}` : ""}`; }); const currentKey = modelKey(input.current); return { text: [ "Live worker model catalog:", ...rows, `Current model fallback: ${currentKey}`, "Choose easy, medium, or hard mechanically; role labels do not select a model family.", ].join("\n"), notices: malformed.length > 0 ? [`Ignored ${malformed.length} malformed model note line(s).`] : [], }; } interface NoteRule { readonly pattern: string; readonly note: string; } async function readNoteRules(path: string, malformed: string[]): Promise { let content: string; try { content = await readFile(path, "utf8"); } catch (error) { if (isNotFound(error)) return []; throw error; } const rules: NoteRule[] = []; for (const rawLine of content.split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith("#")) continue; const separator = line.indexOf("->"); if (separator <= 0 || separator === line.length - 2) { malformed.push(line); continue; } const pattern = line.slice(0, separator).trim(); const note = line.slice(separator + 2).trim(); if (!pattern || !note || /\s/u.test(pattern)) { malformed.push(line); continue; } rules.push({ pattern, note }); } return rules; } function lastMatchingNote(rules: readonly NoteRule[], key: string): string | undefined { let found: string | undefined; for (const rule of rules) { if (matchesPattern(rule.pattern, key)) found = rule.note; } return found; } function matchesPattern(pattern: string, key: string): boolean { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replaceAll("*", ".*"); return new RegExp(`^${escaped}$`, "u").test(key); } function modelKey(model: Pick): string { return `${model.provider}/${model.id}`; } function compareCapabilityTier(left: LiveWorkerModel, right: LiveWorkerModel): number { return ( Number(left.reasoning) - Number(right.reasoning) || left.contextWindow - right.contextWindow || left.maxTokens - right.maxTokens || left.cost - right.cost || modelKey(left).localeCompare(modelKey(right)) ); } function compareEconomyTier(left: LiveWorkerModel, right: LiveWorkerModel): number { return left.cost - right.cost || compareCapabilityTier(left, right); } function isNotFound(error: unknown): boolean { return ( typeof error === "object" && error !== null && "code" in error && (error as { readonly code?: unknown }).code === "ENOENT" ); }