@nodaro/shared 1.14.0 → 1.16.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.14.0",
3
+ "version": "1.16.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",
@@ -24,8 +24,8 @@ import { PIPELINE_PINNABLE_SCRIPT_LLMS } from "../pipeline-types.js"
24
24
  // LLM_MODELS data integrity
25
25
  // ---------------------------------------------------------------------------
26
26
  describe("LLM_MODELS data integrity", () => {
27
- it("should have exactly 13 models", () => {
28
- expect(LLM_MODELS).toHaveLength(13)
27
+ it("should have exactly 15 models", () => {
28
+ expect(LLM_MODELS).toHaveLength(15)
29
29
  })
30
30
 
31
31
  it("each model has all required fields", () => {
@@ -60,14 +60,14 @@ describe("LLM_MODELS data integrity", () => {
60
60
  expect(new Set(ids).size).toBe(ids.length)
61
61
  })
62
62
 
63
- it("has 3 economy, 4 standard, 6 premium models", () => {
63
+ it("has 4 economy, 4 standard, 7 premium models", () => {
64
64
  const tierCounts: Record<LlmTier, number> = { economy: 0, standard: 0, premium: 0 }
65
65
  for (const model of LLM_MODELS) {
66
66
  tierCounts[model.tier]++
67
67
  }
68
- expect(tierCounts.economy).toBe(3)
68
+ expect(tierCounts.economy).toBe(4)
69
69
  expect(tierCounts.standard).toBe(4)
70
- expect(tierCounts.premium).toBe(6)
70
+ expect(tierCounts.premium).toBe(7)
71
71
  })
72
72
 
73
73
  it("all three kieFormats are represented", () => {
@@ -108,6 +108,7 @@ describe("LLM_MODEL_IDS", () => {
108
108
  it("contains all expected model ids", () => {
109
109
  const expected = [
110
110
  "gemini-3-flash",
111
+ "gemini-3.6-flash",
111
112
  "claude-haiku-4.5",
112
113
  "claude-sonnet-4.6",
113
114
  "gpt-5.2",
@@ -120,6 +121,7 @@ describe("LLM_MODEL_IDS", () => {
120
121
  "gpt-5.6-sol",
121
122
  "claude-sonnet-5",
122
123
  "claude-opus-4.8",
124
+ "claude-fable-5",
123
125
  ]
124
126
  expect(LLM_MODEL_IDS).toEqual(expected)
125
127
  })
@@ -387,8 +389,8 @@ describe("LLM_FEATURE_DEFAULTS", () => {
387
389
  }
388
390
  })
389
391
 
390
- it('"prompt-helper" defaults to "gemini-3-flash" (economy)', () => {
391
- expect(LLM_FEATURE_DEFAULTS["prompt-helper"]).toBe("gemini-3-flash")
392
+ it('"prompt-helper" defaults to "gemini-3.6-flash" (economy)', () => {
393
+ expect(LLM_FEATURE_DEFAULTS["prompt-helper"]).toBe("gemini-3.6-flash")
392
394
  expect(getLlmTier(LLM_FEATURE_DEFAULTS["prompt-helper"])).toBe("economy")
393
395
  })
394
396
 
@@ -404,12 +406,12 @@ describe("LLM_FEATURE_DEFAULTS", () => {
404
406
  expect(STRUCTURED_VISION_MODELS.map((m) => m.id)).toContain("claude-opus-4.7")
405
407
  })
406
408
 
407
- it('"generate-script" defaults to "gemini-3-flash" (economy)', () => {
408
- expect(LLM_FEATURE_DEFAULTS["generate-script"]).toBe("gemini-3-flash")
409
+ it('"generate-script" defaults to "gemini-3.6-flash" (economy)', () => {
410
+ expect(LLM_FEATURE_DEFAULTS["generate-script"]).toBe("gemini-3.6-flash")
409
411
  })
410
412
 
411
- it('"translate" defaults to "gemini-3-flash" (economy)', () => {
412
- expect(LLM_FEATURE_DEFAULTS["translate"]).toBe("gemini-3-flash")
413
+ it('"translate" defaults to "gemini-3.6-flash" (economy)', () => {
414
+ expect(LLM_FEATURE_DEFAULTS["translate"]).toBe("gemini-3.6-flash")
413
415
  })
414
416
 
415
417
  it("composition features default to claude-sonnet-4.6", () => {
@@ -454,9 +456,11 @@ describe("STRUCTURED_VISION_MODELS", () => {
454
456
  "claude-opus-4.7",
455
457
  "claude-sonnet-4.6",
456
458
  "gemini-3-flash",
459
+ "gemini-3.6-flash",
457
460
  "gemini-3.1-pro",
458
461
  "claude-sonnet-5",
459
462
  "claude-opus-4.8",
463
+ "claude-fable-5",
460
464
  // responses-format GPTs — KIE text.format json_schema live-verified
461
465
  // 2026-07-14 (text AND vision inputs).
462
466
  "gpt-5.4",
@@ -509,6 +513,14 @@ describe("reasoning effort registry", () => {
509
513
  expect(getLlmTier("claude-sonnet-5")).toBe("standard")
510
514
  expect(getLlmTier("claude-opus-4.8")).toBe("premium")
511
515
  expect(getLlmTier("gpt-5.5")).toBe("premium")
516
+ expect(getLlmTier("gemini-3.6-flash")).toBe("economy")
517
+ expect(getLlmTier("claude-fable-5")).toBe("premium")
518
+ })
519
+
520
+ it("gemini-3.6-flash exposes the KIE low/high thinking levels; claude-fable-5 the full Claude ladder", () => {
521
+ expect(getLlmModel("gemini-3.6-flash")?.reasoningEfforts).toEqual(["low", "high"])
522
+ expect(getLlmModel("claude-fable-5")?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"])
523
+ expect(getLlmModel("claude-fable-5")?.directFallbackModel).toBe("claude-fable-5")
512
524
  })
513
525
  })
514
526
 
@@ -551,6 +563,9 @@ describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
551
563
  expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.4", "xhigh")).toBe("llm-chat:premium")
552
564
  // gpt-5.4 is premium anyway; the real clamp case:
553
565
  expect(buildLlmCreditIdentifier("llm-chat", "gemini-3-flash", "max")).toBe("llm-chat:economy")
566
+ // gemini-3.6-flash tops out at "high" — max clamps to high, never bumps.
567
+ expect(effectiveReasoningEffort("gemini-3.6-flash", "max")).toBe("high")
568
+ expect(buildLlmCreditIdentifier("llm-chat", "gemini-3.6-flash", "max")).toBe("llm-chat:economy")
554
569
  })
555
570
  it("back-compat: no effort arg → identical to today for every model", () => {
556
571
  for (const m of LLM_MODELS) {
@@ -7,7 +7,7 @@ import {
7
7
  } from "../video-analysis-pricing.js"
8
8
  import {
9
9
  VIDEO_ANALYSIS_LLM_MODELS, VIDEO_ANALYSIS_TIERS, VIDEO_ANALYSIS_TIER_ORDER,
10
- VIDEO_ANALYSIS_MIXED_TIERS,
10
+ VIDEO_ANALYSIS_MIXED_TIERS, VIDEO_ANALYSIS_LEGACY_MODELS,
11
11
  DEFAULT_VIDEO_ANALYSIS_TIER, DEFAULT_VIDEO_ANALYSIS_MODEL, resolveVideoAnalysisModel,
12
12
  } from "../llm-models.js"
13
13
 
@@ -41,15 +41,28 @@ describe("video-analysis-pricing", () => {
41
41
  })
42
42
 
43
43
  it("model SSOT is capability-derived and Gemini-only today", () => {
44
- expect(VIDEO_ANALYSIS_LLM_MODELS).toEqual(["gemini-3-flash", "gemini-3.1-pro"])
44
+ expect(VIDEO_ANALYSIS_LLM_MODELS).toEqual(["gemini-3-flash", "gemini-3.6-flash", "gemini-3.1-pro"])
45
45
  })
46
46
 
47
- it("tier layer: every model-backed tier maps to a real model AND every model is tier-reachable (no vendor leak)", () => {
47
+ it("tier layer: every model-backed tier maps to a real model AND every model is tier-reachable or explicitly legacy (no vendor leak)", () => {
48
48
  // Adding a video-analysis model without a tier would silently leave it
49
- // unreachable / unnamed — this fails until a tier decision is made.
49
+ // unreachable / unnamed — this fails until an explicit decision is made:
50
+ // either a tier targets it, or it's acknowledged in
51
+ // VIDEO_ANALYSIS_LEGACY_MODELS (kept only for stored raw-id configs).
50
52
  const tierTargets = Object.values(VIDEO_ANALYSIS_TIERS)
53
+ const legacy = Object.keys(VIDEO_ANALYSIS_LEGACY_MODELS)
51
54
  for (const m of tierTargets) expect(VIDEO_ANALYSIS_LLM_MODELS).toContain(m)
52
- for (const m of VIDEO_ANALYSIS_LLM_MODELS) expect(tierTargets).toContain(m)
55
+ for (const m of VIDEO_ANALYSIS_LLM_MODELS) {
56
+ expect([...tierTargets, ...legacy], `${m} needs a tier or a legacy entry`).toContain(m)
57
+ }
58
+ // Legacy entries are strictly the OTHER side of that decision: a legacy
59
+ // model is never also a tier target, must still be video-capable, and must
60
+ // point at a real tier (the tier it used to back — UIs reverse-map via it).
61
+ for (const [m, tier] of Object.entries(VIDEO_ANALYSIS_LEGACY_MODELS)) {
62
+ expect(tierTargets).not.toContain(m)
63
+ expect(VIDEO_ANALYSIS_LLM_MODELS).toContain(m)
64
+ expect(Object.keys(VIDEO_ANALYSIS_TIERS)).toContain(tier)
65
+ }
53
66
  // TIER_ORDER = model-backed tiers + mixed roll-plan tiers, exactly.
54
67
  expect(new Set(VIDEO_ANALYSIS_TIER_ORDER)).toEqual(
55
68
  new Set([...Object.keys(VIDEO_ANALYSIS_TIERS), ...VIDEO_ANALYSIS_MIXED_TIERS]),
@@ -59,14 +72,25 @@ describe("video-analysis-pricing", () => {
59
72
  for (const t of VIDEO_ANALYSIS_MIXED_TIERS) expect(VIDEO_ANALYSIS_LLM_MODELS).not.toContain(t)
60
73
  })
61
74
 
75
+ it("fast tier never prices above pro in any bucket", () => {
76
+ for (const bucketSec of VIDEO_ANALYSIS_DURATION_BUCKETS) {
77
+ const fast = VIDEO_ANALYSIS_BUCKET_CREDITS[buildVideoAnalysisCreditId(VIDEO_ANALYSIS_TIERS.fast, bucketSec)]!
78
+ const pro = VIDEO_ANALYSIS_BUCKET_CREDITS[buildVideoAnalysisCreditId(VIDEO_ANALYSIS_TIERS.pro, bucketSec)]!
79
+ expect(fast, `fast > pro at ${bucketSec}s`).toBeLessThanOrEqual(pro)
80
+ }
81
+ })
82
+
62
83
  it("resolveVideoAnalysisModel: tier → model, mixed → sentinel, raw model passthrough, default pro on empty/unknown", () => {
63
84
  expect(DEFAULT_VIDEO_ANALYSIS_TIER).toBe("pro")
64
85
  expect(DEFAULT_VIDEO_ANALYSIS_MODEL).toBe("gemini-3.1-pro")
65
86
  expect(resolveVideoAnalysisModel("pro")).toBe("gemini-3.1-pro")
66
- expect(resolveVideoAnalysisModel("fast")).toBe("gemini-3-flash")
87
+ expect(resolveVideoAnalysisModel("fast")).toBe("gemini-3.6-flash")
67
88
  expect(resolveVideoAnalysisModel("mixed")).toBe("mixed") // roll-plan sentinel passthrough
68
89
  expect(resolveVideoAnalysisModel("mixed-fast")).toBe("mixed-fast")
69
- expect(resolveVideoAnalysisModel("gemini-3-flash")).toBe("gemini-3-flash") // raw passthrough
90
+ // Raw passthrough keeps LEGACY stored configs running (and priced) on the
91
+ // exact model they were saved with — never silently re-tiered.
92
+ expect(resolveVideoAnalysisModel("gemini-3-flash")).toBe("gemini-3-flash")
93
+ expect(resolveVideoAnalysisModel("gemini-3.6-flash")).toBe("gemini-3.6-flash")
70
94
  expect(resolveVideoAnalysisModel(undefined)).toBe("gemini-3.1-pro") // default → pro
71
95
  expect(resolveVideoAnalysisModel("")).toBe("gemini-3.1-pro")
72
96
  expect(resolveVideoAnalysisModel("nonsense")).toBe("gemini-3.1-pro") // unknown → default, never throws
@@ -104,6 +104,23 @@ describe("appearance variations (cast-variations spec §4)", () => {
104
104
  })
105
105
  })
106
106
 
107
+ describe("variationFolds (cast-variations §4/§6 — review F5)", () => {
108
+ const meta = { durationSec: 10, width: 1920, height: 1080, aspectRatio: "16:9" }
109
+ const doc = {
110
+ meta,
111
+ slots: [slot],
112
+ scenes: [{ ...baseScene, sceneNumber: 1, visualResolved: "a man juggles", slotRefs: ["hero"] }],
113
+ }
114
+ it("videoAnalysisResultSchema round-trips variationFolds — strip-mode consumers keep the §6 fold note", () => {
115
+ const withFolds = { ...doc, variationFolds: [{ slotId: "hero", variationId: "era", label: "Era" }] }
116
+ expect(videoAnalysisResultSchema.parse(withFolds)).toEqual(withFolds)
117
+ })
118
+ it("absent variationFolds stays absent (no [] materialization)", () => {
119
+ const parsed = videoAnalysisResultSchema.parse(doc)
120
+ expect("variationFolds" in parsed && parsed.variationFolds !== undefined).toBe(false)
121
+ })
122
+ })
123
+
107
124
  describe("binding rewrite helpers (merge consumes — spec §4)", () => {
108
125
  it("rewriteSceneBindings renames slot keys and per-slot variation values", () => {
109
126
  expect(rewriteSceneBindings({ "man-2": "dream", other: "era" }, { "man-2": "hero" }, { hero: { dream: "flashback" } }))
package/src/index.ts CHANGED
@@ -256,6 +256,7 @@ export {
256
256
  STRUCTURED_VISION_MODELS,
257
257
  VIDEO_ANALYSIS_LLM_MODELS,
258
258
  VIDEO_ANALYSIS_TIERS,
259
+ VIDEO_ANALYSIS_LEGACY_MODELS,
259
260
  type VideoAnalysisTier,
260
261
  type VideoAnalysisModelTier,
261
262
  VIDEO_ANALYSIS_MIXED_TIERS,
package/src/llm-models.ts CHANGED
@@ -72,6 +72,24 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
72
72
  supportsImages: true,
73
73
  maxOutputTokens: 8192,
74
74
  },
75
+ {
76
+ id: "gemini-3.6-flash",
77
+ displayName: "Gemini 3.6 Flash",
78
+ desc: "Latest fast Gemini, sharper reasoning",
79
+ tier: "economy",
80
+ kieFormat: "chat-completions",
81
+ // KIE serves Gemini 3.6 Flash on the OpenAI-compatible dialect under this
82
+ // slug (docs.kie.ai/market/gemini/gemini-3-6-flash-openai.md) — same
83
+ // chat-completions path shape as gemini-3-flash, different slug prefix.
84
+ kieSlugOrModel: "gemini-3-6-flash-openai",
85
+ vendor: "google",
86
+ structuredOutputMode: "kie-response-format",
87
+ supportsImages: true,
88
+ maxOutputTokens: 8192,
89
+ // KIE's 3.6 endpoint accepts `reasoning_effort: low | high` (thinking
90
+ // level) — exactly the chat-completions wire mapping deriveParams sends.
91
+ reasoningEfforts: ["low", "high"],
92
+ },
75
93
  {
76
94
  id: "claude-haiku-4.5",
77
95
  displayName: "Claude Haiku 4.5",
@@ -240,6 +258,22 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
240
258
  supportsTemperature: false,
241
259
  preferKie: true,
242
260
  },
261
+ {
262
+ id: "claude-fable-5",
263
+ displayName: "Claude Fable 5",
264
+ desc: "Frontier Claude above Opus, hardest problems",
265
+ tier: "premium",
266
+ kieFormat: "messages",
267
+ kieSlugOrModel: "claude-fable-5",
268
+ vendor: "anthropic",
269
+ structuredOutputMode: "anthropic-tool",
270
+ supportsImages: true,
271
+ maxOutputTokens: 16384,
272
+ directFallbackModel: "claude-fable-5",
273
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
274
+ supportsTemperature: false,
275
+ preferKie: true,
276
+ },
243
277
  ] as const
244
278
 
245
279
  export const LLM_MODEL_IDS = LLM_MODELS.map((m) => m.id)
@@ -280,8 +314,8 @@ export function motionGraphicsFeature(engine?: string): LlmFeature {
280
314
  /** Feature → default model when user hasn't selected one */
281
315
  export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
282
316
  "ai-writer": "claude-sonnet-4.6",
283
- "llm-chat": "gemini-3-flash",
284
- "prompt-helper": "gemini-3-flash",
317
+ "llm-chat": "gemini-3.6-flash",
318
+ "prompt-helper": "gemini-3.6-flash",
285
319
  "scene-graph-ai": "claude-sonnet-4.6",
286
320
  "after-effects": "claude-sonnet-4.6",
287
321
  "motion-graphics": "claude-sonnet-4.6",
@@ -291,8 +325,8 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
291
325
  "image-to-text": "claude-sonnet-4.6",
292
326
  "describe-to-picker": "claude-opus-4.7",
293
327
  "qa-check": "claude-sonnet-4.6",
294
- "generate-script": "gemini-3-flash",
295
- "translate": "gemini-3-flash",
328
+ "generate-script": "gemini-3.6-flash",
329
+ "translate": "gemini-3.6-flash",
296
330
  "image-critic": "claude-sonnet-4.6",
297
331
  }
298
332
 
@@ -307,6 +341,7 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
307
341
  */
308
342
  export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean; audio: boolean }> = {
309
343
  "gemini-3-flash": { image: true, video: true, audio: true },
344
+ "gemini-3.6-flash": { image: true, video: true, audio: true },
310
345
  "gemini-3.1-pro": { image: true, video: true, audio: true },
311
346
  "claude-haiku-4.5": { image: true, video: false, audio: false },
312
347
  "claude-sonnet-4.6": { image: true, video: false, audio: false },
@@ -319,6 +354,7 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
319
354
  "gpt-5.6-sol": { image: true, video: false, audio: false },
320
355
  "claude-sonnet-5": { image: true, video: false, audio: false },
321
356
  "claude-opus-4.8": { image: true, video: false, audio: false },
357
+ "claude-fable-5": { image: true, video: false, audio: false },
322
358
  }
323
359
 
324
360
  /** Capability lookup with safe default — unknown models get image-only. */
@@ -416,8 +452,23 @@ export const VIDEO_ANALYSIS_LLM_MODELS: string[] = LLM_MODELS
416
452
  * VIDEO_ANALYSIS_LLM_MODELS member AND every such model is reachable by a tier,
417
453
  * so adding a video model forces a tier decision instead of silently leaking.
418
454
  */
419
- export const VIDEO_ANALYSIS_TIERS = { fast: "gemini-3-flash", pro: "gemini-3.1-pro" } as const
455
+ export const VIDEO_ANALYSIS_TIERS = { fast: "gemini-3.6-flash", pro: "gemini-3.1-pro" } as const
420
456
  export type VideoAnalysisModelTier = keyof typeof VIDEO_ANALYSIS_TIERS
457
+ /**
458
+ * Models that PREVIOUSLY backed a tier, mapped to the tier they backed.
459
+ * They stay video+audio-capable (so they remain in VIDEO_ANALYSIS_LLM_MODELS):
460
+ * stored raw `llmModel` values keep resolving via the passthrough in
461
+ * `resolveVideoAnalysisModel` and keep pricing under their own credit family —
462
+ * but no tier reaches them for NEW runs. UIs use this map to reverse a stored
463
+ * raw id to the tier the user originally chose (video-configs fail-safe).
464
+ * The tier guard test requires every video-capable model to be a tier target
465
+ * OR a key here, so replacing a tier's backing model stays an explicit,
466
+ * two-sided decision.
467
+ */
468
+ export const VIDEO_ANALYSIS_LEGACY_MODELS: Record<string, VideoAnalysisModelTier> = {
469
+ // fast tier's backing model until 2026-07 (superseded by gemini-3.6-flash)
470
+ "gemini-3-flash": "fast",
471
+ }
421
472
  /**
422
473
  * MIXED tiers — advanced multi-engine analysis plans whose identifier resolves
423
474
  * to an engine-plan SENTINEL consumed by the analysis engine, never to a single
@@ -1810,9 +1810,9 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1810
1810
  kind: "video",
1811
1811
  modes: ["video-analysis"] as const,
1812
1812
  family: "Nodaro",
1813
- label: "Video Analysis (Fast)",
1813
+ label: "Video Analysis (Fast — legacy)",
1814
1814
  series: "Video Analysis",
1815
- description: "Analyze a video into a structured shot list (scenes, camera, audio) fast, economy tier. Billed per duration bucket.",
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.",
1816
1816
  useCases: ["video-analysis", "shot-list", "fast"],
1817
1817
  pricing: [
1818
1818
  { identifier: "video-analysis:gemini-3-flash", credits: 3, note: "10-min ceiling (no duration given)" },
@@ -1822,6 +1822,23 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1822
1822
  { identifier: "video-analysis:gemini-3-flash:600s", credits: 3, note: "10-min ceiling" },
1823
1823
  ],
1824
1824
  },
1825
+ "gemini-3.6-flash-video-analysis": {
1826
+ id: "gemini-3.6-flash-video-analysis",
1827
+ kind: "video",
1828
+ modes: ["video-analysis"] as const,
1829
+ family: "Nodaro",
1830
+ label: "Video Analysis (Fast)",
1831
+ series: "Video Analysis",
1832
+ description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast, economy tier. Billed per duration bucket.",
1833
+ useCases: ["video-analysis", "shot-list", "fast"],
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" },
1840
+ ],
1841
+ },
1825
1842
  "gemini-3.1-pro-video-analysis": {
1826
1843
  id: "gemini-3.1-pro-video-analysis",
1827
1844
  kind: "video",
@@ -46,10 +46,17 @@ export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, O
46
46
  * `docs/nodes/processing-video/video-analysis.md`.
47
47
  */
48
48
  export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
49
+ // Legacy fast-tier model (pre-2026-07) — kept for stored raw-id configs.
49
50
  "video-analysis:gemini-3-flash:60s": 1,
50
51
  "video-analysis:gemini-3-flash:180s": 1,
51
52
  "video-analysis:gemini-3-flash:360s": 2,
52
53
  "video-analysis:gemini-3-flash:600s": 3,
54
+ // Current fast tier — regenerated from the private formula for its backing
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,
53
60
  "video-analysis:gemini-3.1-pro:60s": 2,
54
61
  "video-analysis:gemini-3.1-pro:180s": 3,
55
62
  "video-analysis:gemini-3.1-pro:360s": 7,
@@ -128,6 +128,16 @@ export const videoAnalysisResultSchema = z.object({
128
128
  }),
129
129
  slots: z.array(entitySlotSchema),
130
130
  scenes: z.array(analyzedSceneSchema).min(1),
131
+ /** CAST VARIATIONS (§4 cap handling): looks the analyzer's merge FOLDED into
132
+ * the default at VIDEO_ANALYSIS_MAX_VARIATIONS — recorded, never silent. In
133
+ * the wire schema so strip-mode consumers (the recast client's validated
134
+ * blueprint view) keep it: the §6 "folded into default look" note is the
135
+ * user's only pre-pay defense against a wrong split. */
136
+ variationFolds: z.array(z.object({
137
+ slotId: z.string(),
138
+ variationId: z.string(),
139
+ label: z.string(),
140
+ })).optional(),
131
141
  })
132
142
  export type VideoAnalysisResult = z.infer<typeof videoAnalysisResultSchema>
133
143