@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.
Files changed (53) hide show
  1. package/dist/index.cjs +325 -63
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +806 -41
  4. package/dist/index.d.ts +806 -41
  5. package/dist/index.js +295 -64
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/credit-identifiers.test.ts +51 -1
  9. package/src/__tests__/gvp-supported-providers.test.ts +20 -3
  10. package/src/__tests__/model-catalog-sections.test.ts +59 -0
  11. package/src/__tests__/model-tree.test.ts +5 -2
  12. package/src/__tests__/parameter-node-value.test.ts +35 -0
  13. package/src/__tests__/pricing-default-duration.test.ts +45 -0
  14. package/src/__tests__/prompt-length-limits.test.ts +4 -0
  15. package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
  16. package/src/__tests__/suno-credit-type.test.ts +46 -0
  17. package/src/__tests__/template-categories.test.ts +45 -0
  18. package/src/__tests__/video-ref-limits.test.ts +11 -2
  19. package/src/credit-identifiers.ts +64 -14
  20. package/src/i18n/camera-motions.ar.ts +5 -5
  21. package/src/i18n/camera-motions.de.ts +5 -5
  22. package/src/i18n/camera-motions.es.ts +5 -5
  23. package/src/i18n/camera-motions.fr.ts +5 -5
  24. package/src/i18n/camera-motions.he.ts +5 -5
  25. package/src/i18n/camera-motions.hi.ts +5 -5
  26. package/src/i18n/camera-motions.ja.ts +5 -5
  27. package/src/i18n/camera-motions.ko.ts +5 -5
  28. package/src/i18n/camera-motions.pt-BR.ts +5 -5
  29. package/src/i18n/camera-motions.ru.ts +5 -5
  30. package/src/i18n/camera-motions.zh-CN.ts +5 -5
  31. package/src/i18n/character-motion.ar.ts +1031 -0
  32. package/src/i18n/character-motion.de.ts +1031 -0
  33. package/src/i18n/character-motion.es.ts +1031 -0
  34. package/src/i18n/character-motion.fr.ts +1031 -0
  35. package/src/i18n/character-motion.he.ts +1031 -0
  36. package/src/i18n/character-motion.hi.ts +1031 -0
  37. package/src/i18n/character-motion.ja.ts +1031 -0
  38. package/src/i18n/character-motion.ko.ts +1031 -0
  39. package/src/i18n/character-motion.pt-BR.ts +1031 -0
  40. package/src/i18n/character-motion.ru.ts +1031 -0
  41. package/src/i18n/character-motion.zh-CN.ts +1031 -0
  42. package/src/i18n/types.ts +1 -0
  43. package/src/index.ts +28 -0
  44. package/src/model-catalog.ts +88 -35
  45. package/src/model-constants.ts +118 -24
  46. package/src/node-execution-state.ts +95 -0
  47. package/src/parameter-node-value.ts +33 -0
  48. package/src/presentation-utils.ts +1 -0
  49. package/src/pro-3d-render.ts +159 -0
  50. package/src/scene3d-delivery-notes.ts +490 -0
  51. package/src/scene3d-v2-plan.ts +8 -2
  52. package/src/smart-cut-windows.ts +15 -10
  53. package/src/template-categories.ts +67 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "3.8.0",
3
+ "version": "3.10.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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-reframe", "ideogram-v3"]
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", "veo3.1", "veo3_lite", "gemini-omni-video", "gemini-omni-flash", "grok-i2v", "happyhorse-ref2v", "wan-3", "wan-3-prime"]) {
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
- "veo3",
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 (voice-clone) or unknown id", () => {
27
- expect(modelToNodeTarget("voice-clone")).toBeNull()
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