@nodaro/shared 1.17.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.17.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(15)
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", () => {
@@ -60,14 +84,14 @@ describe("LLM_MODELS data integrity", () => {
60
84
  expect(new Set(ids).size).toBe(ids.length)
61
85
  })
62
86
 
63
- it("has 4 economy, 4 standard, 7 premium models", () => {
87
+ it("has 4 economy, 4 standard, 8 premium models", () => {
64
88
  const tierCounts: Record<LlmTier, number> = { economy: 0, standard: 0, premium: 0 }
65
89
  for (const model of LLM_MODELS) {
66
90
  tierCounts[model.tier]++
67
91
  }
68
92
  expect(tierCounts.economy).toBe(4)
69
93
  expect(tierCounts.standard).toBe(4)
70
- expect(tierCounts.premium).toBe(7)
94
+ expect(tierCounts.premium).toBe(8)
71
95
  })
72
96
 
73
97
  it("all three kieFormats are represented", () => {
@@ -106,24 +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-fable-5",
125
- ]
126
- expect(LLM_MODEL_IDS).toEqual(expected)
133
+ expect(LLM_MODEL_IDS).toEqual(EXPECTED_MODEL_IDS)
127
134
  })
128
135
  })
129
136
 
@@ -399,11 +406,11 @@ describe("LLM_FEATURE_DEFAULTS", () => {
399
406
  expect(getLlmTier(LLM_FEATURE_DEFAULTS["ai-writer"])).toBe("standard")
400
407
  })
401
408
 
402
- it('"describe-to-picker" defaults to "claude-opus-4.7" (premium vision)', () => {
403
- expect(LLM_FEATURE_DEFAULTS["describe-to-picker"]).toBe("claude-opus-4.7")
409
+ it('"describe-to-picker" defaults to "claude-opus-5" (premium vision)', () => {
410
+ expect(LLM_FEATURE_DEFAULTS["describe-to-picker"]).toBe("claude-opus-5")
404
411
  expect(getLlmTier(LLM_FEATURE_DEFAULTS["describe-to-picker"])).toBe("premium")
405
412
  // The default MUST be an accepted analyzer model (vision + structured output).
406
- expect(STRUCTURED_VISION_MODELS.map((m) => m.id)).toContain("claude-opus-4.7")
413
+ expect(STRUCTURED_VISION_MODELS.map((m) => m.id)).toContain("claude-opus-5")
407
414
  })
408
415
 
409
416
  it('"generate-script" defaults to "gemini-3.6-flash" (economy)', () => {
@@ -465,6 +472,7 @@ describe("STRUCTURED_VISION_MODELS", () => {
465
472
  "gemini-3.1-pro",
466
473
  "claude-sonnet-5",
467
474
  "claude-opus-4.8",
475
+ "claude-opus-5",
468
476
  "claude-fable-5",
469
477
  // responses-format GPTs — KIE text.format json_schema live-verified
470
478
  // 2026-07-14 (text AND vision inputs).
@@ -520,6 +528,25 @@ describe("reasoning effort registry", () => {
520
528
  expect(getLlmTier("gpt-5.5")).toBe("premium")
521
529
  expect(getLlmTier("gemini-3.6-flash")).toBe("economy")
522
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
+ }
523
550
  })
524
551
 
525
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",
@@ -245,7 +263,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
245
263
  {
246
264
  id: "claude-opus-4.8",
247
265
  displayName: "Claude Opus 4.8",
248
- desc: "Most capable Claude, long-horizon work",
266
+ desc: "Previous-gen Opus, long-horizon work",
249
267
  tier: "premium",
250
268
  kieFormat: "messages",
251
269
  kieSlugOrModel: "claude-opus-4-8",
@@ -258,6 +276,28 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
258
276
  supportsTemperature: false,
259
277
  preferKie: true,
260
278
  },
279
+ {
280
+ id: "claude-opus-5",
281
+ displayName: "Claude Opus 5",
282
+ desc: "Latest Opus, deepest agentic reasoning",
283
+ tier: "premium",
284
+ kieFormat: "messages",
285
+ // KIE serves Opus 5 on the Claude-native messages dialect under the plain
286
+ // id (docs.kie.ai/market/claude/claude-opus-5.md) — same path shape as
287
+ // claude-opus-4-8 / claude-fable-5.
288
+ kieSlugOrModel: "claude-opus-5",
289
+ vendor: "anthropic",
290
+ structuredOutputMode: "anthropic-tool",
291
+ supportsImages: true,
292
+ maxOutputTokens: 16384,
293
+ directFallbackModel: "claude-opus-5",
294
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
295
+ supportsTemperature: false,
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,
300
+ },
261
301
  {
262
302
  id: "claude-fable-5",
263
303
  displayName: "Claude Fable 5",
@@ -323,7 +363,7 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
323
363
  "lottie-overlay": "claude-sonnet-4.6",
324
364
  "3d-title": "claude-sonnet-4.6",
325
365
  "image-to-text": "claude-sonnet-4.6",
326
- "describe-to-picker": "claude-opus-4.7",
366
+ "describe-to-picker": "claude-opus-5",
327
367
  "qa-check": "gemini-3.6-flash",
328
368
  "generate-script": "gemini-3.6-flash",
329
369
  "translate": "gemini-3.6-flash",
@@ -354,6 +394,7 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
354
394
  "gpt-5.6-sol": { image: true, video: false, audio: false },
355
395
  "claude-sonnet-5": { image: true, video: false, audio: false },
356
396
  "claude-opus-4.8": { image: true, video: false, audio: false },
397
+ "claude-opus-5": { image: true, video: false, audio: false },
357
398
  "claude-fable-5": { image: true, video: false, audio: false },
358
399
  }
359
400
 
@@ -84,6 +84,7 @@ export const PIPELINE_PINNABLE_SCRIPT_LLMS = [
84
84
  "claude-haiku-4-5",
85
85
  "claude-sonnet-4-6",
86
86
  "claude-opus-4-7",
87
+ "claude-opus-5",
87
88
  ] as const
88
89
  export type PipelinePinnableScriptLlm = (typeof PIPELINE_PINNABLE_SCRIPT_LLMS)[number]
89
90
 
@@ -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
+ }