@slatesvideo/shared 0.5.8 → 0.5.10

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.
@@ -0,0 +1,117 @@
1
+ /** Every aspect ratio any Slates model accepts. There is no `9:21`. */
2
+ export type AspectRatio = '1:1' | '2:3' | '3:2' | '3:4' | '4:3' | '4:5' | '5:4' | '9:16' | '16:9' | '21:9';
3
+ export type VideoResolution = '480p' | '720p' | '1080p' | '4k';
4
+ /**
5
+ * The full ten, in display order. `9:21` was in the MCP op's enum and in NO
6
+ * model — it was invented downstream. Do not add a ratio here that no model
7
+ * declares; the op's enum is generated from the union of what models accept, so
8
+ * a phantom entry here becomes a phantom entry an agent can pass.
9
+ */
10
+ export declare const ALL_ASPECT_RATIOS: AspectRatio[];
11
+ /** Duration constraints for a video model. */
12
+ export interface DurationCapability {
13
+ min: number;
14
+ max: number;
15
+ /** 'continuous' = every whole second from min to max; 'discrete' = `values` only. */
16
+ mode: 'continuous' | 'discrete';
17
+ /** For discrete mode: the exact allowed durations. */
18
+ values?: number[];
19
+ /** Resolution-dependent narrowing (Veo forces 8s at 1080p AND 4k). */
20
+ resolutionOverrides?: Record<string, Pick<DurationCapability, 'min' | 'max' | 'mode' | 'values'>>;
21
+ /** Prompt-mode narrowing (Veo's reference-to-video endpoint is 8s only). */
22
+ modeOverrides?: Record<string, Pick<DurationCapability, 'min' | 'max' | 'mode' | 'values'>>;
23
+ }
24
+ /** Video resolution constraints. */
25
+ export interface VideoResolutionCapability {
26
+ options: VideoResolution[];
27
+ /** Set when the resolution is not selectable at all (Omni Flash is 720p, full stop). */
28
+ fixed?: VideoResolution;
29
+ /** Default when this model is chosen (falls back to `options[0]`). */
30
+ default?: VideoResolution;
31
+ }
32
+ /** Everything a model will ACCEPT. Capability only — never a price. */
33
+ export interface ModelCapability {
34
+ aspectRatios: AspectRatio[];
35
+ /** Provider-keyed overrides. `fal` is the one that matters — see AGENT_ROUTE_PROVIDER. */
36
+ providerAspectRatios?: Record<string, AspectRatio[]>;
37
+ videoResolution?: VideoResolutionCapability;
38
+ duration?: DurationCapability;
39
+ /** Max reference images in create-image mode (image models). */
40
+ maxRefImages?: number;
41
+ /** Max ingredient / free reference images (video models). */
42
+ maxIngredientImages?: number;
43
+ /** Reference VIDEOS accepted. Absent/0 = none. */
44
+ maxReferenceVideos?: number;
45
+ /** Reference AUDIO clips accepted. Absent/0 = none. */
46
+ maxReferenceAudio?: number;
47
+ /** Ceiling on TOTAL reference files across ALL modalities. */
48
+ maxReferenceFilesTotal?: number;
49
+ /** Combined seconds across every reference video. */
50
+ maxReferenceVideoSeconds?: number;
51
+ /** Combined seconds across every reference audio clip. */
52
+ maxReferenceAudioSeconds?: number;
53
+ }
54
+ /**
55
+ * The provider every AGENT generation actually lands on for Kling and Veo.
56
+ *
57
+ * 🚨 THIS IS WHY `providerAspectRatios` MATTERS TO THE OP. MCP/CLI/Studio-Agent
58
+ * generations are credits-only (BYOK is retired on the agent surface), and the
59
+ * credits route carries Kling and Veo on fal: `slate/src/main/agent/routes.ts`
60
+ * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`,
61
+ * and `generateVeoVideo`'s proxy arm builds a fal request
62
+ * (`buildFalVeoRequest`). So an agent gets Kling's THREE fal ratios and Veo's
63
+ * TWO — not the eight and ten those models take on their direct APIs. Validating
64
+ * against the direct sets would accept a ratio fal rejects, which is the exact
65
+ * failure this module exists to delete.
66
+ */
67
+ export declare const AGENT_ROUTE_PROVIDER = "fal";
68
+ export declare const MODEL_CAPABILITIES: Record<string, ModelCapability>;
69
+ export declare function getModelCapability(model: string): ModelCapability | undefined;
70
+ /** Aspect ratios a model accepts, honouring the provider override. */
71
+ export declare function aspectRatiosFor(model: string, provider?: string): AspectRatio[];
72
+ /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
73
+ export declare function videoResolutionsFor(model: string): VideoResolution[];
74
+ /** The resolution a model would actually run at. Fixed wins; else keep a legal
75
+ * current value; else the model's own default. Mirrors `clampVideoResolution`. */
76
+ export declare function defaultVideoResolutionFor(model: string): VideoResolution | undefined;
77
+ /**
78
+ * Duration constraints after applying overrides.
79
+ *
80
+ * ⚠️ ORDER IS LOAD-BEARING and mirrors `getAvailableDurations` in
81
+ * slate/src/shared/pricing.ts EXACTLY: mode override first (more specific),
82
+ * resolution override only if no mode override applied. Reversing them would
83
+ * make the desktop and the agent disagree about the same generation.
84
+ */
85
+ export declare function durationsFor(model: string, opts?: {
86
+ videoResolution?: string;
87
+ promptMode?: string;
88
+ }): DurationCapability | undefined;
89
+ /** Every legal whole-second duration. Mirrors `getAvailableDurations`. */
90
+ export declare function durationValuesFor(model: string, opts?: {
91
+ videoResolution?: string;
92
+ promptMode?: string;
93
+ }): number[];
94
+ /** Union of every ratio the given models accept — the legal universe for an enum. */
95
+ export declare function aspectRatioUnion(models: readonly string[], provider?: string): AspectRatio[];
96
+ /** Union of every resolution the given models accept. */
97
+ export declare function videoResolutionUnion(models: readonly string[]): VideoResolution[];
98
+ /** Widest legal duration window across the given models, overrides included. */
99
+ export declare function durationBounds(models: readonly string[]): {
100
+ min: number;
101
+ max: number;
102
+ };
103
+ export declare function checkAspectRatio(model: string, aspectRatio: string | undefined, provider?: string): string | null;
104
+ export declare function checkVideoResolution(model: string, videoResolution: string | undefined): string | null;
105
+ export declare function checkDuration(model: string, duration: number | undefined, opts?: {
106
+ videoResolution?: string;
107
+ promptMode?: string;
108
+ }): string | null;
109
+ /** e.g. "kling-v3.0-std/kling-v3.0-pro: 16:9, 9:16, 1:1 · seedance-2: 21:9, …" */
110
+ export declare function describeAspectRatios(models: readonly string[], provider?: string): string;
111
+ /** e.g. "seedance-2: 480p, 720p, 1080p, 4k (default 1080p) · omni-flash: 720p only (fixed)" */
112
+ export declare function describeVideoResolutions(models: readonly string[]): string;
113
+ /** e.g. "kling-v3.0-std: 3-15s · veo-3.1-fast: 4s/6s/8s (1080p/4k: 8s only; with reference images: 8s only)" */
114
+ export declare function describeDurations(models: readonly string[]): string;
115
+ /** e.g. "seedance-2: 9 · seedance-2.5: 30 · omni-flash: 7 · seedance-2.5-edit: 0 (prompt + source clip only)" */
116
+ export declare function describeReferenceImageCaps(models: readonly string[]): string;
117
+ //# sourceMappingURL=model-capabilities.d.ts.map
@@ -0,0 +1,536 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // MODEL_CAPABILITIES — the SSOT for what a model will ACCEPT.
3
+ //
4
+ // Aspect ratios (including per-provider overrides), video resolutions, duration
5
+ // ranges and reference caps. One definition, imported by everything: the
6
+ // desktop's `MODEL_REGISTRY` (slate/src/shared/pricing.ts) spreads these fields
7
+ // into every entry, `MODEL_FACTS` derives its reference caps from them, and the
8
+ // MCP/CLI op surface both VALIDATES against them and GENERATES its `.describe()`
9
+ // prose from them.
10
+ //
11
+ // 🚨 WHY THIS FILE EXISTS. Until 2026-08-16 the op surface in
12
+ // `operations/index.ts` re-stated all of these constraints by hand, as flat Zod
13
+ // enums plus English prose, with no link of any kind back to the registry. It
14
+ // had drifted on every axis: an aspect-ratio enum offering `9:21` (a value that
15
+ // exists in NO model, invented here), "Kling/Seedance support all" when Seedance
16
+ // takes 6 of 11 and Kling-on-fal takes 3, "Veo locks to 16:9" when Veo on fal
17
+ // takes two, "Kling: 5-15" against a registry minimum of 3, and a
18
+ // `videoResolution` description that never mentioned Kling at all. A customer
19
+ // burned a round trip on 2026-08-16 passing `4:5` to Seedance: the op accepted
20
+ // it, the job queued, credits reserved, and the provider rejected it
21
+ // ASYNCHRONOUSLY. Client-side accept of a server-side reject is the worst shape
22
+ // a constraint bug can take.
23
+ //
24
+ // The fix is NOT a fourth mirror plus a fifth lockstep checker — a checker only
25
+ // proves two hand-written copies agree, it does not remove the second copy, and
26
+ // the second copy is the defect. So the values live HERE, once, and everything
27
+ // downstream imports them.
28
+ //
29
+ // 🚨 NEVER HAND-TYPE A CAPABILITY FACT AN LLM WILL READ. Op descriptions,
30
+ // clarification messages and skill prose all derive from the `describe*`
31
+ // helpers below. If you find yourself typing "4-15s" or "16:9/9:16" into a
32
+ // string, you are re-creating the bug this file deleted.
33
+ //
34
+ // Direction of dependency matches the settled precedent for MODEL_FACTS: this
35
+ // package owns the doctrine, slate DERIVES from the published package at
36
+ // runtime (`slate/src/main/studio-agent/context.ts` already imports
37
+ // `@slatesvideo/shared`). Rates, credit costs and cost-key builders deliberately
38
+ // did NOT move — billing stays in slate with its existing checkers.
39
+ //
40
+ // ⚠️ LEAF MODULE — no imports, no Node built-ins. The desktop RENDERER reaches
41
+ // this through `@slatesvideo/shared/prompts`, so anything pulled in here has to
42
+ // bundle for the browser.
43
+ //
44
+ // Verification that a change here is a pure relocation: the slates-api checkers
45
+ // read `MODEL_REGISTRY` and assert it against `CREDIT_COSTS` —
46
+ // `composition-matrix-check.mjs` (12,608 combinations),
47
+ // `reference-caps-lockstep-check.mjs`, `pricing-consistency-check.mjs`. If a
48
+ // value moved, they go red.
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ /**
51
+ * The full ten, in display order. `9:21` was in the MCP op's enum and in NO
52
+ * model — it was invented downstream. Do not add a ratio here that no model
53
+ * declares; the op's enum is generated from the union of what models accept, so
54
+ * a phantom entry here becomes a phantom entry an agent can pass.
55
+ */
56
+ export const ALL_ASPECT_RATIOS = [
57
+ '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '5:4', '4:5', '21:9',
58
+ ];
59
+ // ── Shared ratio sets ────────────────────────────────────────────────────────
60
+ // Named rather than inlined because several models share a set and a set is the
61
+ // thing that changes (a provider adds a ratio, every model on it gains it).
62
+ /** Google / non-restricted models: all ten. */
63
+ const FULL_ASPECT_RATIOS = ALL_ASPECT_RATIOS;
64
+ /** Kling's DIRECT API: eight — no `5:4`, no `4:5`. */
65
+ const KLING_DIRECT_ASPECT_RATIOS = [
66
+ '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '21:9',
67
+ ];
68
+ /** Kling carried on fal: three. This is the set the CREDITS route uses. */
69
+ const KLING_FAL_ASPECT_RATIOS = ['16:9', '9:16', '1:1'];
70
+ /** Veo carried on fal: two. The credits route again — Veo direct takes all ten. */
71
+ const VEO_FAL_ASPECT_RATIOS = ['16:9', '9:16'];
72
+ /** Gemini Omni Flash (fal schema, 16:9 default): two. */
73
+ const OMNI_FLASH_ASPECT_RATIOS = ['16:9', '9:16'];
74
+ /** Seedance (both seats, and the edit row): six — notably NO `4:5`. */
75
+ const SEEDANCE_ASPECT_RATIOS = ['21:9', '16:9', '4:3', '1:1', '3:4', '9:16'];
76
+ /**
77
+ * The provider every AGENT generation actually lands on for Kling and Veo.
78
+ *
79
+ * 🚨 THIS IS WHY `providerAspectRatios` MATTERS TO THE OP. MCP/CLI/Studio-Agent
80
+ * generations are credits-only (BYOK is retired on the agent surface), and the
81
+ * credits route carries Kling and Veo on fal: `slate/src/main/agent/routes.ts`
82
+ * never sends `klingProvider`, so `handlers/video.ts` defaults it to `'fal'`,
83
+ * and `generateVeoVideo`'s proxy arm builds a fal request
84
+ * (`buildFalVeoRequest`). So an agent gets Kling's THREE fal ratios and Veo's
85
+ * TWO — not the eight and ten those models take on their direct APIs. Validating
86
+ * against the direct sets would accept a ratio fal rejects, which is the exact
87
+ * failure this module exists to delete.
88
+ */
89
+ export const AGENT_ROUTE_PROVIDER = 'fal';
90
+ // ── The data ─────────────────────────────────────────────────────────────────
91
+ //
92
+ // Moved VERBATIM from `MODEL_REGISTRY` in slate/src/shared/pricing.ts on
93
+ // 2026-08-16. A relocation, not a re-derivation — the three slates-api checkers
94
+ // prove it (see the header).
95
+ export const MODEL_CAPABILITIES = {
96
+ // ── Image models ───────────────────────────────────────────────────────────
97
+ 'nano-banana-2': {
98
+ aspectRatios: FULL_ASPECT_RATIOS,
99
+ maxRefImages: 14,
100
+ },
101
+ 'nano-banana-2-lite': {
102
+ aspectRatios: FULL_ASPECT_RATIOS,
103
+ maxRefImages: 4, // fal edit endpoint caps input images at 4
104
+ },
105
+ 'nano-banana-pro': {
106
+ aspectRatios: FULL_ASPECT_RATIOS,
107
+ maxRefImages: 14,
108
+ },
109
+ 'gpt-image-2': {
110
+ // FIVE, not ten. The op's flat enum offered eleven for every image model.
111
+ aspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4'],
112
+ maxRefImages: 10,
113
+ },
114
+ 'flux-2-max': {
115
+ aspectRatios: FULL_ASPECT_RATIOS,
116
+ maxRefImages: 4,
117
+ },
118
+ 'seedream-5-lite': {
119
+ aspectRatios: FULL_ASPECT_RATIOS,
120
+ maxRefImages: 10,
121
+ },
122
+ // ── Kling video ────────────────────────────────────────────────────────────
123
+ 'kling-v3.0-std': {
124
+ aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
125
+ providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
126
+ videoResolution: { options: ['1080p', '4k'] },
127
+ // 3, not 5. The op claimed "Kling: 5-15" and refused legal 3-4s takes.
128
+ duration: { min: 3, max: 15, mode: 'continuous' },
129
+ maxIngredientImages: 4,
130
+ },
131
+ 'kling-v3.0-pro': {
132
+ aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
133
+ providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
134
+ videoResolution: { options: ['1080p', '4k'] },
135
+ duration: { min: 3, max: 15, mode: 'continuous' },
136
+ maxIngredientImages: 4,
137
+ },
138
+ 'kling-v3.0-omni': {
139
+ aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
140
+ providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
141
+ videoResolution: { options: ['1080p', '4k'] },
142
+ duration: { min: 3, max: 15, mode: 'continuous' },
143
+ maxIngredientImages: 4,
144
+ },
145
+ 'kling-v3.0-omni-pro': {
146
+ aspectRatios: KLING_DIRECT_ASPECT_RATIOS,
147
+ providerAspectRatios: { fal: KLING_FAL_ASPECT_RATIOS },
148
+ videoResolution: { options: ['1080p', '4k'] },
149
+ duration: { min: 3, max: 15, mode: 'continuous' },
150
+ maxIngredientImages: 4,
151
+ },
152
+ // Kling O3 video-to-video edit (fal-only; the source clip is the canvas, so
153
+ // aspect/resolution/duration all follow it).
154
+ 'kling-v3.0-omni-edit': {
155
+ aspectRatios: KLING_FAL_ASPECT_RATIOS,
156
+ videoResolution: { options: ['1080p'], fixed: '1080p' },
157
+ duration: { min: 3, max: 15, mode: 'continuous' },
158
+ maxIngredientImages: 4, // elements + style refs combined (fal cap)
159
+ },
160
+ 'kling-v3.0-omni-pro-edit': {
161
+ aspectRatios: KLING_FAL_ASPECT_RATIOS,
162
+ videoResolution: { options: ['1080p'], fixed: '1080p' },
163
+ duration: { min: 3, max: 15, mode: 'continuous' },
164
+ maxIngredientImages: 4,
165
+ },
166
+ // ── Gemini Omni Flash ──────────────────────────────────────────────────────
167
+ 'omni-flash': {
168
+ aspectRatios: OMNI_FLASH_ASPECT_RATIOS,
169
+ videoResolution: { options: ['720p'], fixed: '720p' },
170
+ duration: { min: 3, max: 10, mode: 'continuous' },
171
+ maxIngredientImages: 7,
172
+ },
173
+ 'omni-flash-edit': {
174
+ aspectRatios: OMNI_FLASH_ASPECT_RATIOS,
175
+ videoResolution: { options: ['720p'], fixed: '720p' },
176
+ duration: { min: 3, max: 10, mode: 'continuous' },
177
+ maxIngredientImages: 0,
178
+ },
179
+ // ── Veo ────────────────────────────────────────────────────────────────────
180
+ 'veo-3.1-fast': {
181
+ aspectRatios: FULL_ASPECT_RATIOS,
182
+ providerAspectRatios: { fal: VEO_FAL_ASPECT_RATIOS },
183
+ videoResolution: { options: ['720p', '1080p', '4k'] },
184
+ duration: {
185
+ min: 4, max: 8, mode: 'discrete',
186
+ values: [4, 6, 8],
187
+ // BOTH 1080p and 4k force 8s. The op said "4K only at 8s" and quoted 4s
188
+ // at 1080p, which the provider rejects.
189
+ resolutionOverrides: {
190
+ '1080p': { min: 8, max: 8, mode: 'discrete', values: [8] },
191
+ '4k': { min: 8, max: 8, mode: 'discrete', values: [8] },
192
+ },
193
+ modeOverrides: {
194
+ ingredients: { min: 8, max: 8, mode: 'discrete', values: [8] },
195
+ },
196
+ },
197
+ maxIngredientImages: 3,
198
+ },
199
+ 'veo-3.1-standard': {
200
+ aspectRatios: FULL_ASPECT_RATIOS,
201
+ providerAspectRatios: { fal: VEO_FAL_ASPECT_RATIOS },
202
+ videoResolution: { options: ['720p', '1080p', '4k'] },
203
+ duration: {
204
+ min: 4, max: 8, mode: 'discrete',
205
+ values: [4, 6, 8],
206
+ resolutionOverrides: {
207
+ '1080p': { min: 8, max: 8, mode: 'discrete', values: [8] },
208
+ '4k': { min: 8, max: 8, mode: 'discrete', values: [8] },
209
+ },
210
+ modeOverrides: {
211
+ ingredients: { min: 8, max: 8, mode: 'discrete', values: [8] },
212
+ },
213
+ },
214
+ maxIngredientImages: 3,
215
+ },
216
+ // ── Seedance ───────────────────────────────────────────────────────────────
217
+ 'seedance-2': {
218
+ aspectRatios: SEEDANCE_ASPECT_RATIOS,
219
+ videoResolution: { options: ['480p', '720p', '1080p', '4k'], default: '1080p' },
220
+ duration: { min: 4, max: 15, mode: 'continuous' },
221
+ maxIngredientImages: 9,
222
+ maxReferenceVideos: 3,
223
+ maxReferenceAudio: 3,
224
+ // 12, and it is the FAL ceiling — not a BytePlus one. SETTLED 2026-08-10
225
+ // against both providers' primary sources; do not "correct" it to 15.
226
+ //
227
+ // BytePlus ModelArk (first party, docs → Multimodal reference):
228
+ // "You can combine the following modal content as needed…
229
+ // Images: 0–9 images · Videos: 0–3 videos · Audio: 0–3 audios"
230
+ // Per-arm ranges, combined AS NEEDED. No total is stated anywhere, so
231
+ // on BytePlus the effective maximum really is 9+3+3 = 15.
232
+ // fal live OpenAPI (bytedance/seedance-2.0/reference-to-video):
233
+ // same per-arm maxItems 9/3/3, PLUS an explicit
234
+ // "Total files across all modalities must not exceed 12."
235
+ // EvoLink (the third route): publishes NO numeric reference limits at
236
+ // all — checked 2026-08-10. Genuinely unknown, not assumed to be 15.
237
+ //
238
+ // So the providers that DO state a total disagree, and 12 binds because
239
+ // **a single generation can change providers after the user has approved
240
+ // it**: the real-face consent cascade resubmits an EvoLink rejection to fal
241
+ // mid-flight. A 15-file composition would be quoted, accepted, rejected by
242
+ // ByteDance's real-person classifier, re-quoted through the consent
243
+ // interstitial, and only THEN refused by fal for a reason the user was
244
+ // never shown. 12 is the minimum of the two documented ceilings, with the
245
+ // third unknown — so it is a floor on what is safe, not a proven optimum.
246
+ //
247
+ // The older "the modalities trade against each other" reading was a guess
248
+ // at why fal states 12; it is not what BytePlus documents. The "15" in the
249
+ // 2.5 plan's capability table was a SUM, not a figure anyone read.
250
+ // (2.5 is unaffected: fal states 50 and 30+10+10 = 50, so both agree.)
251
+ maxReferenceFilesTotal: 12,
252
+ maxReferenceVideoSeconds: 15,
253
+ maxReferenceAudioSeconds: 15,
254
+ },
255
+ 'seedance-2.5': {
256
+ aspectRatios: SEEDANCE_ASPECT_RATIOS,
257
+ // 🚨 480p/720p ONLY, on BytePlus, EvoLink AND fal. No 1080p, no 4K.
258
+ videoResolution: { options: ['480p', '720p'], default: '720p' },
259
+ duration: { min: 4, max: 30, mode: 'continuous' },
260
+ maxIngredientImages: 30,
261
+ maxReferenceVideos: 10,
262
+ maxReferenceAudio: 10,
263
+ // fal states 50 and 30+10+10 = 50, so both documented ceilings agree here.
264
+ maxReferenceFilesTotal: 50,
265
+ maxReferenceVideoSeconds: 30,
266
+ maxReferenceAudioSeconds: 30,
267
+ },
268
+ 'seedance-2.5-edit': {
269
+ aspectRatios: SEEDANCE_ASPECT_RATIOS,
270
+ videoResolution: { options: ['480p', '720p'], default: '720p' },
271
+ duration: { min: 4, max: 30, mode: 'continuous' },
272
+ // 🚨 ZERO, AND IT MUST MATCH WHAT THE HANDLER SENDS. The model's edit task
273
+ // type does accept reference images, but slate's
274
+ // `generation/handlers/edit-video.ts` sends the prompt and the source clip
275
+ // and NOTHING ELSE on this row — no `image_urls` on the EvoLink call, no
276
+ // `image_url` items in the BytePlus content array. This declared 30 while
277
+ // the handler sent 0, so attaching references produced no error, no
278
+ // warning, and no images in the request: a silent drop, which is the one
279
+ // outcome `validateComposition` exists to prevent. Both sibling edit rows
280
+ // already model this correctly (Omni Flash Edit is 0 and warns "takes the
281
+ // prompt + source clip only"; Kling O3 Edit is 4 and actually sends them).
282
+ //
283
+ // Raising it is a HANDLER change first: wire the refs, then move the cap.
284
+ maxIngredientImages: 0,
285
+ // NO multimodal reference caps, deliberately: on an edit row the clip IS the
286
+ // canvas and arrives through `sourceVideo`, not as a reference.
287
+ },
288
+ // ── Audio ──────────────────────────────────────────────────────────────────
289
+ //
290
+ // `aspectRatios: []` is deliberate, not an oversight: audio has no frame, and
291
+ // an empty list is what makes the desktop composer HIDE the ratio control
292
+ // instead of offering a meaningless one. Duration for these two lives in
293
+ // `MODEL_REGISTRY.audio.durationSeconds` alongside the billing bounds, which
294
+ // are mirrored in three repos and locked by `pricing-consistency-check.mjs` §4
295
+ // — moving them here would split one clamp across two files.
296
+ 'seed-audio': {
297
+ aspectRatios: [],
298
+ // ONE image XOR up to 3 audio clips; the XOR is enforced by
299
+ // `validateComposition`, this is only the image arm.
300
+ maxRefImages: 1,
301
+ },
302
+ 'eleven-sfx': {
303
+ aspectRatios: [],
304
+ },
305
+ };
306
+ // ── Queries ──────────────────────────────────────────────────────────────────
307
+ export function getModelCapability(model) {
308
+ return MODEL_CAPABILITIES[model];
309
+ }
310
+ /** Aspect ratios a model accepts, honouring the provider override. */
311
+ export function aspectRatiosFor(model, provider) {
312
+ const cap = MODEL_CAPABILITIES[model];
313
+ if (!cap)
314
+ return ALL_ASPECT_RATIOS;
315
+ if (provider && cap.providerAspectRatios?.[provider])
316
+ return cap.providerAspectRatios[provider];
317
+ return cap.aspectRatios;
318
+ }
319
+ /** Video resolutions a model accepts. A FIXED model reports exactly its one value. */
320
+ export function videoResolutionsFor(model) {
321
+ const vr = MODEL_CAPABILITIES[model]?.videoResolution;
322
+ if (!vr)
323
+ return [];
324
+ return vr.fixed ? [vr.fixed] : vr.options;
325
+ }
326
+ /** The resolution a model would actually run at. Fixed wins; else keep a legal
327
+ * current value; else the model's own default. Mirrors `clampVideoResolution`. */
328
+ export function defaultVideoResolutionFor(model) {
329
+ const vr = MODEL_CAPABILITIES[model]?.videoResolution;
330
+ if (!vr)
331
+ return undefined;
332
+ return vr.fixed ?? vr.default ?? vr.options[0];
333
+ }
334
+ /**
335
+ * Duration constraints after applying overrides.
336
+ *
337
+ * ⚠️ ORDER IS LOAD-BEARING and mirrors `getAvailableDurations` in
338
+ * slate/src/shared/pricing.ts EXACTLY: mode override first (more specific),
339
+ * resolution override only if no mode override applied. Reversing them would
340
+ * make the desktop and the agent disagree about the same generation.
341
+ */
342
+ export function durationsFor(model, opts = {}) {
343
+ const base = MODEL_CAPABILITIES[model]?.duration;
344
+ if (!base)
345
+ return undefined;
346
+ if (opts.promptMode && base.modeOverrides?.[opts.promptMode]) {
347
+ return { ...base, ...base.modeOverrides[opts.promptMode] };
348
+ }
349
+ if (opts.videoResolution && base.resolutionOverrides?.[opts.videoResolution]) {
350
+ return { ...base, ...base.resolutionOverrides[opts.videoResolution] };
351
+ }
352
+ return base;
353
+ }
354
+ /** Every legal whole-second duration. Mirrors `getAvailableDurations`. */
355
+ export function durationValuesFor(model, opts = {}) {
356
+ const d = durationsFor(model, opts);
357
+ if (!d)
358
+ return [];
359
+ if (d.mode === 'discrete' && d.values)
360
+ return d.values;
361
+ const out = [];
362
+ for (let i = d.min; i <= d.max; i++)
363
+ out.push(i);
364
+ return out;
365
+ }
366
+ /** Union of every ratio the given models accept — the legal universe for an enum. */
367
+ export function aspectRatioUnion(models, provider) {
368
+ const seen = new Set();
369
+ for (const m of models)
370
+ for (const r of aspectRatiosFor(m, provider))
371
+ seen.add(r);
372
+ // Emit in ALL_ASPECT_RATIOS order so the enum is stable regardless of input order.
373
+ return ALL_ASPECT_RATIOS.filter((r) => seen.has(r));
374
+ }
375
+ /** Union of every resolution the given models accept. */
376
+ export function videoResolutionUnion(models) {
377
+ const order = ['480p', '720p', '1080p', '4k'];
378
+ const seen = new Set();
379
+ for (const m of models)
380
+ for (const r of videoResolutionsFor(m))
381
+ seen.add(r);
382
+ return order.filter((r) => seen.has(r));
383
+ }
384
+ /** Widest legal duration window across the given models, overrides included. */
385
+ export function durationBounds(models) {
386
+ let min = Infinity;
387
+ let max = -Infinity;
388
+ for (const m of models) {
389
+ for (const v of durationValuesFor(m)) {
390
+ if (v < min)
391
+ min = v;
392
+ if (v > max)
393
+ max = v;
394
+ }
395
+ // Overrides can only narrow, never widen — but read them anyway so a future
396
+ // widening override cannot silently fall outside the enum's bounds.
397
+ const base = MODEL_CAPABILITIES[m]?.duration;
398
+ for (const o of [
399
+ ...Object.values(base?.resolutionOverrides ?? {}),
400
+ ...Object.values(base?.modeOverrides ?? {}),
401
+ ]) {
402
+ if (o.min < min)
403
+ min = o.min;
404
+ if (o.max > max)
405
+ max = o.max;
406
+ }
407
+ }
408
+ return Number.isFinite(min) ? { min, max } : { min: 0, max: 0 };
409
+ }
410
+ // ── Validation ───────────────────────────────────────────────────────────────
411
+ //
412
+ // Each returns an ACTIONABLE message naming the legal set, or null when the
413
+ // value is fine. The message is generated, so it can never name a set the data
414
+ // does not contain.
415
+ export function checkAspectRatio(model, aspectRatio, provider) {
416
+ if (!aspectRatio)
417
+ return null;
418
+ const legal = aspectRatiosFor(model, provider);
419
+ if (legal.length === 0 || legal.includes(aspectRatio))
420
+ return null;
421
+ return `${model} does not accept aspectRatio "${aspectRatio}". It accepts ${legal.join(', ')}. Pick one of those, or switch to a model that takes the shape you want.`;
422
+ }
423
+ export function checkVideoResolution(model, videoResolution) {
424
+ if (!videoResolution)
425
+ return null;
426
+ const legal = videoResolutionsFor(model);
427
+ if (legal.length === 0)
428
+ return null;
429
+ if (legal.includes(videoResolution))
430
+ return null;
431
+ const vr = MODEL_CAPABILITIES[model]?.videoResolution;
432
+ if (vr?.fixed) {
433
+ return `${model} renders at ${vr.fixed} only — it has no resolution parameter, so videoResolution "${videoResolution}" cannot apply. Drop the param.`;
434
+ }
435
+ return `${model} does not render at ${videoResolution}. It offers ${legal.join(', ')}. Pick one of those, or switch models.`;
436
+ }
437
+ export function checkDuration(model, duration, opts = {}) {
438
+ if (duration == null)
439
+ return null;
440
+ const d = durationsFor(model, opts);
441
+ if (!d)
442
+ return null;
443
+ const legal = durationValuesFor(model, opts);
444
+ if (legal.includes(duration))
445
+ return null;
446
+ // Name WHY the window narrowed, and how to widen it again — otherwise the
447
+ // message reads as a contradiction of the model's own advertised range.
448
+ const base = MODEL_CAPABILITIES[model]?.duration;
449
+ let why = '';
450
+ let escape = '';
451
+ if (opts.promptMode && base?.modeOverrides?.[opts.promptMode]) {
452
+ why = ' with reference images attached';
453
+ escape = `, or drop the reference images to get back to ${fmtWindow(base)}`;
454
+ }
455
+ else if (opts.videoResolution && base?.resolutionOverrides?.[opts.videoResolution]) {
456
+ why = ` at ${opts.videoResolution}`;
457
+ escape = `, or pick a resolution without that restriction (${fmtWindow(base)} at the unrestricted ones)`;
458
+ }
459
+ const allowed = legal.length === 1 ? `${legal[0]}s only` : d.mode === 'discrete' ? `${legal.join('s, ')}s` : `${d.min}-${d.max}s`;
460
+ return `${model}${why} accepts ${allowed} — ${duration}s is not legal. Pick a duration in range${escape}.`;
461
+ }
462
+ // ── Generated prose ──────────────────────────────────────────────────────────
463
+ //
464
+ // Every `.describe()` string the LLM reads about these three params is built
465
+ // here, so prose CANNOT contradict the data. Grouping models that share a value
466
+ // keeps the desktop's cached token prefix small.
467
+ function groupBy(models, fn) {
468
+ const groups = [];
469
+ for (const m of models) {
470
+ const value = fn(m);
471
+ if (!value)
472
+ continue;
473
+ const existing = groups.find((g) => g.value === value);
474
+ if (existing)
475
+ existing.models.push(m);
476
+ else
477
+ groups.push({ value, models: [m] });
478
+ }
479
+ return groups.map((g) => `${g.models.join('/')}: ${g.value}`).join(' · ');
480
+ }
481
+ /** e.g. "kling-v3.0-std/kling-v3.0-pro: 16:9, 9:16, 1:1 · seedance-2: 21:9, …" */
482
+ export function describeAspectRatios(models, provider) {
483
+ return groupBy(models, (m) => aspectRatiosFor(m, provider).join(', '));
484
+ }
485
+ /** e.g. "seedance-2: 480p, 720p, 1080p, 4k (default 1080p) · omni-flash: 720p only (fixed)" */
486
+ export function describeVideoResolutions(models) {
487
+ return groupBy(models, (m) => {
488
+ const vr = MODEL_CAPABILITIES[m]?.videoResolution;
489
+ if (!vr)
490
+ return '';
491
+ if (vr.fixed)
492
+ return `${vr.fixed} only (fixed — do not pass videoResolution)`;
493
+ const def = vr.default ?? vr.options[0];
494
+ return `${vr.options.join(', ')} (default ${def})`;
495
+ });
496
+ }
497
+ function fmtWindow(d) {
498
+ return d.mode === 'discrete' && d.values ? `${d.values.join('s/')}s` : `${d.min}-${d.max}s`;
499
+ }
500
+ /** e.g. "kling-v3.0-std: 3-15s · veo-3.1-fast: 4s/6s/8s (1080p/4k: 8s only; with reference images: 8s only)" */
501
+ export function describeDurations(models) {
502
+ return groupBy(models, (m) => {
503
+ const d = MODEL_CAPABILITIES[m]?.duration;
504
+ if (!d)
505
+ return '';
506
+ const clauses = [];
507
+ const resGroups = [];
508
+ for (const [res, o] of Object.entries(d.resolutionOverrides ?? {})) {
509
+ const value = fmtWindow(o);
510
+ const hit = resGroups.find((g) => g.value === value);
511
+ if (hit)
512
+ hit.keys.push(res);
513
+ else
514
+ resGroups.push({ value, keys: [res] });
515
+ }
516
+ for (const g of resGroups)
517
+ clauses.push(`${g.keys.join('/')}: ${g.value} only`);
518
+ if (d.modeOverrides?.ingredients) {
519
+ clauses.push(`with reference images: ${fmtWindow(d.modeOverrides.ingredients)} only`);
520
+ }
521
+ return `${fmtWindow(d)}${clauses.length ? ` (${clauses.join('; ')})` : ''}`;
522
+ });
523
+ }
524
+ /** e.g. "seedance-2: 9 · seedance-2.5: 30 · omni-flash: 7 · seedance-2.5-edit: 0 (prompt + source clip only)" */
525
+ export function describeReferenceImageCaps(models) {
526
+ return groupBy(models, (m) => {
527
+ const cap = MODEL_CAPABILITIES[m];
528
+ if (!cap)
529
+ return '';
530
+ const n = cap.maxIngredientImages ?? cap.maxRefImages;
531
+ if (n == null)
532
+ return '';
533
+ return n === 0 ? '0 (prompt + source clip only)' : String(n);
534
+ });
535
+ }
536
+ //# sourceMappingURL=model-capabilities.js.map