@nodaro/shared 1.9.0 → 1.11.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/dist/index.cjs +142 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -46
- package/dist/index.d.ts +140 -46
- package/dist/index.js +133 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +26 -12
- package/src/__tests__/image-ref-limit.test.ts +2 -0
- package/src/__tests__/llm-models.test.ts +10 -3
- package/src/__tests__/prompt-length-limits.test.ts +3 -0
- package/src/__tests__/seedance2-continuation-ref.test.ts +24 -0
- package/src/__tests__/video-analysis-pricing.test.ts +28 -5
- package/src/__tests__/video-analysis.test.ts +1 -1
- package/src/__tests__/video-audio-capability.test.ts +34 -6
- package/src/credit-identifiers.ts +8 -2
- package/src/index.ts +11 -0
- package/src/llm-models.ts +52 -9
- package/src/model-catalog.ts +64 -9
- package/src/model-constants.ts +94 -16
- package/src/node-default-mappings.ts +3 -0
- package/src/video-analysis-pricing.ts +14 -10
- package/src/video-analysis.ts +12 -4
package/package.json
CHANGED
|
@@ -17,6 +17,8 @@ describe("buildCreditModelIdentifier", () => {
|
|
|
17
17
|
"seedream-edit",
|
|
18
18
|
"seedream-5-lite",
|
|
19
19
|
"seedream-5-lite-i2i",
|
|
20
|
+
"seedream-5-pro",
|
|
21
|
+
"seedream-5-pro-i2i",
|
|
20
22
|
]
|
|
21
23
|
|
|
22
24
|
it.each(highQualityProviders)(
|
|
@@ -311,31 +313,31 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
311
313
|
// --- Duration tiers for kling-3.0 ---
|
|
312
314
|
describe("kling-3.0 duration tiers", () => {
|
|
313
315
|
it("5s duration returns :5s tier", () => {
|
|
314
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 5)).toBe("kling-3.0:5s")
|
|
316
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, false)).toBe("kling-3.0:5s")
|
|
315
317
|
})
|
|
316
318
|
|
|
317
319
|
it("3s duration falls into 5s tier", () => {
|
|
318
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 3)).toBe("kling-3.0:5s")
|
|
320
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 3, false)).toBe("kling-3.0:5s")
|
|
319
321
|
})
|
|
320
322
|
|
|
321
323
|
it("10s duration returns :10s tier", () => {
|
|
322
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 10)).toBe("kling-3.0:10s")
|
|
324
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 10, false)).toBe("kling-3.0:10s")
|
|
323
325
|
})
|
|
324
326
|
|
|
325
327
|
it("7s duration falls into 10s tier", () => {
|
|
326
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 7)).toBe("kling-3.0:10s")
|
|
328
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 7, false)).toBe("kling-3.0:10s")
|
|
327
329
|
})
|
|
328
330
|
|
|
329
331
|
it("15s duration returns :15s tier", () => {
|
|
330
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 15)).toBe("kling-3.0:15s")
|
|
332
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 15, false)).toBe("kling-3.0:15s")
|
|
331
333
|
})
|
|
332
334
|
|
|
333
335
|
it("duration exceeding max tier clamps to last tier (20s -> 15s)", () => {
|
|
334
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 20)).toBe("kling-3.0:15s")
|
|
336
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 20, false)).toBe("kling-3.0:15s")
|
|
335
337
|
})
|
|
336
338
|
|
|
337
339
|
it("duration exceeding max tier clamps to last tier (100s -> 15s)", () => {
|
|
338
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 100)).toBe("kling-3.0:15s")
|
|
340
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 100, false)).toBe("kling-3.0:15s")
|
|
339
341
|
})
|
|
340
342
|
})
|
|
341
343
|
|
|
@@ -349,6 +351,18 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
349
351
|
expect(buildVideoCreditModelIdentifier("kling", 5, true)).toBe("kling:5s:audio")
|
|
350
352
|
})
|
|
351
353
|
|
|
354
|
+
it("kling-3.0 + sound UNDEFINED appends :audio (model default is audio ON)", () => {
|
|
355
|
+
// The provider layer generates audio when the caller expressed no intent
|
|
356
|
+
// (models.ts extraParams.sound: true + kling3-client `?? true`). Billing
|
|
357
|
+
// must mirror that via capability defaultOn — reserving the no-audio
|
|
358
|
+
// tier against an audio-on generation was a silent under-bill.
|
|
359
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, undefined)).toBe("kling-3.0:5s:audio")
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("kling (2.6) + sound UNDEFINED does not append :audio (model default is OFF)", () => {
|
|
363
|
+
expect(buildVideoCreditModelIdentifier("kling", 5, undefined)).toBe("kling:5s")
|
|
364
|
+
})
|
|
365
|
+
|
|
352
366
|
it("kling-3.0 + sound=false does not append :audio", () => {
|
|
353
367
|
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, false)).toBe("kling-3.0:5s")
|
|
354
368
|
})
|
|
@@ -405,7 +419,7 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
405
419
|
// --- String duration parsing ---
|
|
406
420
|
describe("duration parsing", () => {
|
|
407
421
|
it('string duration "10" is parsed as number 10', () => {
|
|
408
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", "10")).toBe("kling-3.0:10s")
|
|
422
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", "10", false)).toBe("kling-3.0:10s")
|
|
409
423
|
})
|
|
410
424
|
|
|
411
425
|
it('string duration "5" works', () => {
|
|
@@ -413,19 +427,19 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
413
427
|
})
|
|
414
428
|
|
|
415
429
|
it("NaN duration defaults to 5", () => {
|
|
416
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", "abc")).toBe("kling-3.0:5s")
|
|
430
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", "abc", false)).toBe("kling-3.0:5s")
|
|
417
431
|
})
|
|
418
432
|
|
|
419
433
|
it("undefined duration defaults to 5", () => {
|
|
420
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0")).toBe("kling-3.0:5s")
|
|
434
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", undefined, false)).toBe("kling-3.0:5s")
|
|
421
435
|
})
|
|
422
436
|
|
|
423
437
|
it("0 duration falls into first tier", () => {
|
|
424
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 0)).toBe("kling-3.0:5s")
|
|
438
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 0, false)).toBe("kling-3.0:5s")
|
|
425
439
|
})
|
|
426
440
|
|
|
427
441
|
it("1 duration falls into first tier", () => {
|
|
428
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 1)).toBe("kling-3.0:5s")
|
|
442
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 1, false)).toBe("kling-3.0:5s")
|
|
429
443
|
})
|
|
430
444
|
})
|
|
431
445
|
|
|
@@ -16,7 +16,9 @@ describe("imageReferenceLimit", () => {
|
|
|
16
16
|
"flux-2-max": 8,
|
|
17
17
|
"gpt-image-2": 16, // → gpt-image-2-i2i
|
|
18
18
|
"seedream-5-lite": 16, // → seedream-5-lite-i2i
|
|
19
|
+
"seedream-5-pro": 16, // → seedream-5-pro-i2i
|
|
19
20
|
"nano-banana-2": 4,
|
|
21
|
+
"nano-banana-2-lite": 10, // schema maxItems (image_urls)
|
|
20
22
|
flux: 4, // → flux-pro-i2i
|
|
21
23
|
"flux-2-klein": 1, // product cap (schema array slices at 5)
|
|
22
24
|
"flux-2-pro": 4, // product cap (BFL schema accepts 8)
|
|
@@ -430,20 +430,27 @@ describe("STRUCTURED_VISION_MODELS", () => {
|
|
|
430
430
|
"gemini-3.1-pro",
|
|
431
431
|
"claude-sonnet-5",
|
|
432
432
|
"claude-opus-4.8",
|
|
433
|
+
// responses-format GPTs — KIE text.format json_schema live-verified
|
|
434
|
+
// 2026-07-14 (text AND vision inputs).
|
|
435
|
+
"gpt-5.4",
|
|
436
|
+
"gpt-5.5",
|
|
437
|
+
"gpt-5.6-luna",
|
|
438
|
+
"gpt-5.6-terra",
|
|
439
|
+
"gpt-5.6-sol",
|
|
433
440
|
].sort(),
|
|
434
441
|
)
|
|
435
442
|
})
|
|
436
443
|
|
|
437
|
-
it("includes Anthropic (forced-tool)
|
|
444
|
+
it("includes Anthropic (forced-tool), Gemini (response_format), and OpenAI (responses text.format) vendors", () => {
|
|
438
445
|
const vendors = new Set(STRUCTURED_VISION_MODELS.map((m) => m.vendor))
|
|
439
446
|
expect(vendors).toContain("anthropic")
|
|
440
447
|
expect(vendors).toContain("google")
|
|
448
|
+
expect(vendors).toContain("openai")
|
|
441
449
|
})
|
|
442
450
|
|
|
443
|
-
it("excludes GPT models — no native structured mode (parse+retry only)", () => {
|
|
451
|
+
it("excludes chat-completions GPT models — no native structured mode there (parse+retry only)", () => {
|
|
444
452
|
const ids = STRUCTURED_VISION_MODELS.map((m) => m.id)
|
|
445
453
|
expect(ids).not.toContain("gpt-5.2")
|
|
446
|
-
expect(ids).not.toContain("gpt-5.4")
|
|
447
454
|
})
|
|
448
455
|
|
|
449
456
|
it("every member is vision-capable and has a structuredOutputMode", () => {
|
|
@@ -28,6 +28,7 @@ describe("per-model prompt length limits", () => {
|
|
|
28
28
|
describe("getMaxImagePromptChars", () => {
|
|
29
29
|
it("returns the verified higher caps (provider supports more than 5000)", () => {
|
|
30
30
|
expect(getMaxImagePromptChars("nano-banana-2")).toBe(20000)
|
|
31
|
+
expect(getMaxImagePromptChars("nano-banana-2-lite")).toBe(20000)
|
|
31
32
|
expect(getMaxImagePromptChars("nano-banana-pro")).toBe(20000)
|
|
32
33
|
expect(getMaxImagePromptChars("gpt-image-2-i2i")).toBe(20000)
|
|
33
34
|
})
|
|
@@ -44,6 +45,8 @@ describe("per-model prompt length limits", () => {
|
|
|
44
45
|
it("falls back to IMAGE_PROMPT_MAX for verified-default + unknown providers", () => {
|
|
45
46
|
expect(getMaxImagePromptChars("flux")).toBe(IMAGE_PROMPT_MAX)
|
|
46
47
|
expect(getMaxImagePromptChars("imagen4")).toBe(IMAGE_PROMPT_MAX)
|
|
48
|
+
expect(getMaxImagePromptChars("seedream-5-pro")).toBe(IMAGE_PROMPT_MAX) // verified 5000 in KIE schema
|
|
49
|
+
expect(getMaxImagePromptChars("seedream-5-pro-i2i")).toBe(IMAGE_PROMPT_MAX) // verified 5000 in KIE schema
|
|
47
50
|
expect(getMaxImagePromptChars("grok-i2i")).toBe(IMAGE_PROMPT_MAX) // 390000 doc = sanity-capped
|
|
48
51
|
expect(getMaxImagePromptChars("flux-kontext")).toBe(IMAGE_PROMPT_MAX) // UNVERIFIED
|
|
49
52
|
expect(getMaxImagePromptChars(undefined)).toBe(IMAGE_PROMPT_MAX)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
SEEDANCE_2_CONTINUATION_REF_SEC,
|
|
4
|
+
SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC,
|
|
5
|
+
SEEDANCE_2_EXTEND_STITCH,
|
|
6
|
+
} from "../model-constants.js"
|
|
7
|
+
|
|
8
|
+
/** KIE hard-rejects r2v reference videos below the floor: "the parameter
|
|
9
|
+
* video duration (seconds) specified in the request must be greater than or
|
|
10
|
+
* equal to 1.8 for model dreamina-seedance-2-0-fast in r2v" (2026-07-13,
|
|
11
|
+
* job dbf95612 — the original 1.0s tails made every generate-video-pro
|
|
12
|
+
* continuation segment fail deterministically). Everything this platform
|
|
13
|
+
* sends as a Seedance-2 video reference cuts SEEDANCE_2_CONTINUATION_REF_SEC
|
|
14
|
+
* seconds; do NOT "optimize" it back below the floor. The private-plugin
|
|
15
|
+
* twin constants (nodaro-cloud-plugins chain.ts TAIL_SEC / bridge-math.ts
|
|
16
|
+
* MIN_REF) are guarded by that repo's r2v-ref-floor.test.ts — keep in sync. */
|
|
17
|
+
describe("seedance-2 continuation-reference floor (do not regress)", () => {
|
|
18
|
+
it("the platform's continuation-ref length clears KIE's r2v minimum", () => {
|
|
19
|
+
expect(SEEDANCE_2_CONTINUATION_REF_SEC).toBeGreaterThanOrEqual(SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC)
|
|
20
|
+
})
|
|
21
|
+
it("the extend node's tail IS the shared continuation-ref length (single source)", () => {
|
|
22
|
+
expect(SEEDANCE_2_EXTEND_STITCH.referenceTailSeconds).toBe(SEEDANCE_2_CONTINUATION_REF_SEC)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -5,13 +5,16 @@ import {
|
|
|
5
5
|
pickVideoAnalysisBucket, buildVideoAnalysisCreditId, bucketSecondsFromCreditId,
|
|
6
6
|
videoAnalysisNumWindows,
|
|
7
7
|
} from "../video-analysis-pricing.js"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
VIDEO_ANALYSIS_LLM_MODELS, VIDEO_ANALYSIS_TIERS, VIDEO_ANALYSIS_TIER_ORDER,
|
|
10
|
+
DEFAULT_VIDEO_ANALYSIS_TIER, DEFAULT_VIDEO_ANALYSIS_MODEL, resolveVideoAnalysisModel,
|
|
11
|
+
} from "../llm-models.js"
|
|
9
12
|
|
|
10
13
|
// The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
|
|
11
|
-
// formula
|
|
12
|
-
// tests (
|
|
13
|
-
// cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
|
|
14
|
-
//
|
|
14
|
+
// formula are PRIVATE, in @nodaroai/cloud-plugins
|
|
15
|
+
// (src/plugins/video-analysis/cost.ts) — its tests (the worked-example
|
|
16
|
+
// bucket-credit values and the cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
|
|
17
|
+
// below) live in that private package's __tests__/cost.test.ts. This file
|
|
15
18
|
// covers only the NON-monetary duration-bucketing, window-batching, and
|
|
16
19
|
// credit-id-construction logic that stays in the published package.
|
|
17
20
|
|
|
@@ -40,6 +43,26 @@ describe("video-analysis-pricing", () => {
|
|
|
40
43
|
expect(VIDEO_ANALYSIS_LLM_MODELS).toEqual(["gemini-3-flash", "gemini-3.1-pro"])
|
|
41
44
|
})
|
|
42
45
|
|
|
46
|
+
it("tier layer: every tier maps to a real model AND every model is tier-reachable (no vendor leak)", () => {
|
|
47
|
+
// Adding a video-analysis model without a tier would silently leave it
|
|
48
|
+
// unreachable / unnamed — this fails until a tier decision is made.
|
|
49
|
+
const tierTargets = Object.values(VIDEO_ANALYSIS_TIERS)
|
|
50
|
+
for (const m of tierTargets) expect(VIDEO_ANALYSIS_LLM_MODELS).toContain(m)
|
|
51
|
+
for (const m of VIDEO_ANALYSIS_LLM_MODELS) expect(tierTargets).toContain(m)
|
|
52
|
+
expect(new Set(VIDEO_ANALYSIS_TIER_ORDER)).toEqual(new Set(Object.keys(VIDEO_ANALYSIS_TIERS)))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("resolveVideoAnalysisModel: tier → model, raw model passthrough, default pro on empty/unknown", () => {
|
|
56
|
+
expect(DEFAULT_VIDEO_ANALYSIS_TIER).toBe("pro")
|
|
57
|
+
expect(DEFAULT_VIDEO_ANALYSIS_MODEL).toBe("gemini-3.1-pro")
|
|
58
|
+
expect(resolveVideoAnalysisModel("pro")).toBe("gemini-3.1-pro")
|
|
59
|
+
expect(resolveVideoAnalysisModel("fast")).toBe("gemini-3-flash")
|
|
60
|
+
expect(resolveVideoAnalysisModel("gemini-3-flash")).toBe("gemini-3-flash") // raw passthrough
|
|
61
|
+
expect(resolveVideoAnalysisModel(undefined)).toBe("gemini-3.1-pro") // default → pro
|
|
62
|
+
expect(resolveVideoAnalysisModel("")).toBe("gemini-3.1-pro")
|
|
63
|
+
expect(resolveVideoAnalysisModel("nonsense")).toBe("gemini-3.1-pro") // unknown → default, never throws
|
|
64
|
+
})
|
|
65
|
+
|
|
43
66
|
// Full drift-detection against the live $-formula lives in
|
|
44
67
|
// backend/src/lib/pricing/__tests__/video-analysis-cost.test.ts (this
|
|
45
68
|
// package cannot see the formula post-S5). This is a lightweight shape
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../video-analysis.js"
|
|
8
8
|
|
|
9
9
|
const slot: EntitySlot = { slotId: "hero", label: "Protagonist", source: "wired-character", role: "person", description: "tan man, mustache, black tee" }
|
|
10
|
-
const baseScene = { startSec: 0, endSec: 4, label: "Hook", shotType: "Medium Close-Up", camera: "slow push-in", visual: "{slot:hero} juggles a ball", audio: { mode: "speech" as const, content: "As a kid…", voice: "male, warm" } }
|
|
10
|
+
const baseScene = { startSec: 0, endSec: 4, label: "Hook", shotType: "Medium Close-Up", camera: "slow push-in", visual: "{slot:hero} juggles a ball", audio: [{ mode: "speech" as const, content: "As a kid…", voice: "male, warm" }] }
|
|
11
11
|
|
|
12
12
|
describe("windowAnalysisSchema", () => {
|
|
13
13
|
it("accepts a zero-scene window (quiet footage is a VALID result)", () => {
|
|
@@ -22,15 +22,33 @@ describe("getVideoAudioCapability", () => {
|
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it("returns
|
|
25
|
+
it("returns native_speech (sound toggle, cost-affecting) for KIE Kling", () => {
|
|
26
|
+
// Probe-verified 2026-07-16: scripted quoted dialogue comes back verbatim
|
|
27
|
+
// with articulated lips on the KIE path for BOTH kling (2.6) and kling-3.0.
|
|
26
28
|
for (const m of ["kling", "kling-3.0"]) {
|
|
27
29
|
const cap = getVideoAudioCapability(m)
|
|
28
|
-
expect(cap.mode, m).toBe("
|
|
30
|
+
expect(cap.mode, m).toBe("native_speech")
|
|
29
31
|
expect(cap.field, m).toBe("sound")
|
|
30
32
|
expect(cap.affectsCost, m).toBe(true)
|
|
31
33
|
}
|
|
32
34
|
})
|
|
33
35
|
|
|
36
|
+
it("returns native_speech (generateAudio lever, flat-priced) for kling-3-omni", () => {
|
|
37
|
+
const cap = getVideoAudioCapability("kling-3-omni")
|
|
38
|
+
expect(cap.mode).toBe("native_speech")
|
|
39
|
+
expect(cap.field).toBe("generateAudio")
|
|
40
|
+
// Audio is priced into the Replicate flat per-duration rate — no :audio composite.
|
|
41
|
+
expect(cap.affectsCost).toBeUndefined()
|
|
42
|
+
expect(cap.defaultOn).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("defaultOn mirrors each model's own config default", () => {
|
|
46
|
+
// kling-3.0 generates audio unless explicitly disabled (models.ts
|
|
47
|
+
// extraParams.sound: true + kling3-client `?? true`); kling 2.6 defaults off.
|
|
48
|
+
expect(getVideoAudioCapability("kling-3.0").defaultOn).toBe(true)
|
|
49
|
+
expect(getVideoAudioCapability("kling").defaultOn).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
34
52
|
it("returns audio_driven for Seedance 2.0", () => {
|
|
35
53
|
for (const m of ["seedance-2", "seedance-2-fast"]) {
|
|
36
54
|
const cap = getVideoAudioCapability(m)
|
|
@@ -66,6 +84,7 @@ describe("videoModelSupportsAudio", () => {
|
|
|
66
84
|
it("is true for any model with an audio mode, false for silent", () => {
|
|
67
85
|
expect(videoModelSupportsAudio("veo3")).toBe(true)
|
|
68
86
|
expect(videoModelSupportsAudio("kling-3.0")).toBe(true)
|
|
87
|
+
expect(videoModelSupportsAudio("kling-3-omni")).toBe(true)
|
|
69
88
|
expect(videoModelSupportsAudio("seedance-2")).toBe(true)
|
|
70
89
|
expect(videoModelSupportsAudio("seedance")).toBe(true)
|
|
71
90
|
expect(videoModelSupportsAudio("minimax")).toBe(false)
|
|
@@ -80,9 +99,12 @@ describe("videoModelCanSpeakDialogue", () => {
|
|
|
80
99
|
expect(videoModelCanSpeakDialogue("seedance-2")).toBe(true)
|
|
81
100
|
expect(videoModelCanSpeakDialogue("seedance-2-fast")).toBe(true)
|
|
82
101
|
expect(videoModelCanSpeakDialogue("seedance-2-mini")).toBe(true)
|
|
102
|
+
// Kling 2.6 / 3.0 / Omni speak scripted dialogue natively (probe-verified
|
|
103
|
+
// 2026-07-16) — the Story→Video auto-pick uses in-model speech + revoice.
|
|
104
|
+
expect(videoModelCanSpeakDialogue("kling")).toBe(true)
|
|
105
|
+
expect(videoModelCanSpeakDialogue("kling-3.0")).toBe(true)
|
|
106
|
+
expect(videoModelCanSpeakDialogue("kling-3-omni")).toBe(true)
|
|
83
107
|
// ambient-only models are NOT dialogue-capable — their audio is SFX/ambient
|
|
84
|
-
expect(videoModelCanSpeakDialogue("kling")).toBe(false)
|
|
85
|
-
expect(videoModelCanSpeakDialogue("kling-3.0")).toBe(false)
|
|
86
108
|
expect(videoModelCanSpeakDialogue("seedance")).toBe(false)
|
|
87
109
|
expect(videoModelCanSpeakDialogue("minimax")).toBe(false)
|
|
88
110
|
expect(videoModelCanSpeakDialogue(undefined)).toBe(false)
|
|
@@ -101,10 +123,16 @@ describe("seedance-2-mini is a full Seedance 2 family member", () => {
|
|
|
101
123
|
})
|
|
102
124
|
|
|
103
125
|
describe("VIDEO_AUDIO_CAPABILITY internal consistency", () => {
|
|
104
|
-
it("every
|
|
126
|
+
it("every AUDIO_ADDON provider is cost-affecting on the canonical `sound` lever", () => {
|
|
127
|
+
// The billing-critical invariant: the `:audio` surcharge keys off `sound`,
|
|
128
|
+
// so every surcharged model must declare audio, be marked cost-affecting,
|
|
129
|
+
// and carry its toggle on that exact field (applyVideoAudioToggle then
|
|
130
|
+
// refuses the generateAudio alias for these, making a billed/generated
|
|
131
|
+
// divergence structurally impossible).
|
|
105
132
|
for (const m of AUDIO_ADDON_PROVIDERS) {
|
|
106
133
|
const cap = getVideoAudioCapability(m)
|
|
107
|
-
expect(cap.mode, m).toBe("
|
|
134
|
+
expect(cap.mode, m).not.toBe("none")
|
|
135
|
+
expect(cap.field, m).toBe("sound")
|
|
108
136
|
expect(cap.affectsCost, m).toBe(true)
|
|
109
137
|
}
|
|
110
138
|
})
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
MOTION_DURATION_TIERS,
|
|
19
19
|
T2I_TO_I2I_VARIANT,
|
|
20
20
|
isVeoProvider,
|
|
21
|
+
getVideoAudioCapability,
|
|
21
22
|
} from "./model-constants.js"
|
|
22
23
|
import { isFlux2Model } from "./flux2-pricing.js"
|
|
23
24
|
import { MODEL_CATALOG } from "./model-catalog.js"
|
|
@@ -227,8 +228,13 @@ export function buildVideoCreditModelIdentifier(
|
|
|
227
228
|
const tier = tiers.find(t => durationSec <= t.maxSeconds) ?? tiers[tiers.length - 1]
|
|
228
229
|
let identifier = `${effectiveProvider}:${tier.suffix}`
|
|
229
230
|
|
|
230
|
-
// Append audio suffix if applicable
|
|
231
|
-
|
|
231
|
+
// Append audio suffix if applicable. When the caller expressed no intent
|
|
232
|
+
// (sound === undefined), fall back to the model's own default (capability
|
|
233
|
+
// `defaultOn` — kling-3.0 generates audio unless explicitly disabled), so an
|
|
234
|
+
// intent-less request is billed for the audio it actually produces instead
|
|
235
|
+
// of reserving the cheaper no-audio tier against an audio-on generation.
|
|
236
|
+
const soundOn = sound ?? (getVideoAudioCapability(effectiveProvider).defaultOn === true)
|
|
237
|
+
if (AUDIO_ADDON_PROVIDERS.has(effectiveProvider) && soundOn) {
|
|
232
238
|
identifier += ":audio"
|
|
233
239
|
}
|
|
234
240
|
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER,
|
|
20
20
|
getMaxImagePromptChars,
|
|
21
21
|
PROMPT_HARD_CEILING,
|
|
22
|
+
LLM_TEXT_INPUT_MAX,
|
|
22
23
|
MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER,
|
|
23
24
|
getMaxVideoPromptChars,
|
|
24
25
|
NEGATIVE_PROMPT_MAX,
|
|
@@ -95,6 +96,8 @@ export {
|
|
|
95
96
|
SEEDANCE_2_PROVIDERS,
|
|
96
97
|
SEEDANCE_2_REF_LIMITS,
|
|
97
98
|
SEEDANCE_2_EXTEND_STITCH,
|
|
99
|
+
SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC,
|
|
100
|
+
SEEDANCE_2_CONTINUATION_REF_SEC,
|
|
98
101
|
NATIVE_ADAPTIVE_ASPECT,
|
|
99
102
|
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
100
103
|
VIDEO_PROVIDERS_REQUIRING_IMAGE,
|
|
@@ -250,6 +253,14 @@ export {
|
|
|
250
253
|
LLM_MODEL_IDS,
|
|
251
254
|
STRUCTURED_VISION_MODELS,
|
|
252
255
|
VIDEO_ANALYSIS_LLM_MODELS,
|
|
256
|
+
VIDEO_ANALYSIS_TIERS,
|
|
257
|
+
type VideoAnalysisTier,
|
|
258
|
+
VIDEO_ANALYSIS_TIER_ORDER,
|
|
259
|
+
DEFAULT_VIDEO_ANALYSIS_TIER,
|
|
260
|
+
DEFAULT_VIDEO_ANALYSIS_MODEL,
|
|
261
|
+
VIDEO_ANALYSIS_TIER_LABELS,
|
|
262
|
+
isVideoAnalysisTier,
|
|
263
|
+
resolveVideoAnalysisModel,
|
|
253
264
|
LLM_FEATURE_DEFAULTS,
|
|
254
265
|
LLM_MODALITY_CAPS,
|
|
255
266
|
LLM_REASONING_EFFORTS,
|
package/src/llm-models.ts
CHANGED
|
@@ -37,14 +37,18 @@ export interface LlmModelDef {
|
|
|
37
37
|
maxOutputTokens: number
|
|
38
38
|
/**
|
|
39
39
|
* How this model can be forced into schema-valid structured output.
|
|
40
|
-
* - "anthropic-tool"
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* -
|
|
44
|
-
*
|
|
40
|
+
* - "anthropic-tool" → forced tool_choice on the Claude wire (direct SDK
|
|
41
|
+
* guaranteed; KIE's proxy re-serializes the tool call
|
|
42
|
+
* as a `<tool_calls>` text tag — decoded in llm-client).
|
|
43
|
+
* - "kie-response-format" → KIE chat-completions `response_format: json_schema`
|
|
44
|
+
* (verified enforced for Gemini via KIE).
|
|
45
|
+
* - "responses-json-schema" → KIE codex/v1/responses `text.format: json_schema`
|
|
46
|
+
* (live-verified 2026-07-14 for gpt-5.4/5.5 and the
|
|
47
|
+
* whole GPT-5.6 family, text AND vision inputs).
|
|
48
|
+
* - undefined → no native mode; callers fall back to parse + retry.
|
|
45
49
|
* Capability-driven so `llmCompleteStructured` never hardcodes provider ids.
|
|
46
50
|
*/
|
|
47
|
-
structuredOutputMode?: "anthropic-tool" | "kie-response-format"
|
|
51
|
+
structuredOutputMode?: "anthropic-tool" | "kie-response-format" | "responses-json-schema"
|
|
48
52
|
/** If set, fallback to direct Anthropic SDK with this model ID when KIE.ai fails */
|
|
49
53
|
directFallbackModel?: string
|
|
50
54
|
/** Effort levels this model accepts (ascending). Absent/empty = no effort lever, picker hidden. */
|
|
@@ -142,6 +146,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
142
146
|
kieFormat: "responses",
|
|
143
147
|
kieSlugOrModel: "gpt-5-4",
|
|
144
148
|
vendor: "openai",
|
|
149
|
+
structuredOutputMode: "responses-json-schema",
|
|
145
150
|
supportsImages: true,
|
|
146
151
|
maxOutputTokens: 16384,
|
|
147
152
|
reasoningEfforts: ["low", "medium", "high"],
|
|
@@ -154,6 +159,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
154
159
|
kieFormat: "responses",
|
|
155
160
|
kieSlugOrModel: "gpt-5-5",
|
|
156
161
|
vendor: "openai",
|
|
162
|
+
structuredOutputMode: "responses-json-schema",
|
|
157
163
|
supportsImages: true,
|
|
158
164
|
maxOutputTokens: 16384,
|
|
159
165
|
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
@@ -167,6 +173,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
167
173
|
kieFormat: "responses",
|
|
168
174
|
kieSlugOrModel: "gpt-5-6-luna",
|
|
169
175
|
vendor: "openai",
|
|
176
|
+
structuredOutputMode: "responses-json-schema",
|
|
170
177
|
supportsImages: true,
|
|
171
178
|
maxOutputTokens: 16384,
|
|
172
179
|
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
@@ -180,6 +187,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
180
187
|
kieFormat: "responses",
|
|
181
188
|
kieSlugOrModel: "gpt-5-6-terra",
|
|
182
189
|
vendor: "openai",
|
|
190
|
+
structuredOutputMode: "responses-json-schema",
|
|
183
191
|
supportsImages: true,
|
|
184
192
|
maxOutputTokens: 16384,
|
|
185
193
|
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
@@ -193,6 +201,7 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
193
201
|
kieFormat: "responses",
|
|
194
202
|
kieSlugOrModel: "gpt-5-6-sol",
|
|
195
203
|
vendor: "openai",
|
|
204
|
+
structuredOutputMode: "responses-json-schema",
|
|
196
205
|
supportsImages: true,
|
|
197
206
|
maxOutputTokens: 16384,
|
|
198
207
|
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
@@ -238,9 +247,10 @@ export const LLM_MODEL_IDS = LLM_MODELS.map((m) => m.id)
|
|
|
238
247
|
/** Vision models that can return GUARANTEED structured output — the
|
|
239
248
|
* describe-to-picker analyzer forces a schema over an image, so its model
|
|
240
249
|
* pickers AND the backend route gate offer exactly these: Anthropic (forced
|
|
241
|
-
* tool)
|
|
242
|
-
*
|
|
243
|
-
* picker, the config panel, and the route gate
|
|
250
|
+
* tool), Gemini (KIE `response_format`), and GPT responses-format models
|
|
251
|
+
* (KIE `text.format` json_schema — vision+schema live-verified 2026-07-14).
|
|
252
|
+
* Single source of truth so the picker, the config panel, and the route gate
|
|
253
|
+
* can't drift. */
|
|
244
254
|
export const STRUCTURED_VISION_MODELS = LLM_MODELS.filter(
|
|
245
255
|
(m) => m.supportsImages && m.structuredOutputMode != null,
|
|
246
256
|
)
|
|
@@ -376,3 +386,36 @@ export function resolveLlmCreditId(feature: string, body: unknown): string {
|
|
|
376
386
|
export const VIDEO_ANALYSIS_LLM_MODELS: string[] = LLM_MODELS
|
|
377
387
|
.filter((m) => getLlmModalityCaps(m.id).video && getLlmModalityCaps(m.id).audio)
|
|
378
388
|
.map((m) => m.id)
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Video-analysis quality TIERS — the ONLY analyzer identifiers exposed to users
|
|
392
|
+
* (API, UI, docs). Each maps to an internal analysis model; the underlying
|
|
393
|
+
* vendor/model name is never surfaced. Default is `pro`. The guard test
|
|
394
|
+
* (video-analysis-pricing.test.ts) asserts every tier target is a real
|
|
395
|
+
* VIDEO_ANALYSIS_LLM_MODELS member AND every such model is reachable by a tier,
|
|
396
|
+
* so adding a video model forces a tier decision instead of silently leaking.
|
|
397
|
+
*/
|
|
398
|
+
export const VIDEO_ANALYSIS_TIERS = { fast: "gemini-3-flash", pro: "gemini-3.1-pro" } as const
|
|
399
|
+
export type VideoAnalysisTier = keyof typeof VIDEO_ANALYSIS_TIERS
|
|
400
|
+
/** UI/listing order — recommended (pro) first. */
|
|
401
|
+
export const VIDEO_ANALYSIS_TIER_ORDER = ["pro", "fast"] as const
|
|
402
|
+
export const DEFAULT_VIDEO_ANALYSIS_TIER: VideoAnalysisTier = "pro"
|
|
403
|
+
export const DEFAULT_VIDEO_ANALYSIS_MODEL: string = VIDEO_ANALYSIS_TIERS[DEFAULT_VIDEO_ANALYSIS_TIER]
|
|
404
|
+
/** Neutral, vendor-free display labels for the UI. */
|
|
405
|
+
export const VIDEO_ANALYSIS_TIER_LABELS: Record<VideoAnalysisTier, string> = { fast: "Fast", pro: "Pro" }
|
|
406
|
+
|
|
407
|
+
export function isVideoAnalysisTier(v: string): v is VideoAnalysisTier {
|
|
408
|
+
return Object.prototype.hasOwnProperty.call(VIDEO_ANALYSIS_TIERS, v)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Resolve a user-supplied tier (`"fast"`/`"pro"`) OR a raw internal model id to
|
|
413
|
+
* the internal analysis model id. Empty/unknown → the default tier's model.
|
|
414
|
+
* Real model ids pass through (back-compat for existing stored `llmModel`
|
|
415
|
+
* values); anything else falls back to the default rather than erroring.
|
|
416
|
+
*/
|
|
417
|
+
export function resolveVideoAnalysisModel(input?: string | null): string {
|
|
418
|
+
if (input && isVideoAnalysisTier(input)) return VIDEO_ANALYSIS_TIERS[input]
|
|
419
|
+
if (input && VIDEO_ANALYSIS_LLM_MODELS.includes(input)) return input
|
|
420
|
+
return DEFAULT_VIDEO_ANALYSIS_MODEL
|
|
421
|
+
}
|
package/src/model-catalog.ts
CHANGED
|
@@ -146,7 +146,7 @@ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
|
|
|
146
146
|
{ intent: "best for typography / logos / text-heavy", modelIds: ["nano-banana-pro", "gpt-image-2"], note: "Nano Banana Pro for diagrams / complex text; GPT Image 2 for logos and short copy." },
|
|
147
147
|
{ intent: "cheapest realistic image", modelIds: ["z-image", "qwen", "imagen4-fast"], note: "Z-Image is the cheapest at 1 credit. Qwen / Imagen4 Fast for slightly higher quality." },
|
|
148
148
|
{ intent: "highest fidelity image", modelIds: ["nano-banana-pro", "imagen4-ultra", "flux-flex"], note: "Pick by family preference; all three are premium tiers." },
|
|
149
|
-
{ intent: "image edit / restyle", modelIds: ["flux-kontext", "ideogram-remix", "seedream-5-
|
|
149
|
+
{ intent: "image edit / restyle", modelIds: ["flux-kontext", "ideogram-remix", "seedream-5-pro-i2i"], note: "Flux Kontext preserves identity; Ideogram Remix is character-aware; Seedream 5 Pro for instruction-based edits (5 Lite is the budget option)." },
|
|
150
150
|
{ intent: "highest-resolution image (4K / 8K)", modelIds: ["topaz-image-upscale", "nano-banana-pro", "gpt-image-2"], note: "Generate at native then Topaz upscale for 8K." },
|
|
151
151
|
{ intent: "background removal / cutout", modelIds: ["recraft-remove-bg"], note: "1 credit, no prompt needed." },
|
|
152
152
|
// video
|
|
@@ -165,6 +165,11 @@ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
|
|
|
165
165
|
// can consume them. Keep in sync when adding a new ratio set.)
|
|
166
166
|
// =============================================================================
|
|
167
167
|
const NANO_BANANA_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"] as const
|
|
168
|
+
// Nano Banana 2 Lite only — the model's full documented enum incl. `auto`
|
|
169
|
+
// (native default) and extreme banner ratios (docs.kie.ai/market/google/
|
|
170
|
+
// nano-banana-2-lite). Kept separate so the wider set can't leak to the
|
|
171
|
+
// rest of the family.
|
|
172
|
+
const NANO_BANANA_2_LITE_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "4:1", "1:4", "8:1", "1:8"] as const
|
|
168
173
|
// Note: these arrays are synchronized with the frontend's
|
|
169
174
|
// `model-options.ts` constants of the same names. The frontend now imports
|
|
170
175
|
// them via the `getAspectRatioOptions(modelId)` helper; if you update one
|
|
@@ -233,6 +238,22 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
233
238
|
{ identifier: "nano-banana-2:4K", credits: 5, note: "4K" },
|
|
234
239
|
],
|
|
235
240
|
},
|
|
241
|
+
"nano-banana-2-lite": {
|
|
242
|
+
id: "nano-banana-2-lite",
|
|
243
|
+
kind: "image",
|
|
244
|
+
// Same single-id shape as nano-banana-2: optional input images on the one
|
|
245
|
+
// endpoint (via image_urls, up to 10), so reachable for both t2i and i2i.
|
|
246
|
+
modes: ["t2i", "i2i"] as const,
|
|
247
|
+
family: "Google",
|
|
248
|
+
label: "Nano Banana 2 Lite",
|
|
249
|
+
series: "Nano Banana",
|
|
250
|
+
description: "Lightweight Nano Banana 2 (Gemini 3.1 Flash-Lite) — fast, low-cost 1K generation and editing.",
|
|
251
|
+
useCases: ["realistic", "drafts", "iteration"],
|
|
252
|
+
features: ["reference-image"],
|
|
253
|
+
aspectRatios: NANO_BANANA_2_LITE_RATIOS,
|
|
254
|
+
// 1K only — no resolution lever, flat pricing.
|
|
255
|
+
pricing: [{ identifier: "nano-banana-2-lite", credits: 2, note: "1K flat" }],
|
|
256
|
+
},
|
|
236
257
|
"nano-banana-pro": {
|
|
237
258
|
id: "nano-banana-pro",
|
|
238
259
|
kind: "image",
|
|
@@ -691,6 +712,40 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
691
712
|
{ identifier: "seedream-5-lite-i2i:high", credits: 5, note: "high quality" },
|
|
692
713
|
],
|
|
693
714
|
},
|
|
715
|
+
"seedream-5-pro": {
|
|
716
|
+
id: "seedream-5-pro",
|
|
717
|
+
kind: "image",
|
|
718
|
+
modes: ["t2i"] as const,
|
|
719
|
+
family: "Bytedance",
|
|
720
|
+
label: "Seedream 5 Pro",
|
|
721
|
+
series: "Seedream",
|
|
722
|
+
description: "Flagship Seedream 5 Pro — strongest instruction following and visual reasoning. Basic = 1K, high = 2K.",
|
|
723
|
+
useCases: ["realistic", "instruction"],
|
|
724
|
+
features: ["reference-image"],
|
|
725
|
+
aspectRatios: SEEDREAM_RATIOS,
|
|
726
|
+
qualities: ["basic", "high"],
|
|
727
|
+
pricing: [
|
|
728
|
+
{ identifier: "seedream-5-pro", credits: 3, note: "basic / 1K default" },
|
|
729
|
+
{ identifier: "seedream-5-pro:high", credits: 6, note: "high / 2K" },
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
"seedream-5-pro-i2i": {
|
|
733
|
+
id: "seedream-5-pro-i2i",
|
|
734
|
+
kind: "image",
|
|
735
|
+
modes: ["i2i"] as const,
|
|
736
|
+
family: "Bytedance",
|
|
737
|
+
label: "Seedream 5 Pro (I2I)",
|
|
738
|
+
series: "Seedream",
|
|
739
|
+
description: "Image-to-image with Seedream 5 Pro — multi-reference instruction edits. Basic = 1K, high = 2K.",
|
|
740
|
+
useCases: ["edit", "instruction"],
|
|
741
|
+
features: ["reference-image"],
|
|
742
|
+
aspectRatios: SEEDREAM_RATIOS,
|
|
743
|
+
qualities: ["basic", "high"],
|
|
744
|
+
pricing: [
|
|
745
|
+
{ identifier: "seedream-5-pro-i2i", credits: 3, note: "basic / 1K default" },
|
|
746
|
+
{ identifier: "seedream-5-pro-i2i:high", credits: 6, note: "high / 2K" },
|
|
747
|
+
],
|
|
748
|
+
},
|
|
694
749
|
|
|
695
750
|
// ── Alibaba Qwen ──
|
|
696
751
|
"qwen": {
|
|
@@ -1754,10 +1809,10 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
1754
1809
|
id: "gemini-3-flash-video-analysis",
|
|
1755
1810
|
kind: "video",
|
|
1756
1811
|
modes: ["video-analysis"] as const,
|
|
1757
|
-
family: "
|
|
1758
|
-
label: "Video Analysis (
|
|
1759
|
-
series: "
|
|
1760
|
-
description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast
|
|
1812
|
+
family: "Nodaro",
|
|
1813
|
+
label: "Video Analysis (Fast)",
|
|
1814
|
+
series: "Video Analysis",
|
|
1815
|
+
description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast, economy tier. Billed per duration bucket.",
|
|
1761
1816
|
useCases: ["video-analysis", "shot-list", "fast"],
|
|
1762
1817
|
pricing: [
|
|
1763
1818
|
{ identifier: "video-analysis:gemini-3-flash", credits: 3, note: "10-min ceiling (no duration given)" },
|
|
@@ -1771,10 +1826,10 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
1771
1826
|
id: "gemini-3.1-pro-video-analysis",
|
|
1772
1827
|
kind: "video",
|
|
1773
1828
|
modes: ["video-analysis"] as const,
|
|
1774
|
-
family: "
|
|
1775
|
-
label: "Video Analysis (
|
|
1776
|
-
series: "
|
|
1777
|
-
description: "Analyze a video into a structured shot list (scenes, camera, audio) —
|
|
1829
|
+
family: "Nodaro",
|
|
1830
|
+
label: "Video Analysis (Pro)",
|
|
1831
|
+
series: "Video Analysis",
|
|
1832
|
+
description: "Analyze a video into a structured shot list (scenes, camera, audio) — higher-fidelity, default tier. Billed per duration bucket.",
|
|
1778
1833
|
useCases: ["video-analysis", "shot-list", "cinematic"],
|
|
1779
1834
|
pricing: [
|
|
1780
1835
|
{ identifier: "video-analysis:gemini-3.1-pro", credits: 11, note: "10-min ceiling (no duration given)" },
|