@hanzo/ui 8.27.60 → 8.27.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/Composer.cjs +1 -1
- package/dist/chat/Composer.d.ts +1 -1
- package/dist/chat/Composer.js +1 -1
- package/dist/gallery.cjs +4 -4
- package/dist/gallery.d.ts.map +1 -1
- package/dist/gallery.html +4 -5
- package/dist/gallery.js +5 -5
- package/dist/gallery.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.node.cjs +1 -1
- package/dist/index.node.js +1 -1
- package/dist/models/ModelPicker.cjs +191 -0
- package/dist/models/ModelPicker.d.ts +27 -0
- package/dist/models/ModelPicker.d.ts.map +1 -0
- package/dist/models/ModelPicker.js +189 -0
- package/dist/models/ModelPicker.js.map +1 -0
- package/dist/models/catalog.cjs +313 -184
- package/dist/models/catalog.d.ts +101 -38
- package/dist/models/catalog.d.ts.map +1 -1
- package/dist/models/catalog.js +295 -179
- package/dist/models/catalog.js.map +1 -1
- package/dist/models/index.cjs +33 -12
- package/dist/models/index.d.ts +7 -4
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +7 -6
- package/dist/models/index.js.map +1 -1
- package/dist/models/picker.logic.cjs +83 -0
- package/dist/models/picker.logic.d.ts +51 -0
- package/dist/models/picker.logic.d.ts.map +1 -0
- package/dist/models/picker.logic.js +74 -0
- package/dist/models/picker.logic.js.map +1 -0
- package/dist/product/limits.cjs +14 -13
- package/dist/product/limits.d.ts +9 -10
- package/dist/product/limits.d.ts.map +1 -1
- package/dist/product/limits.js +14 -12
- package/dist/product/limits.js.map +1 -1
- package/dist/styles.css +1 -2
- package/package.json +1 -1
- package/dist/models/ModelSelector.cjs +0 -80
- package/dist/models/ModelSelector.d.ts +0 -44
- package/dist/models/ModelSelector.d.ts.map +0 -1
- package/dist/models/ModelSelector.js +0 -78
- package/dist/models/ModelSelector.js.map +0 -1
package/dist/models/catalog.cjs
CHANGED
|
@@ -1,220 +1,349 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @hanzo/ui/models — Model catalog data layer
|
|
4
|
-
*
|
|
5
|
-
* Pure, SSR-safe helpers shared by the unified ModelSelector. No React,
|
|
6
|
-
* no browser globals — usable on the server, in edge runtimes, or the client.
|
|
7
|
-
*
|
|
8
|
-
* ModelCatalogEntry is the gateway/OpenAI-shaped record ({ id, owned_by, ... })
|
|
9
|
-
* returned by `${baseUrl}/models`. It is intentionally distinct from the
|
|
10
|
-
* registry-shaped ZenModelLike in ./types (which drives the marketing cards).
|
|
11
|
-
*/
|
|
12
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.RESEARCH = void 0;
|
|
14
|
-
exports.
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
17
|
-
exports.filterChatModels = filterChatModels;
|
|
3
|
+
exports.CAPABILITY_NAMES = exports.CLASS_NAMES = exports.FAMILIES = exports.RESEARCH = void 0;
|
|
4
|
+
exports.withResearch = withResearch;
|
|
5
|
+
exports.modelOf = modelOf;
|
|
6
|
+
exports.parseModels = parseModels;
|
|
18
7
|
exports.fetchModelCatalog = fetchModelCatalog;
|
|
8
|
+
exports.makerOf = makerOf;
|
|
9
|
+
exports.makerName = makerName;
|
|
10
|
+
exports.modelName = modelName;
|
|
11
|
+
exports.groupKey = groupKey;
|
|
12
|
+
exports.groupLabel = groupLabel;
|
|
13
|
+
exports.groupModels = groupModels;
|
|
14
|
+
exports.capabilitiesOf = capabilitiesOf;
|
|
15
|
+
exports.can = can;
|
|
16
|
+
exports.matchesPaused = matchesPaused;
|
|
17
|
+
exports.isPaused = isPaused;
|
|
18
|
+
exports.matchesModel = matchesModel;
|
|
19
|
+
exports.sortModels = sortModels;
|
|
20
|
+
exports.formatContext = formatContext;
|
|
21
|
+
exports.formatPrice = formatPrice;
|
|
19
22
|
/**
|
|
20
23
|
* The models that exist and cannot be called, said once for every list of
|
|
21
24
|
* models: a picker draws each one disabled, a catalogue page draws it muted.
|
|
22
|
-
*
|
|
23
|
-
* No gateway serves them, and that is the point: `/v1/models` never answers for
|
|
24
|
-
* one, so nothing can route to it. A host that lists them appends these to what
|
|
25
|
-
* the gateway said.
|
|
25
|
+
* `/v1/models` never answers for one, so nothing can route to it.
|
|
26
26
|
*/
|
|
27
27
|
exports.RESEARCH = [
|
|
28
28
|
{
|
|
29
29
|
id: 'zen7',
|
|
30
30
|
owned_by: 'zenlm',
|
|
31
|
-
|
|
31
|
+
family: 'zen',
|
|
32
|
+
name: 'Zen 7',
|
|
32
33
|
access: 'research',
|
|
33
34
|
request: 'https://hanzo.ai/research-access',
|
|
34
35
|
description: 'The next open-weight generation after Zen 6, in research preview.',
|
|
35
36
|
},
|
|
36
37
|
];
|
|
38
|
+
/** The catalog with the research models it does not already list, appended. */
|
|
39
|
+
function withResearch(models) {
|
|
40
|
+
const ids = new Set(models.map((m) => m.id));
|
|
41
|
+
return [...models, ...exports.RESEARCH.filter((m) => !ids.has(m.id))];
|
|
42
|
+
}
|
|
43
|
+
// ── reading the wire ─────────────────────────────────────────────────────────
|
|
44
|
+
const CLASSES = ['premium', 'ours', 'free'];
|
|
45
|
+
const FAMILY_IDS = ['enso', 'zen', 'kai', 'jev', 'zoo'];
|
|
46
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v : undefined);
|
|
47
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
48
|
+
const pos = (v) => {
|
|
49
|
+
const n = num(v);
|
|
50
|
+
return n !== undefined && n > 0 ? n : undefined;
|
|
51
|
+
};
|
|
52
|
+
const yes = (v) => (v === true ? true : undefined);
|
|
53
|
+
const strs = (v) => {
|
|
54
|
+
if (!Array.isArray(v))
|
|
55
|
+
return undefined;
|
|
56
|
+
const out = v.filter((x) => typeof x === 'string' && x.trim() !== '').map((x) => x.toLowerCase());
|
|
57
|
+
return out.length ? out : undefined;
|
|
58
|
+
};
|
|
59
|
+
const oneOf = (set, v) => typeof v === 'string' && set.includes(v) ? v : undefined;
|
|
60
|
+
function pricingOf(v) {
|
|
61
|
+
if (!v || typeof v !== 'object')
|
|
62
|
+
return undefined;
|
|
63
|
+
const p = v;
|
|
64
|
+
const input = num(p.input_per_million);
|
|
65
|
+
const output = num(p.output_per_million);
|
|
66
|
+
if (input === undefined && output === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return {
|
|
69
|
+
...(input !== undefined && input >= 0 ? { input_per_million: input } : {}),
|
|
70
|
+
...(output !== undefined && output >= 0 ? { output_per_million: output } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** One row, checked field by field: it is a network response. Null without an id. */
|
|
74
|
+
function modelOf(v) {
|
|
75
|
+
if (!v || typeof v !== 'object')
|
|
76
|
+
return null;
|
|
77
|
+
const r = v;
|
|
78
|
+
const id = str(r.id);
|
|
79
|
+
if (!id)
|
|
80
|
+
return null;
|
|
81
|
+
const m = { id };
|
|
82
|
+
const set = (k, val) => {
|
|
83
|
+
if (val !== undefined)
|
|
84
|
+
m[k] = val;
|
|
85
|
+
};
|
|
86
|
+
set('owned_by', str(r.owned_by));
|
|
87
|
+
set('name', str(r.name));
|
|
88
|
+
set('description', str(r.description));
|
|
89
|
+
set('class', oneOf(CLASSES, r.class));
|
|
90
|
+
set('family', oneOf(FAMILY_IDS, r.family));
|
|
91
|
+
set('premium', typeof r.premium === 'boolean' ? r.premium : undefined);
|
|
92
|
+
set('created', pos(r.created));
|
|
93
|
+
set('context_window', pos(r.context_window));
|
|
94
|
+
set('max_output_tokens', pos(r.max_output_tokens));
|
|
95
|
+
set('inputs', strs(r.inputs));
|
|
96
|
+
set('outputs', strs(r.outputs));
|
|
97
|
+
set('supports_vision', yes(r.supports_vision));
|
|
98
|
+
set('supports_tools', yes(r.supports_tools));
|
|
99
|
+
set('supports_reasoning', yes(r.supports_reasoning));
|
|
100
|
+
set('pricing', pricingOf(r.pricing));
|
|
101
|
+
return m;
|
|
102
|
+
}
|
|
37
103
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* There is no `hanzo` entry, and its absence is the point. It used to say
|
|
41
|
-
* `hanzo: 'Zen'`, which was wrong twice: Zoo Labs Foundation makes Zen, and the
|
|
42
|
-
* gateway does not report Zen that way — it answers `owned_by: 'zenlm'`. What
|
|
43
|
-
* `owned_by: 'hanzo'` actually names is the dozen models Hanzo serves under its
|
|
44
|
-
* own namespace, so that entry filed Whisper, Kokoro, GLM, Kimi, Qwen3.5 and
|
|
45
|
-
* MiniMax under "Zen" as well. Without it each falls to its id prefix and lands
|
|
46
|
-
* where it belongs, and what is genuinely Hanzo's capitalizes to "Hanzo".
|
|
104
|
+
* The catalog from a `/v1/models` body (`{data: [...]}`) or its `data` array.
|
|
105
|
+
* Rows without an id are dropped; a repeated id keeps its first row.
|
|
47
106
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
107
|
+
function parseModels(body) {
|
|
108
|
+
const rows = Array.isArray(body)
|
|
109
|
+
? body
|
|
110
|
+
: body && typeof body === 'object' && Array.isArray(body.data)
|
|
111
|
+
? (body.data)
|
|
112
|
+
: [];
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const m = modelOf(row);
|
|
117
|
+
if (!m || seen.has(m.id))
|
|
118
|
+
continue;
|
|
119
|
+
seen.add(m.id);
|
|
120
|
+
out.push(m);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Fetch the catalog from an OpenAI-shaped `/models` endpoint. No caching, no
|
|
126
|
+
* state: the host owns both. Throws on a non-2xx answer.
|
|
127
|
+
*/
|
|
128
|
+
async function fetchModelCatalog(baseUrl, token) {
|
|
129
|
+
const base = (baseUrl ?? 'https://api.hanzo.ai/v1').replace(/\/+$/, '');
|
|
130
|
+
const headers = { Accept: 'application/json' };
|
|
131
|
+
if (token)
|
|
132
|
+
headers.Authorization = `Bearer ${token}`;
|
|
133
|
+
const res = await fetch(`${base}/models`, { headers });
|
|
134
|
+
if (!res.ok)
|
|
135
|
+
throw new Error(`fetchModelCatalog: ${res.status} ${res.statusText}`);
|
|
136
|
+
return parseModels(await res.json());
|
|
137
|
+
}
|
|
138
|
+
// ── what a model is ──────────────────────────────────────────────────────────
|
|
139
|
+
/** The families, in the order every surface leads with them. */
|
|
140
|
+
exports.FAMILIES = [
|
|
141
|
+
{ id: 'enso', label: 'Enso' },
|
|
142
|
+
{ id: 'zen', label: 'Zen' },
|
|
143
|
+
{ id: 'kai', label: 'Kai' },
|
|
144
|
+
{ id: 'jev', label: 'Jev' },
|
|
145
|
+
{ id: 'zoo', label: 'Zoo' },
|
|
146
|
+
];
|
|
147
|
+
exports.CLASS_NAMES = {
|
|
148
|
+
premium: 'Premium',
|
|
149
|
+
ours: 'Hanzo',
|
|
150
|
+
free: 'Free',
|
|
151
|
+
};
|
|
152
|
+
/** The makers whose name is not their id capitalized. */
|
|
153
|
+
const MAKERS = {
|
|
154
|
+
'aion-labs': 'Aion Labs',
|
|
155
|
+
'arcee-ai': 'Arcee AI',
|
|
156
|
+
'bytedance-seed': 'ByteDance Seed',
|
|
157
|
+
'dots-studio': 'dots.studio',
|
|
158
|
+
'ibm-granite': 'IBM Granite',
|
|
159
|
+
'inference-net': 'Inference.net',
|
|
160
|
+
'meta-llama': 'Meta Llama',
|
|
161
|
+
'nex-agi': 'Nex AGI',
|
|
162
|
+
'prism-ml': 'Prism ML',
|
|
163
|
+
'x-ai': 'xAI',
|
|
164
|
+
'z-ai': 'Z.ai',
|
|
165
|
+
ai21: 'AI21',
|
|
50
166
|
anthropic: 'Anthropic',
|
|
51
|
-
|
|
167
|
+
bytedance: 'ByteDance',
|
|
168
|
+
cognitivecomputations: 'Cognitive Computations',
|
|
52
169
|
deepseek: 'DeepSeek',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
google: 'Google Gemma',
|
|
170
|
+
inclusionai: 'inclusionAI',
|
|
171
|
+
minimax: 'MiniMax',
|
|
172
|
+
mistralai: 'Mistral AI',
|
|
173
|
+
moonshotai: 'Moonshot AI',
|
|
174
|
+
nousresearch: 'Nous Research',
|
|
59
175
|
nvidia: 'NVIDIA',
|
|
60
|
-
|
|
176
|
+
openai: 'OpenAI',
|
|
177
|
+
openrouter: 'OpenRouter',
|
|
178
|
+
rekaai: 'Reka AI',
|
|
179
|
+
thedrummer: 'TheDrummer',
|
|
180
|
+
thinkingmachines: 'Thinking Machines',
|
|
181
|
+
typesafe: 'TypeSafe',
|
|
182
|
+
zenlm: 'Zen LM',
|
|
61
183
|
};
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (id.startsWith('deepseek'))
|
|
86
|
-
return 'DeepSeek';
|
|
87
|
-
if (id.startsWith('mistral'))
|
|
88
|
-
return 'Mistral';
|
|
89
|
-
if (id.startsWith('gemma'))
|
|
90
|
-
return 'Google Gemma';
|
|
91
|
-
if (id.startsWith('nemotron'))
|
|
92
|
-
return 'NVIDIA';
|
|
93
|
-
if (id.startsWith('minimax'))
|
|
94
|
-
return 'MiniMax';
|
|
95
|
-
if (id.startsWith('mimo'))
|
|
96
|
-
return 'MiMo';
|
|
97
|
-
return undefined;
|
|
184
|
+
/** Who made a model, as the catalog keys it: `owned_by`, lowercased, without OpenRouter's `~` alias mark. */
|
|
185
|
+
function makerOf(m) {
|
|
186
|
+
return (m.owned_by ?? '').trim().replace(/^~/, '').toLowerCase() || 'other';
|
|
187
|
+
}
|
|
188
|
+
/** A maker's display name. */
|
|
189
|
+
function makerName(maker) {
|
|
190
|
+
return (MAKERS[maker] ??
|
|
191
|
+
maker
|
|
192
|
+
.split(/[-_]/)
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
195
|
+
.join(' '));
|
|
196
|
+
}
|
|
197
|
+
/** The name a model is shown by: the catalog's name, else its id. */
|
|
198
|
+
function modelName(m) {
|
|
199
|
+
return m.name ?? m.id;
|
|
200
|
+
}
|
|
201
|
+
function groupKey(m) {
|
|
202
|
+
return m.family ? `family:${m.family}` : `maker:${makerOf(m)}`;
|
|
203
|
+
}
|
|
204
|
+
function groupLabel(m) {
|
|
205
|
+
const f = m.family && exports.FAMILIES.find((x) => x.id === m.family);
|
|
206
|
+
return f ? f.label : makerName(makerOf(m));
|
|
98
207
|
}
|
|
99
208
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* owned_by map → llama id under any owner → capitalized owned_by → id prefix → "Other".
|
|
209
|
+
* Models in groups: Hanzo's families first, in FAMILIES order, then every maker
|
|
210
|
+
* by name. Models keep the order they came in, so the caller sorts first.
|
|
103
211
|
*/
|
|
104
|
-
function
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const byOwner = OWNER_FAMILY[owned];
|
|
115
|
-
if (byOwner)
|
|
116
|
-
return byOwner;
|
|
117
|
-
// llama* ids resolve to Meta Llama even under a different owner.
|
|
118
|
-
const byId = familyFromId(id);
|
|
119
|
-
if (byId)
|
|
120
|
-
return byId;
|
|
121
|
-
return capitalize(owned);
|
|
122
|
-
}
|
|
123
|
-
return familyFromId(id) ?? 'Other';
|
|
124
|
-
}
|
|
125
|
-
function orderFamilies(a, b) {
|
|
126
|
-
const ia = FAMILY_ORDER.indexOf(a);
|
|
127
|
-
const ib = FAMILY_ORDER.indexOf(b);
|
|
128
|
-
if (ia !== -1 || ib !== -1) {
|
|
129
|
-
if (ia === -1)
|
|
130
|
-
return 1;
|
|
131
|
-
if (ib === -1)
|
|
132
|
-
return -1;
|
|
133
|
-
return ia - ib;
|
|
212
|
+
function groupModels(models) {
|
|
213
|
+
const groups = new Map();
|
|
214
|
+
for (const m of models) {
|
|
215
|
+
const key = groupKey(m);
|
|
216
|
+
let g = groups.get(key);
|
|
217
|
+
if (!g) {
|
|
218
|
+
g = { key, label: groupLabel(m), ...(m.family ? { family: m.family } : {}), models: [] };
|
|
219
|
+
groups.set(key, g);
|
|
220
|
+
}
|
|
221
|
+
g.models.push(m);
|
|
134
222
|
}
|
|
135
|
-
|
|
223
|
+
const rank = (g) => (g.family ? exports.FAMILIES.findIndex((f) => f.id === g.family) : exports.FAMILIES.length);
|
|
224
|
+
return [...groups.values()].sort((a, b) => rank(a) - rank(b) || a.label.localeCompare(b.label));
|
|
136
225
|
}
|
|
226
|
+
const has = (list, ...want) => !!list && want.some((w) => list.includes(w));
|
|
137
227
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
228
|
+
* What a model can do, read from what it publishes. A model that names no
|
|
229
|
+
* outputs is read as a text model, the way the gateway serves it.
|
|
140
230
|
*/
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
231
|
+
function capabilitiesOf(m) {
|
|
232
|
+
const out = [];
|
|
233
|
+
const embeddings = has(m.outputs, 'embeddings', 'embedding');
|
|
234
|
+
const rerank = has(m.outputs, 'rerank');
|
|
235
|
+
const decision = has(m.outputs, 'decision');
|
|
236
|
+
const text = !m.outputs || has(m.outputs, 'text');
|
|
237
|
+
if (text && !embeddings && !rerank && !decision)
|
|
238
|
+
out.push('chat');
|
|
239
|
+
if (m.supports_vision || has(m.inputs, 'image'))
|
|
240
|
+
out.push('vision');
|
|
241
|
+
if (embeddings)
|
|
242
|
+
out.push('embeddings');
|
|
243
|
+
if (rerank)
|
|
244
|
+
out.push('rerank');
|
|
245
|
+
if (has(m.outputs, 'image'))
|
|
246
|
+
out.push('image');
|
|
247
|
+
if (has(m.outputs, 'video'))
|
|
248
|
+
out.push('video');
|
|
249
|
+
if (has(m.outputs, 'audio', 'transcript') || has(m.inputs, 'audio'))
|
|
250
|
+
out.push('audio');
|
|
251
|
+
if (decision)
|
|
252
|
+
out.push('decision');
|
|
253
|
+
if (m.supports_tools)
|
|
254
|
+
out.push('tools');
|
|
255
|
+
if (m.supports_reasoning)
|
|
256
|
+
out.push('reasoning');
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
function can(m, c) {
|
|
260
|
+
return capabilitiesOf(m).includes(c);
|
|
261
|
+
}
|
|
262
|
+
exports.CAPABILITY_NAMES = {
|
|
263
|
+
chat: 'Chat',
|
|
264
|
+
vision: 'Vision',
|
|
265
|
+
embeddings: 'Embeddings',
|
|
266
|
+
rerank: 'Rerank',
|
|
267
|
+
image: 'Image',
|
|
268
|
+
video: 'Video',
|
|
269
|
+
audio: 'Audio',
|
|
270
|
+
decision: 'Decision',
|
|
271
|
+
tools: 'Tools',
|
|
272
|
+
reasoning: 'Reasoning',
|
|
273
|
+
};
|
|
274
|
+
/** Whether `id` matches a paused entry: the same id, or a glob whose `*` stands for any run of characters. */
|
|
275
|
+
function matchesPaused(id, pattern) {
|
|
276
|
+
if (!pattern.includes('*'))
|
|
277
|
+
return id === pattern;
|
|
278
|
+
const parts = pattern.split('*');
|
|
279
|
+
if (!id.startsWith(parts[0]) || !id.endsWith(parts.at(-1)))
|
|
280
|
+
return false;
|
|
281
|
+
let at = parts[0].length;
|
|
282
|
+
const end = id.length - parts.at(-1).length;
|
|
283
|
+
for (const part of parts.slice(1, -1)) {
|
|
284
|
+
const i = id.indexOf(part, at);
|
|
285
|
+
if (i < 0 || i + part.length > end)
|
|
286
|
+
return false;
|
|
287
|
+
at = i + part.length;
|
|
150
288
|
}
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
.map((family) => ({ family, models: byFamily.get(family) }));
|
|
154
|
-
}
|
|
155
|
-
/** Modality/id keywords that mark a model as NOT chat-capable. */
|
|
156
|
-
const NON_CHAT = [
|
|
157
|
-
'embedding',
|
|
158
|
-
'embed',
|
|
159
|
-
'image',
|
|
160
|
-
'video',
|
|
161
|
-
'music',
|
|
162
|
-
'voice',
|
|
163
|
-
'tts',
|
|
164
|
-
'rerank',
|
|
165
|
-
'guard',
|
|
166
|
-
'foley',
|
|
167
|
-
'moderation',
|
|
168
|
-
'router',
|
|
169
|
-
];
|
|
289
|
+
return at <= end;
|
|
290
|
+
}
|
|
170
291
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* or
|
|
292
|
+
* Whether the payer's plan has paused this model: its whole class is limited,
|
|
293
|
+
* or an entry in `paused` names it. It is still listed and still picked; the
|
|
294
|
+
* gateway refuses it or answers from Enso.
|
|
174
295
|
*/
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
if (modality && NON_CHAT.some((k) => modality.includes(k)))
|
|
178
|
-
return false;
|
|
179
|
-
const id = (m.id ?? '').toLowerCase();
|
|
180
|
-
if (NON_CHAT.some((k) => id.includes(k)))
|
|
296
|
+
function isPaused(m, limits) {
|
|
297
|
+
if (!limits)
|
|
181
298
|
return false;
|
|
182
|
-
|
|
299
|
+
if (m.class && limits.classes[m.class]?.state === 'limited')
|
|
300
|
+
return true;
|
|
301
|
+
return !!limits.paused?.some((p) => matchesPaused(m.id, p.model));
|
|
183
302
|
}
|
|
184
|
-
/**
|
|
185
|
-
function
|
|
186
|
-
|
|
303
|
+
/** Case-insensitive search: every word of the query appears in the model's id, name, maker, group or description. */
|
|
304
|
+
function matchesModel(m, query) {
|
|
305
|
+
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
306
|
+
if (!words.length)
|
|
307
|
+
return true;
|
|
308
|
+
const hay = [m.id, m.name, makerOf(m), makerName(makerOf(m)), groupLabel(m), m.description]
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join(' ')
|
|
311
|
+
.toLowerCase();
|
|
312
|
+
return words.every((w) => hay.includes(w));
|
|
187
313
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
314
|
+
/** A sorted copy. Context and price put models without the figure last. */
|
|
315
|
+
function sortModels(models, by = 'name') {
|
|
316
|
+
const name = (a, b) => modelName(a).localeCompare(modelName(b));
|
|
317
|
+
const last = (v, dir) => (v === undefined ? Infinity : dir * v);
|
|
318
|
+
return [...models].sort((a, b) => {
|
|
319
|
+
switch (by) {
|
|
320
|
+
case 'newest':
|
|
321
|
+
return (b.created ?? 0) - (a.created ?? 0) || name(a, b);
|
|
322
|
+
case 'context':
|
|
323
|
+
return last(a.context_window, -1) - last(b.context_window, -1) || name(a, b);
|
|
324
|
+
case 'price':
|
|
325
|
+
return last(a.pricing?.input_per_million, 1) - last(b.pricing?.input_per_million, 1) || name(a, b);
|
|
326
|
+
default:
|
|
327
|
+
return name(a, b);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// ── how a figure reads ───────────────────────────────────────────────────────
|
|
332
|
+
/** "200K", "1M", "1.5M"; "" when the catalog states no window. */
|
|
333
|
+
function formatContext(n) {
|
|
334
|
+
if (!n)
|
|
335
|
+
return '';
|
|
336
|
+
if (n >= 1_000_000)
|
|
337
|
+
return `${+(n / 1_000_000).toFixed(1)}M`;
|
|
338
|
+
if (n >= 1000)
|
|
339
|
+
return `${Math.round(n / 1000)}K`;
|
|
340
|
+
return String(n);
|
|
341
|
+
}
|
|
342
|
+
/** "$3.00" per 1M tokens, "Free" at zero, "" when the catalog lists no price. */
|
|
343
|
+
function formatPrice(n) {
|
|
344
|
+
if (n === undefined)
|
|
345
|
+
return '';
|
|
346
|
+
if (n === 0)
|
|
347
|
+
return 'Free';
|
|
348
|
+
return n < 0.01 ? `$${+n.toPrecision(2)}` : `$${n.toFixed(2)}`;
|
|
220
349
|
}
|