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