export type ModelName = 'haiku' | 'sonnet' | 'opus' export type Effort = 'low' | 'medium' | 'high' export type Tone = 'neutral' | 'praise' | 'scold' export type Route = { difficulty: number; model: ModelName; effort: Effort; tone: Tone; ack: string; work: boolean } export const LADDER: readonly ModelName[] = ['haiku', 'sonnet', 'opus'] export const FALLBACK_ROUTE: Route = { difficulty: 5, model: 'sonnet', effort: 'medium', tone: 'neutral', ack: '', work: false } export const CLASSIFY_PROMPT = '你是路由器。讀下面使用者輸入,只回傳一行 JSON:' + '{"difficulty":1-10,"model":"haiku|sonnet|opus","effort":"low|medium|high","tone":"neutral|praise|scold","ack":"角色以口吻立刻回應的一句話","work":true|false}。work 為 true 表示需要動手做事(讀寫檔、執行指令、搜尋、改程式);純閒聊或一句話可答的問題為 false。' + 'difficulty 低用 haiku,中用 sonnet,高或需深入推理用 opus;tone 是使用者對角色的態度(誇獎/感謝為 praise,責罵為 scold)。\n\n使用者輸入:\n' const isModel = (v: unknown): v is ModelName => v === 'haiku' || v === 'sonnet' || v === 'opus' const isEffort = (v: unknown): v is Effort => v === 'low' || v === 'medium' || v === 'high' const isTone = (v: unknown): v is Tone => v === 'neutral' || v === 'praise' || v === 'scold' export const parseRoute = (text: string): Route => { const m = /\{[\s\S]*\}/.exec(text) if (!m) return FALLBACK_ROUTE try { const j = JSON.parse(m[0]) as Record return { difficulty: typeof j.difficulty === 'number' ? j.difficulty : FALLBACK_ROUTE.difficulty, model: isModel(j.model) ? j.model : FALLBACK_ROUTE.model, effort: isEffort(j.effort) ? j.effort : FALLBACK_ROUTE.effort, tone: isTone(j.tone) ? j.tone : 'neutral', ack: typeof j.ack === 'string' ? j.ack.slice(0, 200) : '', work: j.work === true, } } catch { return FALLBACK_ROUTE } } export type ChooseOptions = { candidates: readonly ModelName[] lockModel?: ModelName lockEffort?: Effort /** 最緊的額度剩餘百分比;低於 downgradeAt 就降一級。 */ quotaRemain?: number downgradeAt: number } export const choose = (r: Route, o: ChooseOptions): { model: ModelName; effort: Effort; downgraded: boolean } => { const effort = o.lockEffort ?? r.effort if (o.lockModel) return { model: o.lockModel, effort, downgraded: false } const allowed = LADDER.filter(m => o.candidates.includes(m)) const pool = allowed.length ? allowed : LADDER let idx = Math.max(0, pool.indexOf(r.model)) if (!pool.includes(r.model)) idx = pool.length - 1 // 選到不在候選內的模型時取候選最強者 let downgraded = false if (o.quotaRemain !== undefined && o.quotaRemain < o.downgradeAt && idx > 0) { idx -= 1 downgraded = true } return { model: pool[idx]!, effort, downgraded } }