@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/operations/index.d.ts +19 -10
- package/dist/operations/index.js +236 -76
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.js +4 -0
- package/dist/prompts/model-capabilities.d.ts +117 -0
- package/dist/prompts/model-capabilities.js +536 -0
- package/dist/prompts/model-facts.d.ts +11 -3
- package/dist/prompts/model-facts.js +64 -48
- package/dist/prompts/prompting-tips.js +2 -2
- package/dist/skills/content.js +4 -4
- package/exports/slates-prompt-builder/generated/slates-prompt-builder-manifest.json +1 -1
- package/package.json +5 -1
- package/skills/slates-model-selection.md +7 -6
- package/skills/slates-one-prompt-film.md +2 -2
- package/skills/slates-prompting-seedance-2-5.md +11 -6
- package/skills/slates-prompting-veo-3.md +2 -2
|
@@ -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
|