From 9b835a4b6e180bd8e5e0a3b3aba223e3fa4fb74e Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Fri, 23 Oct 2026 19:12:16 +0000 Subject: [PATCH] havre: the model picker's order (by name, cheapest first, priciest first) as pure functions --- ventures/havre/abilities.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ventures/havre/abilities.js b/ventures/havre/abilities.js index 443a385..12dfb5a 100644 --- a/ventures/havre/abilities.js +++ b/ventures/havre/abilities.js @@ -81,6 +81,41 @@ export const voicePrice = (model, chars) => (model && model.perMChars != null ? /** a minute of your voice, heard */ export const hearingPerMinute = (model) => (model && model.perSecond != null ? model.perSecond * 60 : null); +/** + * What a model costs, as the numbers the model picker sorts by, the first counting most: a text model's price in, then + * out (a chat is mostly what's sent in: the card, the memories, the conversation so far); a photo's price, then what each + * picture past the included ones adds; a voice's price for a million characters; hearing's price for a second. null when + * Venice lists no price (a clip: it prices each one when asked). + */ +export function modelCost(type, model) { + if (!model) return null; + const n = (x) => (typeof x === "number" && Number.isFinite(x) ? x : null); + const keys = type === "text" ? [n(model.input), n(model.output)] + : type === "inpaint" || type === "image" ? [n(model.price), n(model.extra)] + : type === "tts" ? [n(model.perMChars)] + : type === "asr" ? [n(model.perSecond)] + : []; + return keys.length && keys[0] != null ? keys.map((k) => k ?? 0) : null; +} +/** the orders the model picker has: by name (the one in use first), cheapest first, priciest first */ +export const MODEL_SORTS = ["name", "cheapest", "priciest"]; +/** + * The model picker's list in order, as a new list. "name": the ones rank puts higher first (the one in use, then the + * character's own), then A to Z. "cheapest" and "priciest": by modelCost, A to Z between equals, the ones without a price + * last either way, and none held at the top, so the one in use shows where its price puts it. + */ +export function sortModels(list, how = "name", { type = "text", rank = () => 0 } = {}) { + const all = [...(list || [])], byName = (a, b) => String(a.name || a.id).localeCompare(String(b.name || b.id)); + if (how !== "cheapest" && how !== "priciest") return all.sort((a, b) => rank(b) - rank(a) || byName(a, b)); + const dir = how === "priciest" ? -1 : 1, cost = new Map(all.map((m) => [m, modelCost(type, m)])); + return all.sort((a, b) => { + const x = cost.get(a), y = cost.get(b); + if (!x || !y) return (x ? 0 : 1) - (y ? 0 : 1) || byName(a, b); + for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return dir * (x[i] - y[i]); + return byName(a, b); + }); +} + // ---------- what goes into a photo or a clip ---------- // Content filters (Venice's, and the ones of the models' makers) read the words in a prompt, not the "no" in front of