@nodaro/shared 3.8.0 → 3.10.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/dist/index.cjs +325 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +806 -41
- package/dist/index.d.ts +806 -41
- package/dist/index.js +295 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +51 -1
- package/src/__tests__/gvp-supported-providers.test.ts +20 -3
- package/src/__tests__/model-catalog-sections.test.ts +59 -0
- package/src/__tests__/model-tree.test.ts +5 -2
- package/src/__tests__/parameter-node-value.test.ts +35 -0
- package/src/__tests__/pricing-default-duration.test.ts +45 -0
- package/src/__tests__/prompt-length-limits.test.ts +4 -0
- package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
- package/src/__tests__/suno-credit-type.test.ts +46 -0
- package/src/__tests__/template-categories.test.ts +45 -0
- package/src/__tests__/video-ref-limits.test.ts +11 -2
- package/src/credit-identifiers.ts +64 -14
- package/src/i18n/camera-motions.ar.ts +5 -5
- package/src/i18n/camera-motions.de.ts +5 -5
- package/src/i18n/camera-motions.es.ts +5 -5
- package/src/i18n/camera-motions.fr.ts +5 -5
- package/src/i18n/camera-motions.he.ts +5 -5
- package/src/i18n/camera-motions.hi.ts +5 -5
- package/src/i18n/camera-motions.ja.ts +5 -5
- package/src/i18n/camera-motions.ko.ts +5 -5
- package/src/i18n/camera-motions.pt-BR.ts +5 -5
- package/src/i18n/camera-motions.ru.ts +5 -5
- package/src/i18n/camera-motions.zh-CN.ts +5 -5
- package/src/i18n/character-motion.ar.ts +1031 -0
- package/src/i18n/character-motion.de.ts +1031 -0
- package/src/i18n/character-motion.es.ts +1031 -0
- package/src/i18n/character-motion.fr.ts +1031 -0
- package/src/i18n/character-motion.he.ts +1031 -0
- package/src/i18n/character-motion.hi.ts +1031 -0
- package/src/i18n/character-motion.ja.ts +1031 -0
- package/src/i18n/character-motion.ko.ts +1031 -0
- package/src/i18n/character-motion.pt-BR.ts +1031 -0
- package/src/i18n/character-motion.ru.ts +1031 -0
- package/src/i18n/character-motion.zh-CN.ts +1031 -0
- package/src/i18n/types.ts +1 -0
- package/src/index.ts +28 -0
- package/src/model-catalog.ts +88 -35
- package/src/model-constants.ts +118 -24
- package/src/node-execution-state.ts +95 -0
- package/src/parameter-node-value.ts +33 -0
- package/src/presentation-utils.ts +1 -0
- package/src/pro-3d-render.ts +159 -0
- package/src/scene3d-delivery-notes.ts +490 -0
- package/src/scene3d-v2-plan.ts +8 -2
- package/src/smart-cut-windows.ts +15 -10
- package/src/template-categories.ts +67 -0
package/package.json
CHANGED
|
@@ -132,7 +132,7 @@ describe("buildCreditModelIdentifier", () => {
|
|
|
132
132
|
|
|
133
133
|
// --- Ideogram providers ---
|
|
134
134
|
describe("ideogram providers", () => {
|
|
135
|
-
const ideogramProviders = ["ideogram-edit", "ideogram-remix", "ideogram-
|
|
135
|
+
const ideogramProviders = ["ideogram-edit", "ideogram-remix", "ideogram-v3"]
|
|
136
136
|
|
|
137
137
|
it.each(ideogramProviders)(
|
|
138
138
|
'%s + renderingSpeed="TURBO" returns composite identifier',
|
|
@@ -230,6 +230,56 @@ describe("buildCreditModelIdentifier", () => {
|
|
|
230
230
|
it("does not affect non-flux-2 providers", () => {
|
|
231
231
|
expect(buildCreditModelIdentifier("nano-banana", undefined, undefined, undefined, undefined, 4)).toBe("nano-banana")
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
// ── Off-grid resolutions ────────────────────────────────────────────────
|
|
235
|
+
// Flux 2 is the ONLY family whose identifier interpolates `resolution`
|
|
236
|
+
// rather than matching it, and its callers legitimately hand it another
|
|
237
|
+
// model's value space: the multi-provider cost preview prices ONE node's
|
|
238
|
+
// data against EVERY selected provider (frontend
|
|
239
|
+
// `ee/hooks/use-providers-credits-sum.ts`), and node data written straight
|
|
240
|
+
// into workflow JSON by an agent/import/template never ran the config
|
|
241
|
+
// panel's provider-change fail-safe. "2K" used to interpolate into
|
|
242
|
+
// "flux-2-pro:2KMP:0ref" — an id no pricing row can answer, so every cost
|
|
243
|
+
// badge 503'd price_not_configured (18 production app-reports 2026-09-07..14).
|
|
244
|
+
it.each([
|
|
245
|
+
["flux-2-pro", "2K", "flux-2-pro:2MP:0ref"],
|
|
246
|
+
["flux-2-max", "2K", "flux-2-max:2MP:0ref"],
|
|
247
|
+
["flux-2-klein", "2K", "flux-2-klein:1MP:0ref"],
|
|
248
|
+
["flux-2-pro", "4K", "flux-2-pro:2MP:0ref"],
|
|
249
|
+
["flux-2-pro", "1080p", "flux-2-pro:2MP:0ref"],
|
|
250
|
+
// Empty string is "unset", not a foreign value — it must not emit ":MP:".
|
|
251
|
+
["flux-2-pro", "", "flux-2-pro:1MP:0ref"],
|
|
252
|
+
])("%s + off-grid resolution %p snaps to the model default", (model, resolution, expected) => {
|
|
253
|
+
expect(buildCreditModelIdentifier(model, undefined, resolution)).toBe(expected)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("an off-grid resolution keeps the ref count", () => {
|
|
257
|
+
expect(buildCreditModelIdentifier("flux-2-max", undefined, "2K", undefined, undefined, 3)).toBe("flux-2-max:2MP:3ref")
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("a same-value different-spelling resolution keeps its tier, in the grid's spelling", () => {
|
|
261
|
+
// "2.0 MP" is the same point on the grid as "2 MP" — snapping it to the
|
|
262
|
+
// model default would silently re-price a correct node — but the id must
|
|
263
|
+
// still read ":2MP:", the spelling the pricing rows are keyed on.
|
|
264
|
+
expect(buildCreditModelIdentifier("flux-2-klein", undefined, "2.0 MP")).toBe("flux-2-klein:2MP:0ref")
|
|
265
|
+
expect(buildCreditModelIdentifier("flux-2-klein", undefined, " 2 ")).toBe("flux-2-klein:2MP:0ref")
|
|
266
|
+
expect(buildCreditModelIdentifier("flux-2-klein", undefined, "0.5")).toBe("flux-2-klein:0.5MP:0ref")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("the snap agrees with the route: normalize-then-build === build", () => {
|
|
270
|
+
// resolveNormalizedImageGen (routes + orchestrator) snaps "2K" through
|
|
271
|
+
// normalizeModelInput BEFORE building. The badge builds directly. Both
|
|
272
|
+
// must land on the same id or the preview quotes a price the reserve
|
|
273
|
+
// will not charge.
|
|
274
|
+
for (const provider of ["flux-2-klein", "flux-2-pro", "flux-2-max"]) {
|
|
275
|
+
for (const resolution of ["2K", "4K", "1K"]) {
|
|
276
|
+
expect(
|
|
277
|
+
resolveNormalizedImageGen({ provider, resolution, refCount: 0 }).identifier,
|
|
278
|
+
`${provider} + ${resolution}`,
|
|
279
|
+
).toBe(buildCreditModelIdentifier(provider, undefined, resolution))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
})
|
|
233
283
|
})
|
|
234
284
|
})
|
|
235
285
|
|
|
@@ -41,13 +41,21 @@ describe("GVP_SUPPORTED_PROVIDERS", () => {
|
|
|
41
41
|
// Not a hand-kept list any more — this pin documents the CURRENT result so
|
|
42
42
|
// a catalog edit that changes the offered SKUs is visible in review, while
|
|
43
43
|
// the derivation itself is what ships.
|
|
44
|
+
//
|
|
45
|
+
// `veo3` (VEO 3.1 QUALITY) LEFT the set on 2026-09-15 and the removal is
|
|
46
|
+
// the derivation working: the bar is "takes a generated start still AND
|
|
47
|
+
// can carry the identity/continuity reference images the anchor wave
|
|
48
|
+
// attaches", and KIE serves reference-to-video on the Fast and Lite SKUs
|
|
49
|
+
// only ("Reference to video only supports the Veo Fast model and Veo Lite
|
|
50
|
+
// model.", production 2026-09-04). A Quality run that reached the anchor
|
|
51
|
+
// wave with identity refs was a 422 mid-render, after the credits for a
|
|
52
|
+
// multi-segment job were reserved. The Fast and Lite SKUs stay.
|
|
44
53
|
expect([...GVP_SUPPORTED_PROVIDERS]).toEqual([
|
|
45
54
|
"seedance-2",
|
|
46
55
|
"seedance-2-fast",
|
|
47
56
|
"seedance-2-mini",
|
|
48
57
|
"seedance-2-5",
|
|
49
58
|
"minimax-h3",
|
|
50
|
-
"veo3",
|
|
51
59
|
"veo3.1",
|
|
52
60
|
"veo3_lite",
|
|
53
61
|
"gemini-omni-video",
|
|
@@ -121,6 +129,12 @@ describe("GVP_SUPPORTED_PROVIDERS", () => {
|
|
|
121
129
|
expect(isGvpSupportedProvider("hailuo-2.3-pro")).toBe(false)
|
|
122
130
|
expect(isGvpSupportedProvider("bytedance-pro")).toBe(false)
|
|
123
131
|
expect(isGvpSupportedProvider("grok-imagine-video-1.5")).toBe(false)
|
|
132
|
+
// VEO 3.1 Quality joins them: i2v and segmentable, but no reference
|
|
133
|
+
// transport at KIE (Fast/Lite only), so the anchor wave has nowhere to
|
|
134
|
+
// put identity refs.
|
|
135
|
+
expect(isGvpSupportedProvider("veo3")).toBe(false)
|
|
136
|
+
expect(isGvpSupportedProvider("veo3.1")).toBe(true)
|
|
137
|
+
expect(isGvpSupportedProvider("veo3_lite")).toBe(true)
|
|
124
138
|
// Not i2v at all.
|
|
125
139
|
expect(isGvpSupportedProvider("seedance-2-extend")).toBe(false)
|
|
126
140
|
expect(isGvpSupportedProvider("runway-aleph")).toBe(false)
|
|
@@ -170,7 +184,7 @@ describe("GVP_EXTEND_PROVIDERS", () => {
|
|
|
170
184
|
})
|
|
171
185
|
|
|
172
186
|
it("keyframes-only SKUs reject extend", () => {
|
|
173
|
-
for (const p of ["veo3
|
|
187
|
+
for (const p of ["veo3.1", "veo3_lite", "gemini-omni-video", "gemini-omni-flash", "grok-i2v", "happyhorse-ref2v", "wan-3", "wan-3-prime"]) {
|
|
174
188
|
expect(isGvpSupportedProvider(p)).toBe(true)
|
|
175
189
|
expect(supportsExtendRender(p)).toBe(false)
|
|
176
190
|
}
|
|
@@ -186,7 +200,10 @@ describe("GVP_END_FRAME_PROVIDERS", () => {
|
|
|
186
200
|
"seedance-2-mini",
|
|
187
201
|
"seedance-2-5",
|
|
188
202
|
"minimax-h3",
|
|
189
|
-
|
|
203
|
+
// `veo3` is absent because GVP_END_FRAME_PROVIDERS is a subset of
|
|
204
|
+
// GVP_SUPPORTED_PROVIDERS, which VEO 3.1 Quality left — see above. The
|
|
205
|
+
// catalog still declares "end-frame" for the SKU; it is simply not a
|
|
206
|
+
// pro-node SKU any more.
|
|
190
207
|
"veo3.1",
|
|
191
208
|
"veo3_lite",
|
|
192
209
|
"wan-3",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { MODEL_CATALOG, MODEL_KINDS, groupByKindAndFamily, listModels } from "../model-catalog.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Image / Video / Audio envelope of every model-discovery surface.
|
|
6
|
+
*
|
|
7
|
+
* #1332: `GET /v1/models` and MCP `list_models` grouped by family FIRST and
|
|
8
|
+
* let the family's first model pick the section, so a vendor that ships more
|
|
9
|
+
* than one kind — Google (Imagen + VEO), ByteDance (Seedream + Seedance),
|
|
10
|
+
* Alibaba, xAI — was filed whole under whichever kind came first in catalog
|
|
11
|
+
* order: 23 video models shipped under "image" whenever the call carried no
|
|
12
|
+
* `kind` filter. The invariant that was missing: every model appears in the
|
|
13
|
+
* section of its OWN kind.
|
|
14
|
+
*/
|
|
15
|
+
describe("groupByKindAndFamily", () => {
|
|
16
|
+
const all = listModels({})
|
|
17
|
+
const sections = groupByKindAndFamily(all)
|
|
18
|
+
|
|
19
|
+
it("files every catalog model under the section of its own kind", () => {
|
|
20
|
+
for (const section of sections) {
|
|
21
|
+
for (const { models } of section.families) {
|
|
22
|
+
for (const m of models) expect(m.kind, `${m.id} filed under "${section.kind}"`).toBe(section.kind)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("loses nothing and duplicates nothing", () => {
|
|
28
|
+
const filed = sections.flatMap((s) => s.families.flatMap((f) => f.models.map((m) => m.id))).sort()
|
|
29
|
+
expect(filed).toEqual(all.map((m) => m.id).sort())
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("a vendor that ships more than one kind appears once per kind — VEO is video, Imagen is image", () => {
|
|
33
|
+
const familiesOf = (kind: string) => sections.find((s) => s.kind === kind)?.families.map((f) => f.family) ?? []
|
|
34
|
+
expect(familiesOf("video")).toContain("Google")
|
|
35
|
+
expect(familiesOf("image")).toContain("Google")
|
|
36
|
+
const videoIds = sections.find((s) => s.kind === "video")!.families.flatMap((f) => f.models.map((m) => m.id))
|
|
37
|
+
for (const id of ["veo3", "veo3.1", "seedance-2", "seedance-2-5", "wan-3", "grok-i2v"]) {
|
|
38
|
+
if (MODEL_CATALOG[id]) expect(videoIds, id).toContain(id)
|
|
39
|
+
}
|
|
40
|
+
const imageIds = sections.find((s) => s.kind === "image")!.families.flatMap((f) => f.models.map((m) => m.id))
|
|
41
|
+
for (const id of videoIds) expect(imageIds).not.toContain(id)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("keeps the fixed image → video → audio order and omits an empty section", () => {
|
|
45
|
+
expect(sections.map((s) => s.kind)).toEqual([...MODEL_KINDS])
|
|
46
|
+
expect(groupByKindAndFamily(all.filter((m) => m.kind === "audio")).map((s) => s.kind)).toEqual(["audio"])
|
|
47
|
+
expect(groupByKindAndFamily([])).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("one spelling per vendor — a family name never differs from another only by case", () => {
|
|
51
|
+
const byLower = new Map<string, Set<string>>()
|
|
52
|
+
for (const m of all) {
|
|
53
|
+
const set = byLower.get(m.family.toLowerCase()) ?? new Set<string>()
|
|
54
|
+
set.add(m.family)
|
|
55
|
+
byLower.set(m.family.toLowerCase(), set)
|
|
56
|
+
}
|
|
57
|
+
for (const [, spellings] of byLower) expect([...spellings], [...spellings].join(" / ")).toHaveLength(1)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -10,6 +10,9 @@ describe("modelToNodeTarget", () => {
|
|
|
10
10
|
expect(modelToNodeTarget("suno")).toEqual({ nodeType: "suno-generate", field: "model", value: "V4" })
|
|
11
11
|
expect(modelToNodeTarget("suno-v5")).toEqual({ nodeType: "suno-generate", field: "model", value: "V5" })
|
|
12
12
|
expect(modelToNodeTarget("suno-v5_5")).toEqual({ nodeType: "suno-generate", field: "model", value: "V5_5" })
|
|
13
|
+
expect(modelToNodeTarget("suno-v6")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6" })
|
|
14
|
+
expect(modelToNodeTarget("suno-v6_wild")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6_WILD" })
|
|
15
|
+
expect(modelToNodeTarget("suno-v6_mini")).toEqual({ nodeType: "suno-generate", field: "model", value: "V6_MINI" })
|
|
13
16
|
})
|
|
14
17
|
// `elevenlabs-dubbing` is a single-provider utility whose id is NOT in any
|
|
15
18
|
// provider-enum array, so it exercises the modes fallback → bare node, no preset.
|
|
@@ -23,8 +26,8 @@ describe("modelToNodeTarget", () => {
|
|
|
23
26
|
it("returns a provider preset for an enum video model (gemini-omni-video is enum-valid)", () => {
|
|
24
27
|
expect(modelToNodeTarget("gemini-omni-video")).toEqual({ nodeType: "generate-video", field: "provider", value: "gemini-omni-video" })
|
|
25
28
|
})
|
|
26
|
-
it("returns null when there is no node (
|
|
27
|
-
expect(modelToNodeTarget("
|
|
29
|
+
it("returns null when there is no node (video-audit) or unknown id", () => {
|
|
30
|
+
expect(modelToNodeTarget("video-audit")).toBeNull()
|
|
28
31
|
expect(modelToNodeTarget("totally-unknown")).toBeNull()
|
|
29
32
|
})
|
|
30
33
|
it("never targets suno-generate without a model value (every music model carries dataValue)", () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from "vitest"
|
|
2
2
|
import {
|
|
3
3
|
PARAMETER_NODE_TYPES,
|
|
4
|
+
VIDEO_ONLY_PARAMETER_NODE_TYPES,
|
|
5
|
+
EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES,
|
|
4
6
|
getParameterValue,
|
|
5
7
|
} from "../parameter-node-value.js"
|
|
6
8
|
import { setRegisteredPersonPackFields } from "../index.js"
|
|
@@ -123,6 +125,39 @@ describe("getParameterValue — character-fx", () => {
|
|
|
123
125
|
})
|
|
124
126
|
})
|
|
125
127
|
|
|
128
|
+
describe("getParameterValue — character-motion", () => {
|
|
129
|
+
it("returns the id for a single string value", () => {
|
|
130
|
+
expect(getParameterValue({ characterMotion: "wave-hello" }, "character-motion")).toBe("wave-hello")
|
|
131
|
+
})
|
|
132
|
+
it("returns the first id of an ordered multi-pick", () => {
|
|
133
|
+
expect(getParameterValue({ characterMotion: ["walk-in-from-left", "wave-hello"] }, "character-motion")).toBe("walk-in-from-left")
|
|
134
|
+
})
|
|
135
|
+
it("returns undefined for empty string / empty array", () => {
|
|
136
|
+
expect(getParameterValue({ characterMotion: "" }, "character-motion")).toBeUndefined()
|
|
137
|
+
expect(getParameterValue({ characterMotion: [] }, "character-motion")).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
it("character-motion is a parameter node", () => {
|
|
140
|
+
expect(PARAMETER_NODE_TYPES.has("character-motion")).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("VIDEO_ONLY_PARAMETER_NODE_TYPES", () => {
|
|
145
|
+
it("names exactly the pickers a still image must never receive", () => {
|
|
146
|
+
expect([...VIDEO_ONLY_PARAMETER_NODE_TYPES].sort()).toEqual(
|
|
147
|
+
["camera-motion", "character-fx", "character-motion", "temporal", "transition"],
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
it("is a subset of PARAMETER_NODE_TYPES", () => {
|
|
151
|
+
for (const t of VIDEO_ONLY_PARAMETER_NODE_TYPES) expect(PARAMETER_NODE_TYPES.has(t), t).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe("EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES", () => {
|
|
156
|
+
it("names the pickers whose fragment needs the graph at execution", () => {
|
|
157
|
+
expect([...EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES].sort()).toEqual(["camera-motion", "character-motion"])
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
126
161
|
describe("parameter-node-value — furniture", () => {
|
|
127
162
|
it("includes furniture in PARAMETER_NODE_TYPES set", () => {
|
|
128
163
|
expect(PARAMETER_NODE_TYPES.has("furniture")).toBe(true)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { PRICING_DEFAULT_DURATION_SEC, pricedOutputDurationSec } from "../model-constants.js"
|
|
3
|
+
import { buildVideoCreditModelIdentifier } from "../credit-identifiers.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #1397: a request that names a provider but omits `duration` renders the
|
|
7
|
+
* provider's OWN default, so it must be priced — identifier tier AND every
|
|
8
|
+
* reservation that scales by output seconds — at that default, never at a
|
|
9
|
+
* literal 5. `pricedOutputDurationSec` is the one source both read.
|
|
10
|
+
*/
|
|
11
|
+
describe("pricedOutputDurationSec", () => {
|
|
12
|
+
it("the requested duration wins, as a number or a numeric string", () => {
|
|
13
|
+
expect(pricedOutputDurationSec("seedance-2-5", 12)).toBe(12)
|
|
14
|
+
expect(pricedOutputDurationSec("seedance-2-5", "12")).toBe(12)
|
|
15
|
+
expect(pricedOutputDurationSec("minimax-h3", 4)).toBe(4)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("an omitted duration prices the provider's declared default render length", () => {
|
|
19
|
+
expect(pricedOutputDurationSec("seedance-2-5", undefined)).toBe(8)
|
|
20
|
+
expect(pricedOutputDurationSec("grok-imagine-video-1.5", undefined)).toBe(8)
|
|
21
|
+
expect(pricedOutputDurationSec("minimax-h3", undefined)).toBe(6)
|
|
22
|
+
expect(PRICING_DEFAULT_DURATION_SEC["seedance-2-5"]).toBe(8)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("a provider with no declared default keeps the historical 5s fallback", () => {
|
|
26
|
+
expect(pricedOutputDurationSec("seedance-2", undefined)).toBe(5)
|
|
27
|
+
expect(pricedOutputDurationSec("kling", undefined)).toBe(5)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("an unparseable duration falls back the same way", () => {
|
|
31
|
+
expect(pricedOutputDurationSec("seedance-2-5", "abc")).toBe(8)
|
|
32
|
+
expect(pricedOutputDurationSec("seedance-2", "abc")).toBe(5)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("the identifier tier for an omitted duration is the default's tier, not the 5s one (seedance-2-5)", () => {
|
|
36
|
+
for (const nodeType of ["image-to-video", "text-to-video"] as const) {
|
|
37
|
+
expect(buildVideoCreditModelIdentifier("seedance-2-5", undefined, undefined, nodeType, undefined, "720p")).toBe(
|
|
38
|
+
buildVideoCreditModelIdentifier("seedance-2-5", 8, undefined, nodeType, undefined, "720p"),
|
|
39
|
+
)
|
|
40
|
+
expect(buildVideoCreditModelIdentifier("seedance-2-5", undefined, undefined, nodeType, undefined, "720p", true)).toBe(
|
|
41
|
+
"seedance-2-5:8s:720p-ref",
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -40,6 +40,10 @@ describe("per-model prompt length limits", () => {
|
|
|
40
40
|
expect(getMaxImagePromptChars("seedream-5-lite")).toBe(1000)
|
|
41
41
|
expect(getMaxImagePromptChars("qwen")).toBe(3000)
|
|
42
42
|
expect(getMaxImagePromptChars("qwen-edit")).toBe(2000)
|
|
43
|
+
// The shortest of the family. Listing it as the 5000 default is what sent
|
|
44
|
+
// three prod runs (2026-09-07) into KIE's "The text length cannot exceed
|
|
45
|
+
// the maximum limit".
|
|
46
|
+
expect(getMaxImagePromptChars("z-image")).toBe(1000)
|
|
43
47
|
})
|
|
44
48
|
it("siblings differ — does not copy the t2i number to i2i/edit", () => {
|
|
45
49
|
// seedream t2i 3000, 5-lite t2i 1000 but its i2i is 3000
|