@nodaro/shared 1.18.0 → 1.19.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.19.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",
@@ -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
+ }