@nodaro/shared 1.15.0 → 1.17.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.15.0",
3
+ "version": "1.17.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,17 @@ 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('"qa-check" defaults to "gemini-3.6-flash" (economy)', () => {
414
+ expect(LLM_FEATURE_DEFAULTS["qa-check"]).toBe("gemini-3.6-flash")
415
+ expect(getLlmTier(LLM_FEATURE_DEFAULTS["qa-check"])).toBe("economy")
416
+ })
417
+
418
+ it('"translate" defaults to "gemini-3.6-flash" (economy)', () => {
419
+ expect(LLM_FEATURE_DEFAULTS["translate"]).toBe("gemini-3.6-flash")
413
420
  })
414
421
 
415
422
  it("composition features default to claude-sonnet-4.6", () => {
@@ -454,9 +461,11 @@ describe("STRUCTURED_VISION_MODELS", () => {
454
461
  "claude-opus-4.7",
455
462
  "claude-sonnet-4.6",
456
463
  "gemini-3-flash",
464
+ "gemini-3.6-flash",
457
465
  "gemini-3.1-pro",
458
466
  "claude-sonnet-5",
459
467
  "claude-opus-4.8",
468
+ "claude-fable-5",
460
469
  // responses-format GPTs — KIE text.format json_schema live-verified
461
470
  // 2026-07-14 (text AND vision inputs).
462
471
  "gpt-5.4",
@@ -509,6 +518,14 @@ describe("reasoning effort registry", () => {
509
518
  expect(getLlmTier("claude-sonnet-5")).toBe("standard")
510
519
  expect(getLlmTier("claude-opus-4.8")).toBe("premium")
511
520
  expect(getLlmTier("gpt-5.5")).toBe("premium")
521
+ expect(getLlmTier("gemini-3.6-flash")).toBe("economy")
522
+ expect(getLlmTier("claude-fable-5")).toBe("premium")
523
+ })
524
+
525
+ it("gemini-3.6-flash exposes the KIE low/high thinking levels; claude-fable-5 the full Claude ladder", () => {
526
+ expect(getLlmModel("gemini-3.6-flash")?.reasoningEfforts).toEqual(["low", "high"])
527
+ expect(getLlmModel("claude-fable-5")?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"])
528
+ expect(getLlmModel("claude-fable-5")?.directFallbackModel).toBe("claude-fable-5")
512
529
  })
513
530
  })
514
531
 
@@ -551,6 +568,9 @@ describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
551
568
  expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.4", "xhigh")).toBe("llm-chat:premium")
552
569
  // gpt-5.4 is premium anyway; the real clamp case:
553
570
  expect(buildLlmCreditIdentifier("llm-chat", "gemini-3-flash", "max")).toBe("llm-chat:economy")
571
+ // gemini-3.6-flash tops out at "high" — max clamps to high, never bumps.
572
+ expect(effectiveReasoningEffort("gemini-3.6-flash", "max")).toBe("high")
573
+ expect(buildLlmCreditIdentifier("llm-chat", "gemini-3.6-flash", "max")).toBe("llm-chat:economy")
554
574
  })
555
575
  it("back-compat: no effort arg → identical to today for every model", () => {
556
576
  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
@@ -27,6 +27,8 @@ export interface FreecutImportFile {
27
27
 
28
28
  export interface NodaroLoadVideoPayload {
29
29
  readonly videoUrl: string
30
+ /** Display/file name for the primary video ("Shot 1.mp4") — optional; absent = URL-derived name (fork default). */
31
+ readonly videoName?: string
30
32
  readonly videoBuffer?: ArrayBuffer
31
33
  readonly projectJson?: unknown
32
34
  readonly additionalFiles?: ReadonlyArray<FreecutImportFile>
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",
@@ -290,9 +324,9 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
290
324
  "3d-title": "claude-sonnet-4.6",
291
325
  "image-to-text": "claude-sonnet-4.6",
292
326
  "describe-to-picker": "claude-opus-4.7",
293
- "qa-check": "claude-sonnet-4.6",
294
- "generate-script": "gemini-3-flash",
295
- "translate": "gemini-3-flash",
327
+ "qa-check": "gemini-3.6-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,