@nodaro/shared 1.18.0 → 1.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "1.18.0",
3
+ "version": "1.20.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",
@@ -20,12 +20,36 @@ import { PIPELINE_PINNABLE_SCRIPT_LLMS } from "../pipeline-types.js"
20
20
  // the NON-monetary model registry (ids, capabilities, tiers, feature
21
21
  // defaults) that stays in the published package.
22
22
 
23
+ /** The registry's expected membership, in order — the single list both the
24
+ * count guard and the id-order guard below assert against, so a model
25
+ * addition can never leave one of them stale. */
26
+ const EXPECTED_MODEL_IDS = [
27
+ "gemini-3-flash",
28
+ "gemini-3.6-flash",
29
+ "claude-haiku-4.5",
30
+ "claude-sonnet-4.6",
31
+ "gpt-5.2",
32
+ "gemini-3.1-pro",
33
+ "claude-opus-4.7",
34
+ "gpt-5.4",
35
+ "gpt-5.5",
36
+ "gpt-5.6-luna",
37
+ "gpt-5.6-terra",
38
+ "gpt-5.6-sol",
39
+ "claude-sonnet-5",
40
+ "claude-opus-4.8",
41
+ "claude-opus-5",
42
+ "claude-fable-5",
43
+ ]
44
+
23
45
  // ---------------------------------------------------------------------------
24
46
  // LLM_MODELS data integrity
25
47
  // ---------------------------------------------------------------------------
26
48
  describe("LLM_MODELS data integrity", () => {
27
- it("should have exactly 15 models", () => {
28
- expect(LLM_MODELS).toHaveLength(16)
49
+ it("model count matches the enumerated id list", () => {
50
+ // Bound to the id list asserted below rather than a hand-typed number, so
51
+ // the count and the title can never disagree after a model addition.
52
+ expect(LLM_MODELS).toHaveLength(EXPECTED_MODEL_IDS.length)
29
53
  })
30
54
 
31
55
  it("each model has all required fields", () => {
@@ -106,25 +130,7 @@ describe("LLM_MODEL_IDS", () => {
106
130
  })
107
131
 
108
132
  it("contains all expected model ids", () => {
109
- const expected = [
110
- "gemini-3-flash",
111
- "gemini-3.6-flash",
112
- "claude-haiku-4.5",
113
- "claude-sonnet-4.6",
114
- "gpt-5.2",
115
- "gemini-3.1-pro",
116
- "claude-opus-4.7",
117
- "gpt-5.4",
118
- "gpt-5.5",
119
- "gpt-5.6-luna",
120
- "gpt-5.6-terra",
121
- "gpt-5.6-sol",
122
- "claude-sonnet-5",
123
- "claude-opus-4.8",
124
- "claude-opus-5",
125
- "claude-fable-5",
126
- ]
127
- expect(LLM_MODEL_IDS).toEqual(expected)
133
+ expect(LLM_MODEL_IDS).toEqual(EXPECTED_MODEL_IDS)
128
134
  })
129
135
  })
130
136
 
@@ -522,6 +528,25 @@ describe("reasoning effort registry", () => {
522
528
  expect(getLlmTier("gpt-5.5")).toBe("premium")
523
529
  expect(getLlmTier("gemini-3.6-flash")).toBe("economy")
524
530
  expect(getLlmTier("claude-fable-5")).toBe("premium")
531
+ expect(getLlmTier("claude-opus-5")).toBe("premium")
532
+ })
533
+
534
+ it("claude-opus-5 carries the full ladder, its own direct fallback, and thinkingDefaultOn", () => {
535
+ const m = getLlmModel("claude-opus-5")
536
+ expect(m?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"])
537
+ expect(m?.directFallbackModel).toBe("claude-opus-5")
538
+ // Load-bearing: consumers floor max_tokens off this flag, because Opus 5
539
+ // reasons even with NO thinking param sent. Dropping it silently
540
+ // reintroduces truncated answers at Effort=Auto on a premium model.
541
+ expect(m?.thinkingDefaultOn).toBe(true)
542
+ })
543
+
544
+ it("thinkingDefaultOn is set only where the vendor default actually reasons", () => {
545
+ // Opus 4.8 / 4.7 and Sonnet 5 do NOT reason when `thinking` is omitted —
546
+ // flagging them would inflate every call's cap for no reason.
547
+ for (const id of ["claude-opus-4.8", "claude-opus-4.7", "claude-sonnet-5", "claude-sonnet-4.6"]) {
548
+ expect(getLlmModel(id)?.thinkingDefaultOn, id).toBeUndefined()
549
+ }
525
550
  })
526
551
 
527
552
  it("gemini-3.6-flash exposes the KIE low/high thinking levels; claude-fable-5 the full Claude ladder", () => {
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ clampSmartCutWindow,
4
+ SMART_CUT_WINDOW_MIN,
5
+ SMART_CUT_WINDOW_MAX,
6
+ SMART_CUT_WINDOW_DEFAULT,
7
+ } from "../smart-cut-windows.js"
8
+
9
+ describe("smart-cut search windows", () => {
10
+ it("keeps in-range integers untouched", () => {
11
+ for (const n of [1, 4, 8, 12, 24]) expect(clampSmartCutWindow(n)).toBe(n)
12
+ })
13
+
14
+ it("clamps to the product bounds instead of erroring", () => {
15
+ expect(clampSmartCutWindow(999)).toBe(SMART_CUT_WINDOW_MAX)
16
+ expect(clampSmartCutWindow(25)).toBe(SMART_CUT_WINDOW_MAX)
17
+ expect(clampSmartCutWindow(0)).toBe(SMART_CUT_WINDOW_MIN)
18
+ expect(clampSmartCutWindow(-5)).toBe(SMART_CUT_WINDOW_MIN)
19
+ })
20
+
21
+ it("rounds fractional input (a number <input> can emit one)", () => {
22
+ expect(clampSmartCutWindow(8.4)).toBe(8)
23
+ expect(clampSmartCutWindow(8.6)).toBe(9)
24
+ })
25
+
26
+ it('returns undefined — "use the engine default" — for anything non-numeric', () => {
27
+ // Load-bearing: undefined means the field is OMITTED from the request, so
28
+ // the engine applies its own 8/8. Coercing junk to a NUMBER instead would
29
+ // silently run a search width the user never chose.
30
+ for (const v of [undefined, null, NaN, Infinity, "8", "", {}, []]) {
31
+ expect(clampSmartCutWindow(v), String(v)).toBeUndefined()
32
+ }
33
+ })
34
+
35
+ it("bounds are coherent and the default sits inside them", () => {
36
+ expect(SMART_CUT_WINDOW_MIN).toBeLessThan(SMART_CUT_WINDOW_MAX)
37
+ expect(SMART_CUT_WINDOW_DEFAULT).toBeGreaterThanOrEqual(SMART_CUT_WINDOW_MIN)
38
+ expect(SMART_CUT_WINDOW_DEFAULT).toBeLessThanOrEqual(SMART_CUT_WINDOW_MAX)
39
+ // The engine route accepts up to 48; the product cap must stay within it
40
+ // or a clamped value would still 400 at the boundary we were protecting.
41
+ expect(SMART_CUT_WINDOW_MAX).toBeLessThanOrEqual(48)
42
+ })
43
+ })
package/src/index.ts CHANGED
@@ -889,5 +889,8 @@ export * from "./video-analysis.js"
889
889
  // --- Video-analysis pricing (duration buckets + structural credit formula) ---
890
890
  export * from "./video-analysis-pricing.js"
891
891
 
892
+ // --- Smart-cut best-pair search windows (shared bound + clamp) ---
893
+ export * from "./smart-cut-windows.js"
894
+
892
895
  export * from "./entity-asset-types.js"
893
896
  export * from "./hint-graph-types.js"
package/src/llm-models.ts CHANGED
@@ -57,6 +57,20 @@ export interface LlmModelDef {
57
57
  supportsTemperature?: false
58
58
  /** Claude-only: KIE is the preferred routing, direct Anthropic the fallback. */
59
59
  preferKie?: true
60
+ /**
61
+ * true = the model reasons even when NO thinking parameter is sent, so its
62
+ * reasoning tokens share the `max_tokens` budget on EVERY call — not just
63
+ * effort-bearing ones. Claude Opus 5 flipped this default (on Opus 4.8/4.7
64
+ * and Sonnet 5, omitting `thinking` means no thinking at all).
65
+ *
66
+ * Consumers MUST give such a model output headroom regardless of the
67
+ * requested effort, or reasoning silently eats a small legacy cap and the
68
+ * answer truncates with `stop_reason: max_tokens` — a paid-for empty reply,
69
+ * not an error. `deriveParams` (llm-client.ts) and the film-pipeline's
70
+ * `callLLM` both floor on this flag; keep it in sync with the vendor's
71
+ * documented default rather than inferring it from the model name.
72
+ */
73
+ thinkingDefaultOn?: true
60
74
  }
61
75
 
62
76
  export const LLM_MODELS: readonly LlmModelDef[] = [
@@ -143,7 +157,11 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
143
157
  {
144
158
  id: "claude-opus-4.7",
145
159
  displayName: "Claude Opus 4.7",
146
- desc: "Highest quality, complex tasks",
160
+ // Superlatives belong to the CURRENT top model only — `desc` renders in every
161
+ // model picker, so leaving "highest quality" on an older Opus steers
162
+ // quality-critical work backwards (all three Opus entries bill premium, so
163
+ // the mis-steer is invisible on the invoice).
164
+ desc: "Older Opus, complex tasks",
147
165
  tier: "premium",
148
166
  kieFormat: "messages",
149
167
  kieSlugOrModel: "claude-opus-4-7",
@@ -276,6 +294,9 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
276
294
  reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
277
295
  supportsTemperature: false,
278
296
  preferKie: true,
297
+ // Opus 5 reasons with NO thinking param sent (vendor default flipped from
298
+ // Opus 4.8/4.7) — so every call needs output headroom, not just xhigh/max.
299
+ thinkingDefaultOn: true,
279
300
  },
280
301
  {
281
302
  id: "claude-fable-5",
@@ -1812,14 +1812,14 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1812
1812
  family: "Nodaro",
1813
1813
  label: "Video Analysis (Fast — legacy)",
1814
1814
  series: "Video Analysis",
1815
- description: "Legacy fast-tier analysis model (pre-2026-07). Kept so stored raw-model configs keep running and pricing unchanged; new fast-tier runs use the current fast model.",
1815
+ description: "Legacy fast-tier analysis model (pre-2026-07). Kept so stored raw-model configs keep running and keep pricing under their own identifier; new fast-tier runs use the current fast model.",
1816
1816
  useCases: ["video-analysis", "shot-list", "fast"],
1817
1817
  pricing: [
1818
- { identifier: "video-analysis:gemini-3-flash", credits: 3, note: "10-min ceiling (no duration given)" },
1819
- { identifier: "video-analysis:gemini-3-flash:60s", credits: 1 },
1820
- { identifier: "video-analysis:gemini-3-flash:180s", credits: 1 },
1821
- { identifier: "video-analysis:gemini-3-flash:360s", credits: 2 },
1822
- { identifier: "video-analysis:gemini-3-flash:600s", credits: 3, note: "10-min ceiling" },
1818
+ { identifier: "video-analysis:gemini-3-flash", credits: 9, note: "10-min ceiling (no duration given)" },
1819
+ { identifier: "video-analysis:gemini-3-flash:60s", credits: 2 },
1820
+ { identifier: "video-analysis:gemini-3-flash:180s", credits: 3 },
1821
+ { identifier: "video-analysis:gemini-3-flash:360s", credits: 6 },
1822
+ { identifier: "video-analysis:gemini-3-flash:600s", credits: 9, note: "10-min ceiling" },
1823
1823
  ],
1824
1824
  },
1825
1825
  "gemini-3.6-flash-video-analysis": {
@@ -1832,11 +1832,11 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1832
1832
  description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast, economy tier. Billed per duration bucket.",
1833
1833
  useCases: ["video-analysis", "shot-list", "fast"],
1834
1834
  pricing: [
1835
- { identifier: "video-analysis:gemini-3.6-flash", credits: 9, note: "10-min ceiling (no duration given)" },
1836
- { identifier: "video-analysis:gemini-3.6-flash:60s", credits: 2 },
1837
- { identifier: "video-analysis:gemini-3.6-flash:180s", credits: 2 },
1838
- { identifier: "video-analysis:gemini-3.6-flash:360s", credits: 5 },
1839
- { identifier: "video-analysis:gemini-3.6-flash:600s", credits: 9, note: "10-min ceiling" },
1835
+ { identifier: "video-analysis:gemini-3.6-flash", credits: 25, note: "10-min ceiling (no duration given)" },
1836
+ { identifier: "video-analysis:gemini-3.6-flash:60s", credits: 5 },
1837
+ { identifier: "video-analysis:gemini-3.6-flash:180s", credits: 6 },
1838
+ { identifier: "video-analysis:gemini-3.6-flash:360s", credits: 15 },
1839
+ { identifier: "video-analysis:gemini-3.6-flash:600s", credits: 25, note: "10-min ceiling" },
1840
1840
  ],
1841
1841
  },
1842
1842
  "gemini-3.1-pro-video-analysis": {
@@ -1849,11 +1849,11 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1849
1849
  description: "Analyze a video into a structured shot list (scenes, camera, audio) — higher-fidelity, default tier. Billed per duration bucket.",
1850
1850
  useCases: ["video-analysis", "shot-list", "cinematic"],
1851
1851
  pricing: [
1852
- { identifier: "video-analysis:gemini-3.1-pro", credits: 11, note: "10-min ceiling (no duration given)" },
1853
- { identifier: "video-analysis:gemini-3.1-pro:60s", credits: 2 },
1854
- { identifier: "video-analysis:gemini-3.1-pro:180s", credits: 3 },
1855
- { identifier: "video-analysis:gemini-3.1-pro:360s", credits: 7 },
1856
- { identifier: "video-analysis:gemini-3.1-pro:600s", credits: 11, note: "10-min ceiling" },
1852
+ { identifier: "video-analysis:gemini-3.1-pro", credits: 33, note: "10-min ceiling (no duration given)" },
1853
+ { identifier: "video-analysis:gemini-3.1-pro:60s", credits: 6 },
1854
+ { identifier: "video-analysis:gemini-3.1-pro:180s", credits: 8 },
1855
+ { identifier: "video-analysis:gemini-3.1-pro:360s", credits: 20 },
1856
+ { identifier: "video-analysis:gemini-3.1-pro:600s", credits: 33, note: "10-min ceiling" },
1857
1857
  ],
1858
1858
  },
1859
1859
  // Both mixed tiers are variants of the same advanced multi-engine analysis
@@ -1869,11 +1869,11 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1869
1869
  description: "Our most advanced analysis tier — multiple analysis engines combined into one result for maximum completeness and accuracy. Billed per duration bucket.",
1870
1870
  useCases: ["video-analysis", "shot-list", "premium", "most-complete"],
1871
1871
  pricing: [
1872
- { identifier: "video-analysis:mixed", credits: 14, note: "10-min ceiling (no duration given)" },
1873
- { identifier: "video-analysis:mixed:60s", credits: 3 },
1874
- { identifier: "video-analysis:mixed:180s", credits: 4 },
1875
- { identifier: "video-analysis:mixed:360s", credits: 9 },
1876
- { identifier: "video-analysis:mixed:600s", credits: 14, note: "10-min ceiling" },
1872
+ { identifier: "video-analysis:mixed", credits: 57, note: "10-min ceiling (no duration given)" },
1873
+ { identifier: "video-analysis:mixed:60s", credits: 10 },
1874
+ { identifier: "video-analysis:mixed:180s", credits: 13 },
1875
+ { identifier: "video-analysis:mixed:360s", credits: 35 },
1876
+ { identifier: "video-analysis:mixed:600s", credits: 57, note: "10-min ceiling" },
1877
1877
  ],
1878
1878
  },
1879
1879
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Smart-cut best-pair SEARCH WINDOWS — the shared bound + clamp for
3
+ * generate-video-pro's `smartCutFramesPrev` / `smartCutFramesNext`.
4
+ *
5
+ * What they do: the best-pair matcher (the mode the node calls "legacy-8x8")
6
+ * PSNR-compares the last N frames of a segment against the first M of the
7
+ * next, ends the previous clip ON the best match and starts the next right
8
+ * AFTER its twin — so the duplicated frame plays once and motion stays
9
+ * continuous. N and M are those windows. Absent → the engine's own 8/8
10
+ * default, which is byte-identical to the behavior before they were
11
+ * exposed.
12
+ *
13
+ * Why wider helps: a continuation can re-enact a longer stretch of the
14
+ * previous tail than 8 frames covers, and a match outside the window is
15
+ * simply never found — the boundary silently falls back to the fixed
16
+ * freeze-trims. recast pins 24/24 for exactly this reason.
17
+ *
18
+ * Why a shared clamp: the canvas node (single-node Run) and the orchestrator
19
+ * (workflow Run) are two independent send paths into the same engine route,
20
+ * whose Zod schema rejects out-of-range values. Deriving both from this one
21
+ * function means a stale or hand-edited node value degrades to a legal
22
+ * request instead of 400-ing an entire multi-segment run at finalize time —
23
+ * and the two paths cannot drift apart.
24
+ */
25
+
26
+ /** Widest window the UI offers. The engine route itself accepts up to 48;
27
+ * 24 is the product cap — it already covers a full second of re-enactment
28
+ * at 24fps, and every frame added past the real overlap only costs match
29
+ * time and invites a spurious pairing. */
30
+ export const SMART_CUT_WINDOW_MAX = 24
31
+ /** Narrowest meaningful window — one frame each side. */
32
+ export const SMART_CUT_WINDOW_MIN = 1
33
+ /** The engine's default when the field is absent. Documented here so the UI
34
+ * can show it as the placeholder without hardcoding a second copy. */
35
+ export const SMART_CUT_WINDOW_DEFAULT = 8
36
+
37
+ /**
38
+ * Narrow an arbitrary node value to a legal window, or `undefined` to mean
39
+ * "let the engine use its default". Non-numbers, NaN, and non-integers all
40
+ * collapse to `undefined` rather than to a guessed number: a malformed value
41
+ * should fall back to the proven default, not silently pick a different
42
+ * search width than the user asked for.
43
+ */
44
+ export function clampSmartCutWindow(value: unknown): number | undefined {
45
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined
46
+ const n = Math.round(value)
47
+ if (n < SMART_CUT_WINDOW_MIN) return SMART_CUT_WINDOW_MIN
48
+ if (n > SMART_CUT_WINDOW_MAX) return SMART_CUT_WINDOW_MAX
49
+ return n
50
+ }
@@ -47,27 +47,27 @@ export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, O
47
47
  */
48
48
  export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
49
49
  // Legacy fast-tier model (pre-2026-07) — kept for stored raw-id configs.
50
- "video-analysis:gemini-3-flash:60s": 1,
51
- "video-analysis:gemini-3-flash:180s": 1,
52
- "video-analysis:gemini-3-flash:360s": 2,
53
- "video-analysis:gemini-3-flash:600s": 3,
50
+ "video-analysis:gemini-3-flash:60s": 2,
51
+ "video-analysis:gemini-3-flash:180s": 3,
52
+ "video-analysis:gemini-3-flash:360s": 6,
53
+ "video-analysis:gemini-3-flash:600s": 9,
54
54
  // Current fast tier — regenerated from the private formula for its backing
55
55
  // model; higher than the legacy fast schedule but still ≤ pro per bucket.
56
- "video-analysis:gemini-3.6-flash:60s": 2,
57
- "video-analysis:gemini-3.6-flash:180s": 2,
58
- "video-analysis:gemini-3.6-flash:360s": 5,
59
- "video-analysis:gemini-3.6-flash:600s": 9,
60
- "video-analysis:gemini-3.1-pro:60s": 2,
61
- "video-analysis:gemini-3.1-pro:180s": 3,
62
- "video-analysis:gemini-3.1-pro:360s": 7,
63
- "video-analysis:gemini-3.1-pro:600s": 11,
56
+ "video-analysis:gemini-3.6-flash:60s": 5,
57
+ "video-analysis:gemini-3.6-flash:180s": 6,
58
+ "video-analysis:gemini-3.6-flash:360s": 15,
59
+ "video-analysis:gemini-3.6-flash:600s": 25,
60
+ "video-analysis:gemini-3.1-pro:60s": 6,
61
+ "video-analysis:gemini-3.1-pro:180s": 8,
62
+ "video-analysis:gemini-3.1-pro:360s": 20,
63
+ "video-analysis:gemini-3.1-pro:600s": 33,
64
64
  // Mixed tiers (`mixed` + `mixed-fast`) share ONE credit family — they are
65
65
  // variants of the same engine plan (plan internals live in the private
66
66
  // analysis plugin). Admin-tunable via model_pricing like every other row.
67
- "video-analysis:mixed:60s": 3,
68
- "video-analysis:mixed:180s": 4,
69
- "video-analysis:mixed:360s": 9,
70
- "video-analysis:mixed:600s": 14,
67
+ "video-analysis:mixed:60s": 10,
68
+ "video-analysis:mixed:180s": 13,
69
+ "video-analysis:mixed:360s": 35,
70
+ "video-analysis:mixed:600s": 57,
71
71
  }
72
72
 
73
73
  /**