@vidofy/mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/backend.d.ts +133 -0
- package/dist/backend.js +455 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.js +207 -0
- package/dist/heartbeat.d.ts +11 -0
- package/dist/heartbeat.js +70 -0
- package/dist/http.d.ts +44 -0
- package/dist/http.js +885 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +661 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +22 -0
- package/dist/map/b2c.d.ts +139 -0
- package/dist/map/b2c.js +236 -0
- package/dist/media-duration.d.ts +33 -0
- package/dist/media-duration.js +137 -0
- package/dist/oauth/authorize.d.ts +61 -0
- package/dist/oauth/authorize.js +275 -0
- package/dist/oauth/clients.d.ts +137 -0
- package/dist/oauth/clients.js +621 -0
- package/dist/oauth/ratelimit.d.ts +92 -0
- package/dist/oauth/ratelimit.js +116 -0
- package/dist/oauth/store.d.ts +135 -0
- package/dist/oauth/store.js +277 -0
- package/dist/oauth/token.d.ts +29 -0
- package/dist/oauth/token.js +165 -0
- package/dist/schema.d.ts +147 -0
- package/dist/schema.js +629 -0
- package/dist/tools/account.d.ts +28 -0
- package/dist/tools/account.js +130 -0
- package/dist/tools/generation.d.ts +102 -0
- package/dist/tools/generation.js +1194 -0
- package/dist/tools/info.d.ts +50 -0
- package/dist/tools/info.js +95 -0
- package/dist/ui/generation-card.html +869 -0
- package/package.json +79 -0
- package/server.json +36 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn one model's admin-authored `m_options` into a JSON Schema an agent can
|
|
3
|
+
* fill in, plus the wire names needed to actually submit it.
|
|
4
|
+
*
|
|
5
|
+
* This is the widest surface in the package. A human wrote the options for every
|
|
6
|
+
* model in the catalogue, so the rule here is: describe only what the data
|
|
7
|
+
* actually contains, and stay silent rather than invent. A property this file
|
|
8
|
+
* omits costs the agent a feature; a property it invents costs the user a
|
|
9
|
+
* rejected generation they already waited for.
|
|
10
|
+
*
|
|
11
|
+
* SHAPES, MEASURED ACROSS THE WHOLE ACTIVE CATALOGUE — not read off the docs:
|
|
12
|
+
* m_aspect_ratio ARRAY all (often empty)
|
|
13
|
+
* m_resolution_quality ARRAY all (often empty)
|
|
14
|
+
* m_duration OBJECT some KEYS are the durations; each value is
|
|
15
|
+
* the list of resolutions that duration
|
|
16
|
+
* allows ({"5": [], "10": []} = any)
|
|
17
|
+
* m_output_number INTEGER all
|
|
18
|
+
* m_dynamic_fields ARRAY all a long tail of field names
|
|
19
|
+
* m_multi_upload OBJECT all 8 key sets, TWO families (see below)
|
|
20
|
+
* m_negative_prompt / m_seed / m_generate_audio /
|
|
21
|
+
* m_camera_fixed / m_enhance_prompt all BOOLEAN
|
|
22
|
+
*
|
|
23
|
+
* DYNAMIC FIELD TYPES IN USE — only five, though the docs list nine:
|
|
24
|
+
* radio_group · select · toggle · slider · file_upload_image
|
|
25
|
+
* Unknown types degrade to a string rather than throwing: a model added
|
|
26
|
+
* tomorrow must not break the whole catalogue.
|
|
27
|
+
*/
|
|
28
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
29
|
+
const asRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
30
|
+
const asArray = (v) => (Array.isArray(v) ? v : []);
|
|
31
|
+
const isTrue = (v) => v === true || v === 1 || v === '1' || v === 'true' || v === 'on';
|
|
32
|
+
const num = (v) => {
|
|
33
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? Number(v) : NaN;
|
|
34
|
+
return Number.isFinite(n) ? n : null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Clean names for wire fields whose obvious de-prefixing would be wrong or
|
|
38
|
+
* inconsistent.
|
|
39
|
+
*
|
|
40
|
+
* EMPTY, and that is the finished state rather than a stub.
|
|
41
|
+
*
|
|
42
|
+
* It held one entry — `m_resolution_quality: 'resolution'` — placed there for a
|
|
43
|
+
* good reason: that field reaches the schema by TWO routes (the base
|
|
44
|
+
* `m_resolution_quality` array, and on other models a dynamic field of the same
|
|
45
|
+
* name), and without the table the same input would have been called
|
|
46
|
+
* `resolution` on one model and `resolution_quality` on the next. One name for
|
|
47
|
+
* one input was right.
|
|
48
|
+
*
|
|
49
|
+
* The name it chose was not. `m_resolution` is a DIFFERENT wire field, carried
|
|
50
|
+
* by video models as a per-duration list, and it also claimed `resolution` — so
|
|
51
|
+
* two distinct fields shared one clean name, and `wire[]` kept only whichever
|
|
52
|
+
* registered last. Harmless while no model needed both, which was measured and
|
|
53
|
+
* true; `test-i2i` then carried both and the agent lost the ability to send a
|
|
54
|
+
* field the validator demands (gate: SERVER_REQUIRES_MISSING_FIELD, 2026-09-12).
|
|
55
|
+
*
|
|
56
|
+
* Nothing aliases the two for us. The server DOES alias them — but only on a
|
|
57
|
+
* JSON body, and account mode submits multipart, where the only
|
|
58
|
+
* canonicalisation is stripping a "p" suffix. The partner reference says it
|
|
59
|
+
* outright:
|
|
60
|
+
* "a multipart submit that sends only m_resolution to a model requiring it gets
|
|
61
|
+
* a 422 listing m_resolution_quality as missing."
|
|
62
|
+
*
|
|
63
|
+
* So each field now keeps its own de-prefixed name — `resolution_quality` and
|
|
64
|
+
* `resolution` — which is also the pair /api/v1 documents, so MCP and the
|
|
65
|
+
* Partners API say the same words. Collision is gone by construction, and the
|
|
66
|
+
* both-routes consistency the table existed for still holds, because plain
|
|
67
|
+
* de-prefixing gives both routes the same answer.
|
|
68
|
+
*
|
|
69
|
+
* Add an entry here only for a field whose de-prefixed name would be WRONG,
|
|
70
|
+
* never to merge two fields into one name.
|
|
71
|
+
*/
|
|
72
|
+
const CLEAN_NAME = {};
|
|
73
|
+
const cleanNameFor = (wireName) => CLEAN_NAME[wireName] ?? wireName.replace(/^m_/, '');
|
|
74
|
+
/**
|
|
75
|
+
* The server's own fallback when a model declares no upload limits.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors the server's own hardcoded upload defaults, whose rule is that an
|
|
78
|
+
* empty declared list falls back to them — an empty list means "use the
|
|
79
|
+
* defaults", NEVER "allow anything".
|
|
80
|
+
*
|
|
81
|
+
* This package used to read the declared list and, when it was empty, emit
|
|
82
|
+
* `accepts: []`, which the transport read as "no allowlist to enforce". That is
|
|
83
|
+
* fail-OPEN where the server fails closed, and it was not hypothetical: 10+
|
|
84
|
+
* active, popular models (nano-banana-i2i, gpt-image-1-5-edit-i2i,
|
|
85
|
+
* seedream-4-5-edit-i2i, wan-2-6-r2v …) ship a multi-upload slot with no
|
|
86
|
+
* extensions declared. On those, an agent talked into passing a path to an SSH
|
|
87
|
+
* key or a .env would have had it read and uploaded — measured 2026-09-07,
|
|
88
|
+
* `wallet.pem` went through untouched.
|
|
89
|
+
*/
|
|
90
|
+
const UPLOAD_DEFAULTS = {
|
|
91
|
+
image: { accepts: ['jpg', 'jpeg', 'png', 'webp'], maxSizeMb: 10 },
|
|
92
|
+
video: { accepts: ['mp4', 'mov'], maxSizeMb: 50 },
|
|
93
|
+
audio: { accepts: ['mp3', 'wav'], maxSizeMb: 25 },
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Resolve one slot's real limits, the way the server resolves them.
|
|
97
|
+
*
|
|
98
|
+
* `kind` is image/video/audio; anything else falls back to image, matching the
|
|
99
|
+
* server's own defaults table and its image fallback.
|
|
100
|
+
*/
|
|
101
|
+
function resolveUploadRules(declaredExts, declaredMax, kind) {
|
|
102
|
+
const d = UPLOAD_DEFAULTS[kind] ?? UPLOAD_DEFAULTS['image'];
|
|
103
|
+
const exts = asArray(declaredExts)
|
|
104
|
+
// CSV-in-array is real legacy data — the server's own normaliser
|
|
105
|
+
// handles 'jpg,png' and ['jpg,png'] as well as a proper array.
|
|
106
|
+
.flatMap((v) => String(v).split(','))
|
|
107
|
+
.map((s) => s.trim().toLowerCase().replace(/^\./, ''))
|
|
108
|
+
.filter((s) => s !== '');
|
|
109
|
+
const max = num(declaredMax);
|
|
110
|
+
return {
|
|
111
|
+
accepts: exts.length ? exts : d.accepts,
|
|
112
|
+
maxSizeMb: max !== null && max > 0 ? max : d.maxSizeMb,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Descriptions are admin-authored and contain markup like `<br>`. */
|
|
116
|
+
const plain = (v) => String(v ?? '')
|
|
117
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
118
|
+
.replace(/<[^>]*>/g, '')
|
|
119
|
+
.replace(/\s+/g, ' ')
|
|
120
|
+
.trim();
|
|
121
|
+
/* ── the builder ─────────────────────────────────────────────────────────── */
|
|
122
|
+
/**
|
|
123
|
+
* @param payload The whole `/info/model-info/{slug}` body. The B2C door wraps
|
|
124
|
+
* the model under `.model` with m_-prefixed keys; the B2B door returns the
|
|
125
|
+
* options flat and unprefixed. Both are accepted, and which one arrived is
|
|
126
|
+
* decided by the PAYLOAD, never by the configured mode — a status lookup can
|
|
127
|
+
* legitimately return the other shape.
|
|
128
|
+
*/
|
|
129
|
+
export function buildModelSchema(payload) {
|
|
130
|
+
const root = asRecord(payload);
|
|
131
|
+
const wrapped = asRecord(root['model']);
|
|
132
|
+
const isWrapped = Object.keys(wrapped).length > 0;
|
|
133
|
+
const model = isWrapped ? wrapped : root;
|
|
134
|
+
/** Read a model field under either naming. */
|
|
135
|
+
const mf = (key) => model[`m_${key}`] ?? model[key];
|
|
136
|
+
const opts = asRecord(mf('options'));
|
|
137
|
+
/** Read an option under either naming. */
|
|
138
|
+
const opt = (key) => opts[`m_${key}`] ?? opts[key];
|
|
139
|
+
const properties = {};
|
|
140
|
+
const required = [];
|
|
141
|
+
const wire = {};
|
|
142
|
+
/* supportsReuse is derived from the finished list, not set at each of the
|
|
143
|
+
* five push sites — see the block just above the return. */
|
|
144
|
+
const files = [];
|
|
145
|
+
const notes = [];
|
|
146
|
+
/* Admin defaults — the reason a "required" field often is not.
|
|
147
|
+
*
|
|
148
|
+
* The server applies model defaults BEFORE
|
|
149
|
+
* every required check, filling any listed field that arrives absent or
|
|
150
|
+
* empty from `m_options.m_defaults`. So a
|
|
151
|
+
* field with a non-empty default can never be missing by the time the check
|
|
152
|
+
* runs — marking it required in the schema is stricter than the server and
|
|
153
|
+
* makes the agent supply something it did not need to.
|
|
154
|
+
*
|
|
155
|
+
* 72 model/field pairs carry one today (60 resolution, 4 output_format,
|
|
156
|
+
* 4 style, 4 movement_amplitude), and this was missed when those four
|
|
157
|
+
* fields were made required. It also inflated the figure quoted that day:
|
|
158
|
+
* the count of models missing one of those four fields included many that
|
|
159
|
+
* carried a default the server would have filled, so the number that could
|
|
160
|
+
* actually never generate was far smaller. */
|
|
161
|
+
const modelDefaults = asRecord(opt('defaults'));
|
|
162
|
+
const defaultFor = (wireName) => {
|
|
163
|
+
const v = modelDefaults[wireName];
|
|
164
|
+
return v === undefined || v === null ? '' : String(v);
|
|
165
|
+
};
|
|
166
|
+
const add = (name, wireName, prop, isRequired = false) => {
|
|
167
|
+
/* Two wire fields may never share one clean name.
|
|
168
|
+
*
|
|
169
|
+
* `wire[name] = wireName` below is a plain assignment, so a second
|
|
170
|
+
* registration under the same name used to overwrite the first and take
|
|
171
|
+
* the losing field out of the map entirely. Nothing failed at that
|
|
172
|
+
* moment: the schema still validated, the tool still answered, and the
|
|
173
|
+
* only symptom arrived later as a 422 from the server naming a field the
|
|
174
|
+
* agent was never told about.
|
|
175
|
+
*
|
|
176
|
+
* It happened. `resolution` was issued to both m_resolution_quality and
|
|
177
|
+
* m_resolution, safe only because no model populated both — measured,
|
|
178
|
+
* documented in a comment, and then falsified by a single row.
|
|
179
|
+
*
|
|
180
|
+
* So the invariant is enforced here instead of being asserted in prose.
|
|
181
|
+
* Throwing is the right answer rather than renaming silently: a
|
|
182
|
+
* duplicate means two inputs were given one identity, and which of them
|
|
183
|
+
* the agent should send is a decision for whoever added the second, not
|
|
184
|
+
* a default this helper can guess. get_model surfaces the throw as an
|
|
185
|
+
* error on that model alone, which is loud, local, and impossible to
|
|
186
|
+
* ship unnoticed — the gate builds a schema for every active model. */
|
|
187
|
+
if (wire[name] !== undefined && wire[name] !== wireName) {
|
|
188
|
+
throw new Error(`schema: clean name "${name}" is already mapped to "${wire[name]}" and cannot ` +
|
|
189
|
+
`also carry "${wireName}" — give one of them its own name (see CLEAN_NAME).`);
|
|
190
|
+
}
|
|
191
|
+
const dflt = defaultFor(wireName);
|
|
192
|
+
// Surface the default so the agent can send it deliberately, and drop
|
|
193
|
+
// the required flag when one exists — the server will fill it anyway.
|
|
194
|
+
// Only a string default is copied in, and only when the property has
|
|
195
|
+
// none of its own. exactOptionalPropertyTypes forbids spreading a
|
|
196
|
+
// possibly-undefined `default` back over the object.
|
|
197
|
+
properties[name] =
|
|
198
|
+
dflt !== '' && prop.default === undefined && prop.type === 'string'
|
|
199
|
+
? { ...prop, default: dflt }
|
|
200
|
+
: prop;
|
|
201
|
+
wire[name] = wireName;
|
|
202
|
+
if (isRequired && dflt === '')
|
|
203
|
+
required.push(name);
|
|
204
|
+
};
|
|
205
|
+
/* ── prompt ──────────────────────────────────────────────────────────
|
|
206
|
+
Required exactly when the shared validator requires it:
|
|
207
|
+
m_active_prompt is on AND m_prompt_settings.required is not false.
|
|
208
|
+
Mirrored from the server's shared validator rather than guessed — this is
|
|
209
|
+
the single most common reason a submit is refused. */
|
|
210
|
+
const promptSettings = asRecord(opt('prompt_settings'));
|
|
211
|
+
if (isTrue(opt('active_prompt'))) {
|
|
212
|
+
const maxChars = num(promptSettings['max_chars']);
|
|
213
|
+
add('prompt', 'm_prompt', {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: plain(promptSettings['description']) ||
|
|
216
|
+
plain(promptSettings['placeholder']) ||
|
|
217
|
+
'What to generate.',
|
|
218
|
+
}, promptSettings['required'] !== false);
|
|
219
|
+
// Carried as a note, not a schema keyword: `maximum` constrains numbers,
|
|
220
|
+
// and the length keyword this subset does not model is `maxLength`. A
|
|
221
|
+
// note the agent reads beats a keyword that would silently do nothing.
|
|
222
|
+
if (maxChars)
|
|
223
|
+
notes.push(`prompt is limited to ${maxChars} characters`);
|
|
224
|
+
}
|
|
225
|
+
if (isTrue(opt('negative_prompt'))) {
|
|
226
|
+
add('negative_prompt', 'm_negative_prompt', {
|
|
227
|
+
type: 'string',
|
|
228
|
+
description: 'What to avoid in the output.',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/* ── choice lists ────────────────────────────────────────────────────
|
|
232
|
+
Emitted only when non-empty. An empty array means the model does not
|
|
233
|
+
offer the choice at all, and advertising it would invite a rejected
|
|
234
|
+
value.
|
|
235
|
+
|
|
236
|
+
AND A NON-EMPTY LIST MEANS THE FIELD IS REQUIRED. That is not an
|
|
237
|
+
inference — the validator tests "set, an array, and not empty" and then
|
|
238
|
+
rejects the request when the
|
|
239
|
+
value is absent, and the pricing endpoint runs the same validator. An
|
|
240
|
+
earlier version of this file emitted these as optional, which would have
|
|
241
|
+
made every generate fail on the large majority of models, which carry at
|
|
242
|
+
least one enum; the gate caught it as MISSING_REQUIRED_FIELDS. */
|
|
243
|
+
const aspect = asArray(opt('aspect_ratio')).map(String).filter((s) => s !== '');
|
|
244
|
+
if (aspect.length) {
|
|
245
|
+
add('aspect_ratio', 'm_aspect_ratio', { type: 'string', enum: aspect, description: 'Output shape.' }, true);
|
|
246
|
+
}
|
|
247
|
+
/* IMAGE resolution. Measured 2026-09-12: 24 active listed models carry a
|
|
248
|
+
non-empty list here and every one of them is an image — values read
|
|
249
|
+
"1K"/"2K"/"4K", not "720"/"1080". The video counterpart is m_resolution
|
|
250
|
+
below, a separate wire field; see the CLEAN_NAME note for why they must
|
|
251
|
+
not share a clean name. */
|
|
252
|
+
const resolution = asArray(opt('resolution_quality')).map(String).filter((s) => s !== '');
|
|
253
|
+
if (resolution.length) {
|
|
254
|
+
add('resolution_quality', 'm_resolution_quality', {
|
|
255
|
+
type: 'string',
|
|
256
|
+
enum: resolution,
|
|
257
|
+
description: 'Output resolution.',
|
|
258
|
+
}, true);
|
|
259
|
+
}
|
|
260
|
+
/* Duration is an OBJECT whose KEYS are the allowed durations; each value
|
|
261
|
+
lists the resolutions that duration permits (empty = all). The pairing
|
|
262
|
+
is enforced server-side, so the schema offers the durations and says so
|
|
263
|
+
rather than trying to express a cross-field rule JSON Schema cannot. */
|
|
264
|
+
const durations = Object.keys(asRecord(opt('duration')));
|
|
265
|
+
if (durations.length) {
|
|
266
|
+
// Required for the same reason as the two above: a populated list is a
|
|
267
|
+
// choice the validator insists on.
|
|
268
|
+
add('duration', 'm_duration', {
|
|
269
|
+
type: 'string',
|
|
270
|
+
enum: durations,
|
|
271
|
+
description: 'Length in seconds.',
|
|
272
|
+
}, true);
|
|
273
|
+
/* Collapse when every duration allows the same resolutions, which is the
|
|
274
|
+
common case. Spelling out "3s → 720/1080, 4s → 720/1080, …" eight
|
|
275
|
+
times says the same thing eight times and buries the models where the
|
|
276
|
+
pairing genuinely differs. */
|
|
277
|
+
const pairs = Object.entries(asRecord(opt('duration')))
|
|
278
|
+
.map(([d, v]) => [d, asArray(v).map(String).filter((s) => s !== '')])
|
|
279
|
+
.filter(([, v]) => v.length > 0);
|
|
280
|
+
if (pairs.length) {
|
|
281
|
+
/* m_resolution is REQUIRED here, and it is a separate wire field
|
|
282
|
+
from m_resolution_quality above.
|
|
283
|
+
The validator looks up the chosen
|
|
284
|
+
duration's list and rejects the submit when the list is
|
|
285
|
+
non-empty and m_resolution is absent.
|
|
286
|
+
|
|
287
|
+
This block used to emit the note alone. That produced a schema
|
|
288
|
+
that contradicted itself — the note told the agent resolution
|
|
289
|
+
had to be 720 or 1080, while additionalProperties:false forbade
|
|
290
|
+
sending it — and every generate on such a model 422'd. It
|
|
291
|
+
affected roughly half the catalogue. Nothing caught it because
|
|
292
|
+
schema.test.mjs only checks the schema against itself, never
|
|
293
|
+
against what the server requires.
|
|
294
|
+
|
|
295
|
+
The enum is the union across durations; which subset applies to
|
|
296
|
+
the chosen duration is a cross-field rule JSON Schema cannot
|
|
297
|
+
express, so the note carries it.
|
|
298
|
+
|
|
299
|
+
This block used to say reusing the clean name `resolution` was
|
|
300
|
+
"safe: measured, zero models carry both a populated
|
|
301
|
+
m_resolution_quality and a populated per-duration list". The
|
|
302
|
+
measurement was true and the conclusion was still wrong — one row
|
|
303
|
+
is all it takes to end it, and `test-i2i` was that row. The name
|
|
304
|
+
is no longer shared (see CLEAN_NAME), and add() now refuses a
|
|
305
|
+
duplicate outright, so this no longer rests on a count. */
|
|
306
|
+
const union = [...new Set(pairs.flatMap(([, v]) => v))];
|
|
307
|
+
const distinct = new Set(pairs.map(([, v]) => v.join('/')));
|
|
308
|
+
const uniform = distinct.size === 1 && pairs.length === durations.length;
|
|
309
|
+
/* The enum is the UNION across durations, which on a non-uniform
|
|
310
|
+
model offers a value the chosen duration forbids. JSON Schema
|
|
311
|
+
draft-07 cannot express the dependency in a way clients reliably
|
|
312
|
+
honour, so the pairing goes in the property's OWN description —
|
|
313
|
+
the text the agent reads while filling this exact field —
|
|
314
|
+
instead of only in `notes`, which it may never reach.
|
|
315
|
+
Measured 2026-09-07: 55 models pair differently per duration. */
|
|
316
|
+
const pairing = pairs.map(([d, v]) => `${d}s → ${v.join('/')}`).join(', ');
|
|
317
|
+
add('resolution', 'm_resolution', {
|
|
318
|
+
type: 'string',
|
|
319
|
+
enum: union,
|
|
320
|
+
description: uniform
|
|
321
|
+
? 'Output resolution.'
|
|
322
|
+
: `Output resolution. NOT every value is valid at every duration — ${pairing}. ` +
|
|
323
|
+
'Pick the one that matches the duration you chose, or the submit is refused.',
|
|
324
|
+
}, true);
|
|
325
|
+
notes.push(uniform
|
|
326
|
+
? `resolution must be one of ${[...distinct][0]} at any duration`
|
|
327
|
+
: `resolution depends on the duration you pick: ${pairing}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/* The remaining three fixed lists, each enforced by the same
|
|
331
|
+
`isset && is_array && count > 0` → required rule as aspect_ratio:
|
|
332
|
+
m_output_format 68 models
|
|
333
|
+
m_movement_amplitude 13 models
|
|
334
|
+
m_style 9 models
|
|
335
|
+
All three were missing until 2026-09-07. */
|
|
336
|
+
const formats = [
|
|
337
|
+
...new Set(
|
|
338
|
+
// Keyed by media type ("image": ["jpeg","png"]); the validator
|
|
339
|
+
// flattens across every type and de-duplicates, so the
|
|
340
|
+
// agent is offered the same flat set the server will compare against.
|
|
341
|
+
Object.values(asRecord(opt('output_format')))
|
|
342
|
+
.flatMap((v) => asArray(v).map(String))
|
|
343
|
+
.filter((s) => s.trim() !== '')),
|
|
344
|
+
];
|
|
345
|
+
if (formats.length) {
|
|
346
|
+
add('output_format', 'm_output_format', {
|
|
347
|
+
type: 'string',
|
|
348
|
+
enum: formats,
|
|
349
|
+
description: 'File format of the output.',
|
|
350
|
+
}, true);
|
|
351
|
+
}
|
|
352
|
+
const amplitude = asArray(opt('movement_amplitude')).map(String).filter((s) => s !== '');
|
|
353
|
+
if (amplitude.length) {
|
|
354
|
+
add('movement_amplitude', 'm_movement_amplitude', {
|
|
355
|
+
type: 'string',
|
|
356
|
+
enum: amplitude,
|
|
357
|
+
description: 'How much motion to apply.',
|
|
358
|
+
}, true);
|
|
359
|
+
}
|
|
360
|
+
const style = asArray(opt('style')).map(String).filter((s) => s !== '');
|
|
361
|
+
if (style.length) {
|
|
362
|
+
add('style', 'm_style', {
|
|
363
|
+
type: 'string',
|
|
364
|
+
enum: style,
|
|
365
|
+
description: 'Visual style of the output.',
|
|
366
|
+
}, true);
|
|
367
|
+
}
|
|
368
|
+
const outputNumber = num(opt('output_number')) ?? 1;
|
|
369
|
+
if (outputNumber > 1) {
|
|
370
|
+
add('output_number', 'm_output_number', {
|
|
371
|
+
type: 'integer',
|
|
372
|
+
minimum: 1,
|
|
373
|
+
maximum: outputNumber,
|
|
374
|
+
default: 1,
|
|
375
|
+
description: `How many outputs to generate (up to ${outputNumber}). Each one is charged.`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/* ── the boolean switches ────────────────────────────────────────────
|
|
379
|
+
The option is a CAPABILITY flag ("this model can do it"), and the input
|
|
380
|
+
it unlocks is what goes in the schema. m_seed is the odd one: the flag
|
|
381
|
+
is boolean but the value the server wants is a number. */
|
|
382
|
+
for (const [flag, name, wireName, description] of [
|
|
383
|
+
['generate_audio', 'generate_audio', 'm_generate_audio', 'Generate an audio track with the video.'],
|
|
384
|
+
['generate_sound_effect', 'generate_sound_effect', 'm_generate_sound_effect', 'Add sound effects.'],
|
|
385
|
+
['camera_fixed', 'camera_fixed', 'm_camera_fixed', 'Keep the camera still.'],
|
|
386
|
+
['enhance_prompt', 'enhance_prompt', 'm_enhance_prompt', 'Let the provider rewrite the prompt for better results.'],
|
|
387
|
+
]) {
|
|
388
|
+
if (isTrue(opt(flag))) {
|
|
389
|
+
add(name, wireName, { type: 'boolean', description });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (isTrue(opt('seed'))) {
|
|
393
|
+
add('seed', 'm_seed', {
|
|
394
|
+
type: 'integer',
|
|
395
|
+
description: 'Seed for a reproducible result. Omit for a random one.',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/* ── dynamic fields ──────────────────────────────────────────────────
|
|
399
|
+
Per-model, admin-defined, open-ended by design. Their `name` is already
|
|
400
|
+
the wire name (it must match ^m_[A-Za-z0-9_]{1,64}$ server-side), so the
|
|
401
|
+
clean name is that with the m_ removed. */
|
|
402
|
+
for (const raw of asArray(opt('dynamic_fields'))) {
|
|
403
|
+
const f = asRecord(raw);
|
|
404
|
+
const wireName = String(f['name'] ?? '');
|
|
405
|
+
if (!/^m_[A-Za-z0-9_]{1,64}$/.test(wireName))
|
|
406
|
+
continue; // not a field the server would accept
|
|
407
|
+
const cleanName = cleanNameFor(wireName);
|
|
408
|
+
if (properties[cleanName])
|
|
409
|
+
continue; // a base field already claimed it
|
|
410
|
+
const type = String(f['type'] ?? '');
|
|
411
|
+
const label = plain(f['label']) || cleanName;
|
|
412
|
+
const description = [label, plain(f['description'])].filter(Boolean).join(' — ');
|
|
413
|
+
const isRequired = f['required'] === true;
|
|
414
|
+
/* is_label options are GROUP HEADERS in the UI, not values. 19 of them
|
|
415
|
+
exist in the catalogue; letting one into an enum would offer the
|
|
416
|
+
agent a choice the server refuses. */
|
|
417
|
+
const choices = asArray(f['options'])
|
|
418
|
+
.map(asRecord)
|
|
419
|
+
.filter((o) => !isTrue(o['is_label']))
|
|
420
|
+
.map((o) => String(o['value'] ?? ''))
|
|
421
|
+
.filter((s) => s !== '');
|
|
422
|
+
if (type === 'file_upload_image' || type === 'file_upload_video' || type === 'file_upload_audio') {
|
|
423
|
+
files.push({
|
|
424
|
+
name: cleanName,
|
|
425
|
+
wire: wireName,
|
|
426
|
+
required: isRequired,
|
|
427
|
+
...resolveUploadRules(f['value'], f['max_size'], type.replace('file_upload_', '')),
|
|
428
|
+
maxDurationSec: null,
|
|
429
|
+
description,
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (type === 'toggle' || type === 'checkbox') {
|
|
434
|
+
const prop = { type: 'boolean', description };
|
|
435
|
+
if (f['default'] !== undefined && f['default'] !== '')
|
|
436
|
+
prop.default = isTrue(f['default']);
|
|
437
|
+
add(cleanName, wireName, prop, isRequired);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (type === 'slider') {
|
|
441
|
+
const prop = { type: 'number', description };
|
|
442
|
+
const min = num(f['min']);
|
|
443
|
+
const max = num(f['max']);
|
|
444
|
+
const step = num(f['step']);
|
|
445
|
+
if (min !== null)
|
|
446
|
+
prop.minimum = min;
|
|
447
|
+
if (max !== null)
|
|
448
|
+
prop.maximum = max;
|
|
449
|
+
if (step !== null && step > 0)
|
|
450
|
+
prop.multipleOf = step;
|
|
451
|
+
const dflt = num(f['default']);
|
|
452
|
+
if (dflt !== null)
|
|
453
|
+
prop.default = dflt;
|
|
454
|
+
add(cleanName, wireName, prop, isRequired);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
// select · radio_group · text · textarea · and anything added tomorrow.
|
|
458
|
+
const prop = { type: 'string', description };
|
|
459
|
+
if (choices.length)
|
|
460
|
+
prop.enum = choices;
|
|
461
|
+
const dflt = f['default'];
|
|
462
|
+
if (dflt !== undefined && dflt !== '' && (!choices.length || choices.includes(String(dflt)))) {
|
|
463
|
+
prop.default = String(dflt);
|
|
464
|
+
}
|
|
465
|
+
add(cleanName, wireName, prop, isRequired);
|
|
466
|
+
}
|
|
467
|
+
/* ── single-file inputs ──────────────────────────────────────────────── */
|
|
468
|
+
for (const [flag, name, wireName, settingsKey] of [
|
|
469
|
+
['upload_image', 'image', 'm_image', 'upload_image_settings'],
|
|
470
|
+
['upload_video', 'video', 'm_video', 'upload_video_settings'],
|
|
471
|
+
['upload_audio', 'audio', 'm_audio', 'upload_audio_settings'],
|
|
472
|
+
]) {
|
|
473
|
+
if (!isTrue(opt(flag)))
|
|
474
|
+
continue;
|
|
475
|
+
const s = asRecord(opt(settingsKey));
|
|
476
|
+
files.push({
|
|
477
|
+
name,
|
|
478
|
+
wire: wireName,
|
|
479
|
+
required: true, // the model asked for this input; it is not optional
|
|
480
|
+
...resolveUploadRules(s['allowed_extensions'], s['max_size'], name),
|
|
481
|
+
maxDurationSec: num(opt(`max_duration_${name}`)),
|
|
482
|
+
description: plain(s['label']) || `The ${name} to work from.`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (isTrue(opt('upload_frames'))) {
|
|
486
|
+
const fs = asRecord(opt('upload_frames_settings'));
|
|
487
|
+
for (const slot of ['first_frame', 'last_frame']) {
|
|
488
|
+
const s = asRecord(fs[slot]);
|
|
489
|
+
files.push({
|
|
490
|
+
name: slot,
|
|
491
|
+
wire: `m_${slot}`,
|
|
492
|
+
required: true,
|
|
493
|
+
// A frame slot is always an image.
|
|
494
|
+
...resolveUploadRules(s['allowed_extensions'], s['max_size'], 'image'),
|
|
495
|
+
maxDurationSec: null,
|
|
496
|
+
description: plain(s['label']) || `The ${slot.replace('_', ' ')}.`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/* ── multi-upload: two families, DIFFERENT field names ───────────────
|
|
501
|
+
A `slots` model REJECTS the legacy name outright, so getting this wrong is not a
|
|
502
|
+
degraded experience, it is a hard refusal. */
|
|
503
|
+
const multi = asRecord(opt('multi_upload'));
|
|
504
|
+
if (isTrue(multi['active'])) {
|
|
505
|
+
const slots = asRecord(multi['slots']);
|
|
506
|
+
if (Object.keys(slots).length > 0) {
|
|
507
|
+
for (const [slotType, rawSlot] of Object.entries(slots)) {
|
|
508
|
+
const s = asRecord(rawSlot);
|
|
509
|
+
const count = num(s['number']) ?? 0;
|
|
510
|
+
const minMedia = num(s['min_media']) ?? 0;
|
|
511
|
+
if (count <= 0)
|
|
512
|
+
continue;
|
|
513
|
+
files.push({
|
|
514
|
+
name: `${slotType}_files`,
|
|
515
|
+
wire: `m_multi_${slotType}_file_N`,
|
|
516
|
+
required: minMedia > 0,
|
|
517
|
+
...resolveUploadRules(s['allowed_extensions'], s['max_size'], slotType),
|
|
518
|
+
maxDurationSec: num(s['max_duration']),
|
|
519
|
+
description: `${plain(s['label']) || `${slotType} files`} — up to ${count}` +
|
|
520
|
+
(minMedia > 0 ? `, at least ${minMedia} required` : '') +
|
|
521
|
+
`. Numbered from 0: m_multi_${slotType}_file_0, _1, …`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
notes.push('this model uses PER-TYPE upload fields (m_multi_<type>_file_N); the plain m_multi_file_N is refused');
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
const slotType = String(multi['type'] ?? 'image');
|
|
528
|
+
const count = num(multi['number']) ?? 0;
|
|
529
|
+
const minMedia = num(multi['min_media']) ?? 0;
|
|
530
|
+
if (count > 0) {
|
|
531
|
+
files.push({
|
|
532
|
+
name: `${slotType}_files`,
|
|
533
|
+
wire: 'm_multi_file_N',
|
|
534
|
+
required: minMedia > 0,
|
|
535
|
+
...resolveUploadRules(multi['allowed_extensions'], multi['max_size'], slotType),
|
|
536
|
+
maxDurationSec: num(multi['max_duration']),
|
|
537
|
+
description: `Up to ${count} ${slotType} files` +
|
|
538
|
+
(minMedia > 0 ? `, at least ${minMedia} required` : '') +
|
|
539
|
+
'. Numbered from 0: m_multi_file_0, _1, …',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (isTrue(multi['paid_uploads'])) {
|
|
544
|
+
const free = num(multi['free_count']) ?? 0;
|
|
545
|
+
notes.push(`files beyond the first ${free} are charged extra — call estimate_cost with the real count`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/* See ModelSchema.billedByUploadDuration for where this value comes from
|
|
549
|
+
* and why the served field is the right source rather than the column. */
|
|
550
|
+
const billedByUploadDuration = String(opt('pricing_mode') ?? '') === 'per_second';
|
|
551
|
+
if (billedByUploadDuration) {
|
|
552
|
+
notes.push('billed by the DURATION of the media you upload — estimate_cost prices the ' +
|
|
553
|
+
'settings only, so the real charge is higher than its figure');
|
|
554
|
+
}
|
|
555
|
+
/* Mark which slots can be chained, derived once from the wire name rather
|
|
556
|
+
* than repeated at each of the five construction sites above.
|
|
557
|
+
*
|
|
558
|
+
* The list is the server's, copied from the allow-list the submit path
|
|
559
|
+
* applies to a reuse map: the fixed
|
|
560
|
+
* image/video/audio slots under either spelling, the two frame slots,
|
|
561
|
+
* m_multi_file_<n>, and every dynamic file field by its own name. A field
|
|
562
|
+
* outside it is dropped from regen_source_map without a word, so the job
|
|
563
|
+
* runs with that input missing.
|
|
564
|
+
*
|
|
565
|
+
* m_multi_<type>_file_N is the notable exclusion — a server-side gap, not a
|
|
566
|
+
* client one; the studio cannot chain those slots either. Saying so here
|
|
567
|
+
* turns a wasted submit into a readable capability flag. */
|
|
568
|
+
const REUSABLE_FIXED = new Set([
|
|
569
|
+
'm_image', 'm_input_image',
|
|
570
|
+
'm_video', 'm_input_video',
|
|
571
|
+
'm_audio', 'm_input_audio',
|
|
572
|
+
'm_first_frame', 'm_last_frame',
|
|
573
|
+
]);
|
|
574
|
+
const filesOut = files.map((f) => ({
|
|
575
|
+
...f,
|
|
576
|
+
/* Say the length cap out loud.
|
|
577
|
+
*
|
|
578
|
+
* Nine models cap what you may upload, and the admin's label is all
|
|
579
|
+
* the agent used to get — "Video:" on kling-lipsync, which caps video
|
|
580
|
+
* at 30 seconds. Worse on exactly those models: they are the
|
|
581
|
+
* upload-billed ones, so the agent is told "you are billed by the
|
|
582
|
+
* duration of the media you upload" and never told where the ceiling
|
|
583
|
+
* is. Now it can pick a clip that fits, and tell the user the limit
|
|
584
|
+
* before asking them for a file.
|
|
585
|
+
*
|
|
586
|
+
* Appended here rather than at each of the four places a slot is
|
|
587
|
+
* built, so a fifth cannot be added without it. */
|
|
588
|
+
description: f.maxDurationSec !== null && f.maxDurationSec > 0
|
|
589
|
+
? `${f.description} Up to ${f.maxDurationSec} seconds.`
|
|
590
|
+
: f.description,
|
|
591
|
+
supportsReuse: REUSABLE_FIXED.has(f.wire) ||
|
|
592
|
+
f.wire === 'm_multi_file_N' ||
|
|
593
|
+
// A dynamic file field is whitelisted under its own name, and its
|
|
594
|
+
// wire IS that name — any m_* field that is neither a fixed slot
|
|
595
|
+
// nor one of the numbered multi families.
|
|
596
|
+
(/^m_[a-z0-9_]+$/i.test(f.wire) && !/^m_multi_/.test(f.wire)),
|
|
597
|
+
}));
|
|
598
|
+
return {
|
|
599
|
+
model_key: String(mf('model_key') ?? ''),
|
|
600
|
+
effect_key: String(mf('effect_key') ?? ''),
|
|
601
|
+
slug: String(mf('slug') ?? root['slug'] ?? ''),
|
|
602
|
+
name: String(mf('name') ?? ''),
|
|
603
|
+
icon: String(mf('icon') ?? ''),
|
|
604
|
+
/* TWO mode values, because the agent and the wire want different ones
|
|
605
|
+
* and conflating them breaks one of the two.
|
|
606
|
+
*
|
|
607
|
+
* `mode` — the SHORT code (t2v), which is what the agent sees and what
|
|
608
|
+
* list_models accepts. Measured 2026-09-07: /info/models-flat/t2v
|
|
609
|
+
* answers, /info/models-flat/text-to-video is INVALID_MODE. This field
|
|
610
|
+
* used to carry the long key, so an agent that took `mode` out of
|
|
611
|
+
* get_model and passed it to list_models got an error, while the same
|
|
612
|
+
* field name coming from list_modes worked.
|
|
613
|
+
*
|
|
614
|
+
* `mode_wire` — the LONG key (text-to-video), which is what m_mode
|
|
615
|
+
* means on submit and model-credits, and what the media row stores.
|
|
616
|
+
* Never show this to the agent; never post the other one. */
|
|
617
|
+
mode: String(root['mode_code'] ?? mf('mode_code') ?? ''),
|
|
618
|
+
mode_wire: String(mf('mode') ?? root['mode'] ?? ''),
|
|
619
|
+
media_type: String(mf('media_type') ?? ''),
|
|
620
|
+
credits_from: num(mf('coins')) ?? num(mf('credits')),
|
|
621
|
+
estimated_seconds: num(mf('sec')) ?? num(mf('estimated_seconds')),
|
|
622
|
+
inputSchema: { type: 'object', properties, required, additionalProperties: false },
|
|
623
|
+
wire,
|
|
624
|
+
files: filesOut,
|
|
625
|
+
billedByUploadDuration,
|
|
626
|
+
notes,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account tools — what the balance is, and where it went.
|
|
3
|
+
*
|
|
4
|
+
* Both read-only. Neither can move money: the endpoints that could (checkout,
|
|
5
|
+
* auto-topup, purchases) are deliberately not exposed by this package at all,
|
|
6
|
+
* so an agent cannot buy coins on the user's behalf however it is prompted.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { Config } from '../config.js';
|
|
10
|
+
export declare function getBalance(cfg: Config): Promise<unknown>;
|
|
11
|
+
export declare const getUsageInput: z.ZodObject<{
|
|
12
|
+
days: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
days?: number | undefined;
|
|
17
|
+
limit?: number | undefined;
|
|
18
|
+
offset?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
days?: number | undefined;
|
|
21
|
+
limit?: number | undefined;
|
|
22
|
+
offset?: number | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function getUsage(cfg: Config, args: {
|
|
25
|
+
days?: number | undefined;
|
|
26
|
+
limit?: number | undefined;
|
|
27
|
+
offset?: number | undefined;
|
|
28
|
+
}): Promise<unknown>;
|