@nodaro/shared 2.1.0 → 2.2.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 +455 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +307 -29
- package/dist/index.d.ts +307 -29
- package/dist/index.js +431 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/gvp-anchor-mode.test.ts +65 -0
- package/src/__tests__/gvp-supported-providers.test.ts +234 -15
- package/src/__tests__/minimax-h3-catalog.test.ts +116 -0
- package/src/__tests__/minimax-h3-credit-id.test.ts +62 -0
- package/src/__tests__/reference-roles.test.ts +22 -1
- package/src/__tests__/seedance-2-5-catalog.test.ts +128 -0
- package/src/__tests__/video-analysis-catalog-sync.test.ts +69 -3
- package/src/__tests__/video-analysis-pricing.test.ts +105 -0
- package/src/__tests__/video-analysis.test.ts +13 -0
- package/src/credit-identifiers.ts +27 -3
- package/src/film-pricing.ts +1 -0
- package/src/index.ts +25 -0
- package/src/llm-models.ts +8 -5
- package/src/model-catalog.ts +170 -38
- package/src/model-constants.ts +453 -16
- package/src/node-default-mappings.ts +2 -0
- package/src/node-mappable-fields.ts +1 -1
- package/src/pipeline-types.ts +1 -0
- package/src/provider-directive-defaults.ts +4 -0
- package/src/reference-roles.ts +4 -1
- package/src/types.ts +11 -1
- package/src/video-analysis-pricing.ts +145 -25
- package/src/video-analysis.ts +17 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { MODEL_CATALOG } from "../model-catalog.js"
|
|
3
|
+
import {
|
|
4
|
+
FRAME_MODE_ADAPTIVE_ONLY_ASPECT,
|
|
5
|
+
MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER,
|
|
6
|
+
NATIVE_ADAPTIVE_ASPECT,
|
|
7
|
+
PRICING_DEFAULT_RESOLUTION,
|
|
8
|
+
PROMPT_HARD_CEILING,
|
|
9
|
+
SEEDANCE_2_5_REF_LIMITS,
|
|
10
|
+
SEEDANCE_2_PROVIDERS,
|
|
11
|
+
VIDEO_DURATION_TIERS,
|
|
12
|
+
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
13
|
+
getMaxVideoPromptChars,
|
|
14
|
+
isSeedance2Provider,
|
|
15
|
+
seedance2AudioLimitSec,
|
|
16
|
+
} from "../model-constants.js"
|
|
17
|
+
import { buildVideoCreditModelIdentifier } from "../credit-identifiers.js"
|
|
18
|
+
|
|
19
|
+
const ID = "seedance-2-5"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Seedance 2.5's shape was established by a live capability probe against
|
|
23
|
+
* api.kie.ai on 2026-08-08, NOT by the published schema — KIE's docs advertise
|
|
24
|
+
* a wider surface than the proxy actually accepts. These tests pin the probed
|
|
25
|
+
* reality so a future "the spec sheet says 4K/180s" edit has to re-probe first.
|
|
26
|
+
*/
|
|
27
|
+
describe("seedance-2-5 catalog (probe-verified 2026-08-08)", () => {
|
|
28
|
+
it("is 480p/720p ONLY — KIE rejects 1080p/4k/2k the same way it rejects a nonsense value", () => {
|
|
29
|
+
expect(MODEL_CATALOG[ID].resolutions).toEqual(["480p", "720p"])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("runs 4-30s contiguously — 30 is the probed ceiling (31 was rejected)", () => {
|
|
33
|
+
const durations = MODEL_CATALOG[ID].durations
|
|
34
|
+
expect(durations?.[0]).toBe(4)
|
|
35
|
+
expect(durations?.[durations.length - 1]).toBe(30)
|
|
36
|
+
expect(durations).toHaveLength(27)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("carries the wider 30/10/10 reference caps, NOT the 2.0 family's 9/3/3", () => {
|
|
40
|
+
expect(VIDEO_REF_LIMITS_BY_PROVIDER[ID]).toEqual({ images: 30, videos: 10, audio: 10 })
|
|
41
|
+
expect(SEEDANCE_2_5_REF_LIMITS.images).toBeGreaterThan(
|
|
42
|
+
VIDEO_REF_LIMITS_BY_PROVIDER["seedance-2"]!.images!,
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("joins the Seedance 2 capability family (adaptive aspect, audio, refs)", () => {
|
|
47
|
+
expect(isSeedance2Provider(ID)).toBe(true)
|
|
48
|
+
expect(SEEDANCE_2_PROVIDERS.has(ID)).toBe(true)
|
|
49
|
+
expect(NATIVE_ADAPTIVE_ASPECT[ID]).toBe("adaptive")
|
|
50
|
+
expect(MODEL_CATALOG[ID].aspectRatios).toContain("adaptive")
|
|
51
|
+
expect(MODEL_CATALOG[ID].aspectRatios).toContain("21:9")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("forces adaptive aspect in frame mode — the 2.0 SKUs deliberately do NOT", () => {
|
|
55
|
+
expect(FRAME_MODE_ADAPTIVE_ONLY_ASPECT.has(ID)).toBe(true)
|
|
56
|
+
for (const p of ["seedance-2", "seedance-2-fast", "seedance-2-mini"]) {
|
|
57
|
+
expect(FRAME_MODE_ADAPTIVE_ONLY_ASPECT.has(p)).toBe(false)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("accepts a 30000-char prompt, and the route ceiling is wide enough to pass it through", () => {
|
|
62
|
+
expect(getMaxVideoPromptChars(ID)).toBe(30000)
|
|
63
|
+
// The generous route ceiling must never sit BELOW a real per-model cap, or
|
|
64
|
+
// Zod hard-rejects a prompt the model would have accepted.
|
|
65
|
+
const caps = Object.values(MAX_VIDEO_PROMPT_CHARS_BY_PROVIDER)
|
|
66
|
+
expect(PROMPT_HARD_CEILING).toBeGreaterThanOrEqual(Math.max(...caps))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("enforces the documented 30s reference-audio cap", () => {
|
|
70
|
+
expect(seedance2AudioLimitSec(ID)).toBe(30)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe("seedance-2-5 pricing identifiers", () => {
|
|
75
|
+
const build = (duration?: number, resolution?: string, hasVideoRef?: boolean) =>
|
|
76
|
+
buildVideoCreditModelIdentifier(ID, duration, undefined, undefined, undefined, resolution, hasVideoRef)
|
|
77
|
+
|
|
78
|
+
it("gives every allowed second its OWN tier — no round-up to a coarser rung", () => {
|
|
79
|
+
// The 2.0 family's 4/8/12/15 ladder would price a 23s render at the 15s
|
|
80
|
+
// rung; commit_credits only refunds a surplus, so that gap is permanent.
|
|
81
|
+
expect(VIDEO_DURATION_TIERS[ID]).toHaveLength(27)
|
|
82
|
+
expect(build(23, "720p")).toBe("seedance-2-5:23s:720p")
|
|
83
|
+
expect(build(17, "480p")).toBe("seedance-2-5:17s:480p")
|
|
84
|
+
expect(build(30, "720p")).toBe("seedance-2-5:30s:720p")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("prices an OMITTED resolution at the model's real KIE default (720p), not the cheapest tier", () => {
|
|
88
|
+
// KIE renders 720p when resolution is absent. Falling back to 480p here
|
|
89
|
+
// would reserve the cheap tier against an expensive render.
|
|
90
|
+
expect(PRICING_DEFAULT_RESOLUTION[ID]).toBe("720p")
|
|
91
|
+
expect(build(8)).toBe("seedance-2-5:8s:720p")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("leaves the 2.0 family's omitted-resolution behaviour untouched", () => {
|
|
95
|
+
// Adding PRICING_DEFAULT_RESOLUTION must not silently reprice live models.
|
|
96
|
+
for (const p of ["seedance-2", "seedance-2-fast", "seedance-2-mini"]) {
|
|
97
|
+
expect(PRICING_DEFAULT_RESOLUTION[p]).toBeUndefined()
|
|
98
|
+
expect(
|
|
99
|
+
buildVideoCreditModelIdentifier(p, 8, undefined, undefined, undefined, undefined, false),
|
|
100
|
+
).toBe(`${p}:8s:480p`)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("clamps an unsupported resolution to the top real tier so the id is always seeded", () => {
|
|
105
|
+
expect(build(8, "1080p")).toBe("seedance-2-5:8s:720p")
|
|
106
|
+
expect(build(8, "4k")).toBe("seedance-2-5:8s:720p")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("selects the cheaper -ref ladder when a reference video is wired", () => {
|
|
110
|
+
expect(build(8, "720p", true)).toBe("seedance-2-5:8s:720p-ref")
|
|
111
|
+
expect(build(8, "480p", true)).toBe("seedance-2-5:8s:480p-ref")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("resolves every catalog duration x resolution x ref-mode to a distinct tier", () => {
|
|
115
|
+
const seen = new Set<string>()
|
|
116
|
+
for (const d of MODEL_CATALOG[ID].durations!) {
|
|
117
|
+
for (const res of ["480p", "720p"]) {
|
|
118
|
+
for (const ref of [false, true]) {
|
|
119
|
+
const identifier = build(d, res, ref)
|
|
120
|
+
expect(identifier).toBe(`${ID}:${d}s:${res}${ref ? "-ref" : ""}`)
|
|
121
|
+
seen.add(identifier)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 27 durations x 2 resolutions x 2 ref-modes, none collapsing onto another.
|
|
126
|
+
expect(seen.size).toBe(108)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
VIDEO_ANALYSIS_DURATION_BUCKETS,
|
|
15
15
|
VIDEO_ANALYSIS_MAX_DURATION_SEC,
|
|
16
16
|
buildVideoAnalysisCreditId,
|
|
17
|
+
VIDEO_AUDIT_BUCKET_CREDITS,
|
|
17
18
|
} from "../video-analysis-pricing.js"
|
|
18
19
|
import { DEFAULT_VIDEO_ANALYSIS_MODEL } from "../llm-models.js"
|
|
19
20
|
|
|
@@ -22,6 +23,18 @@ const catalogRows = Object.values(MODEL_CATALOG)
|
|
|
22
23
|
.flatMap((m) => (m.pricing ?? []) as ReadonlyArray<{ identifier: string; credits: number }>)
|
|
23
24
|
.filter((r) => r.identifier.startsWith("video-analysis:"))
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Every `video-audit*` pricing row declared anywhere in the catalog. NOTE:
|
|
28
|
+
* no trailing colon in the filter (unlike the `video-analysis:` filter
|
|
29
|
+
* above) — `video-audit`'s bare base-family id has no colon at all (there's
|
|
30
|
+
* no per-model segment to be bare "within", unlike video-analysis's bare
|
|
31
|
+
* per-model ids which still carry `video-analysis:<model>`), so a
|
|
32
|
+
* colon-anchored filter would silently miss it.
|
|
33
|
+
*/
|
|
34
|
+
const auditCatalogRows = Object.values(MODEL_CATALOG)
|
|
35
|
+
.flatMap((m) => (m.pricing ?? []) as ReadonlyArray<{ identifier: string; credits: number }>)
|
|
36
|
+
.filter((r) => r.identifier.startsWith("video-audit"))
|
|
37
|
+
|
|
25
38
|
describe("model catalog video-analysis pricing", () => {
|
|
26
39
|
it("declares at least one row (guard is actually wired to something)", () => {
|
|
27
40
|
expect(catalogRows.length).toBeGreaterThan(0)
|
|
@@ -64,6 +77,56 @@ describe("model catalog video-analysis pricing", () => {
|
|
|
64
77
|
})
|
|
65
78
|
})
|
|
66
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Same guard as above, for `video-audit` ("AI Audit") — a sibling node whose
|
|
82
|
+
* catalog rows hand-copy `VIDEO_AUDIT_BUCKET_CREDITS`. Mirrors the
|
|
83
|
+
* video-analysis describe block's structure exactly, adapted for
|
|
84
|
+
* video-audit's simpler id shape (two FAMILIES — `video-audit` /
|
|
85
|
+
* `video-audit:auto` — rather than an open set of per-model segments).
|
|
86
|
+
*/
|
|
87
|
+
describe("model catalog video-audit pricing", () => {
|
|
88
|
+
it("declares at least one row (guard is actually wired to something)", () => {
|
|
89
|
+
expect(auditCatalogRows.length).toBeGreaterThan(0)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("every bucketed catalog row matches VIDEO_AUDIT_BUCKET_CREDITS exactly", () => {
|
|
93
|
+
const bucketed = auditCatalogRows.filter((r) => /:\d+s$/.test(r.identifier))
|
|
94
|
+
expect(bucketed.length).toBeGreaterThan(0)
|
|
95
|
+
for (const row of bucketed) {
|
|
96
|
+
expect(
|
|
97
|
+
row.credits,
|
|
98
|
+
`catalog "${row.identifier}" = ${row.credits} but the table says ${VIDEO_AUDIT_BUCKET_CREDITS[row.identifier]}`,
|
|
99
|
+
).toBe(VIDEO_AUDIT_BUCKET_CREDITS[row.identifier])
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("every bare (no-duration) catalog row equals its 600s ceiling", () => {
|
|
104
|
+
// A bare id means "duration unknown", which prices at the ceiling.
|
|
105
|
+
const bare = auditCatalogRows.filter((r) => !/:\d+s$/.test(r.identifier))
|
|
106
|
+
expect(bare.length).toBe(2) // exactly `video-audit` + `video-audit:auto`
|
|
107
|
+
for (const row of bare) {
|
|
108
|
+
const ceiling = VIDEO_AUDIT_BUCKET_CREDITS[`${row.identifier}:${VIDEO_ANALYSIS_MAX_DURATION_SEC}s`]
|
|
109
|
+
expect(ceiling, `no ceiling row for ${row.identifier}`).toBeDefined()
|
|
110
|
+
expect(row.credits, `catalog "${row.identifier}" must equal its ${VIDEO_ANALYSIS_MAX_DURATION_SEC}s ceiling`).toBe(ceiling)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("catalog covers every bucket for both families", () => {
|
|
115
|
+
const byFamily = new Map<string, Set<number>>()
|
|
116
|
+
for (const r of auditCatalogRows) {
|
|
117
|
+
const m = /^(video-audit(?::auto)?):(\d+)s$/.exec(r.identifier)
|
|
118
|
+
if (!m) continue
|
|
119
|
+
if (!byFamily.has(m[1]!)) byFamily.set(m[1]!, new Set())
|
|
120
|
+
byFamily.get(m[1]!)!.add(Number(m[2]))
|
|
121
|
+
}
|
|
122
|
+
expect([...byFamily.keys()].sort()).toEqual(["video-audit", "video-audit:auto"])
|
|
123
|
+
for (const [family, buckets] of byFamily) {
|
|
124
|
+
expect([...buckets].sort((a, b) => a - b), `${family} bucket coverage`)
|
|
125
|
+
.toEqual([...VIDEO_ANALYSIS_DURATION_BUCKETS])
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
67
130
|
/**
|
|
68
131
|
* The BARE `video-analysis` node-type id — the one every colon-scoped guard misses.
|
|
69
132
|
*
|
|
@@ -90,9 +153,12 @@ describe("bare video-analysis node-type credit id", () => {
|
|
|
90
153
|
// 279 wrote 739, 283 wrote 346, 284 wrote 350 — `smart` owns the ceiling and
|
|
91
154
|
// gained the continuity pass — 288 wrote 3500 as a x10 of that, 293
|
|
92
155
|
// corrected it to 3496 by RE-DERIVING `smart:600s` from the plugin formula,
|
|
93
|
-
//
|
|
94
|
-
// 24 and the schedule regenerated ~47% lower at the ceiling bucket
|
|
95
|
-
|
|
156
|
+
// 294 wrote 1868: the smart re-base measured 6 fps equal-or-better than
|
|
157
|
+
// 24 and the schedule regenerated ~47% lower at the ceiling bucket, and 300
|
|
158
|
+
// wrote 2064: the V1 hybrid-smart reprice (task A3) moved `smart` to a
|
|
159
|
+
// multi-roll plan (native skeleton + donor rolls, always refined) and
|
|
160
|
+
// trued up every tier's judge/refine terms from staging measurement).
|
|
161
|
+
expect(ceiling).toBe(2064)
|
|
96
162
|
})
|
|
97
163
|
|
|
98
164
|
it("the bare id still bounds the default tier at the ceiling bucket", () => {
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
VIDEO_ANALYSIS_BUCKET_CREDITS,
|
|
5
5
|
pickVideoAnalysisBucket, buildVideoAnalysisCreditId, bucketSecondsFromCreditId,
|
|
6
6
|
videoAnalysisNumWindows,
|
|
7
|
+
VIDEO_AUDIT_BUCKET_CREDITS,
|
|
8
|
+
buildVideoAuditCreditId, videoAuditCreditsForBucket, bucketSecondsFromAuditCreditId,
|
|
7
9
|
} from "../video-analysis-pricing.js"
|
|
8
10
|
import {
|
|
9
11
|
VIDEO_ANALYSIS_LLM_MODELS, VIDEO_ANALYSIS_TIERS, VIDEO_ANALYSIS_TIER_ORDER,
|
|
@@ -135,3 +137,106 @@ describe("video-analysis-pricing", () => {
|
|
|
135
137
|
}
|
|
136
138
|
})
|
|
137
139
|
})
|
|
140
|
+
|
|
141
|
+
// `video-audit` ("AI Audit") is a SEPARATE node from video-analysis, but
|
|
142
|
+
// shares this module's bucket ladder and generator-authoritative table
|
|
143
|
+
// pattern. Its own $-formula and generator live in the private
|
|
144
|
+
// `@nodaroai/cloud-plugins` package exactly like video-analysis's — see
|
|
145
|
+
// `VIDEO_AUDIT_BUCKET_CREDITS`'s doc comment. These values are pasted
|
|
146
|
+
// verbatim from that generator's output; this file only covers the
|
|
147
|
+
// NON-monetary bucket/id-construction logic, same split as above.
|
|
148
|
+
describe("video-audit-pricing", () => {
|
|
149
|
+
it("buildVideoAuditCreditId: family selection by analysisProvided, same bucket ladder/rounding as buildVideoAnalysisCreditId", () => {
|
|
150
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 60 })).toBe("video-audit:60s")
|
|
151
|
+
expect(buildVideoAuditCreditId({ analysisProvided: false, durationSec: 60 })).toBe("video-audit:auto:60s")
|
|
152
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 170 })).toBe("video-audit:180s")
|
|
153
|
+
expect(buildVideoAuditCreditId({ analysisProvided: false, durationSec: 170 })).toBe("video-audit:auto:180s")
|
|
154
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 360 })).toBe("video-audit:360s")
|
|
155
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 600 })).toBe("video-audit:600s")
|
|
156
|
+
|
|
157
|
+
// Tolerance/boundary edges: 60 stays in the 60s bucket (inclusive upper
|
|
158
|
+
// bound); 61/63/64 all bump straight to the next bucket (180s) — the
|
|
159
|
+
// builder has NO grace period of its own (identical cliff behavior to
|
|
160
|
+
// pickVideoAnalysisBucket / buildVideoAnalysisCreditId; any tolerance
|
|
161
|
+
// grace is a WORKER re-check concern, private to the plugin, never baked
|
|
162
|
+
// into this pure id builder).
|
|
163
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 60 })).toBe("video-audit:60s")
|
|
164
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 61 })).toBe("video-audit:180s")
|
|
165
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 63 })).toBe("video-audit:180s")
|
|
166
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 64 })).toBe("video-audit:180s")
|
|
167
|
+
expect(buildVideoAuditCreditId({ analysisProvided: false, durationSec: 61 })).toBe("video-audit:auto:180s")
|
|
168
|
+
|
|
169
|
+
// No / invalid duration → 600s ceiling composite, both families — the
|
|
170
|
+
// ONLY silent-ceiling path, matching buildVideoAnalysisCreditId.
|
|
171
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true })).toBe("video-audit:600s")
|
|
172
|
+
expect(buildVideoAuditCreditId({ analysisProvided: false })).toBe("video-audit:auto:600s")
|
|
173
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: 0 })).toBe("video-audit:600s")
|
|
174
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: -5 })).toBe("video-audit:600s")
|
|
175
|
+
expect(buildVideoAuditCreditId({ analysisProvided: true, durationSec: Number.NaN })).toBe("video-audit:600s")
|
|
176
|
+
// Beyond the ceiling clamps to 600s rather than an out-of-ladder bucket.
|
|
177
|
+
expect(buildVideoAuditCreditId({ analysisProvided: false, durationSec: 9999 })).toBe("video-audit:auto:600s")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("videoAuditCreditsForBucket: table lookup by resolved bucket + family, snapping a raw duration onto the ladder", () => {
|
|
181
|
+
expect(videoAuditCreditsForBucket(60, false)).toBe(213)
|
|
182
|
+
expect(videoAuditCreditsForBucket(60, true)).toBe(393)
|
|
183
|
+
expect(videoAuditCreditsForBucket(180, false)).toBe(289)
|
|
184
|
+
expect(videoAuditCreditsForBucket(360, false)).toBe(659)
|
|
185
|
+
expect(videoAuditCreditsForBucket(600, false)).toBe(1066)
|
|
186
|
+
expect(videoAuditCreditsForBucket(600, true)).toBe(1912)
|
|
187
|
+
// Not just exact ladder values — a raw duration snaps up to its bucket.
|
|
188
|
+
expect(videoAuditCreditsForBucket(70, false)).toBe(VIDEO_AUDIT_BUCKET_CREDITS["video-audit:180s"])
|
|
189
|
+
expect(videoAuditCreditsForBucket(9999, true)).toBe(VIDEO_AUDIT_BUCKET_CREDITS["video-audit:auto:600s"])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("bucketSecondsFromAuditCreditId: round-trips both families, null on bare ids / video-analysis ids / garbage", () => {
|
|
193
|
+
for (const bucketSec of VIDEO_ANALYSIS_DURATION_BUCKETS) {
|
|
194
|
+
expect(bucketSecondsFromAuditCreditId(`video-audit:${bucketSec}s`)).toBe(bucketSec)
|
|
195
|
+
expect(bucketSecondsFromAuditCreditId(`video-audit:auto:${bucketSec}s`)).toBe(bucketSec)
|
|
196
|
+
// Round-trips the builder's own output too.
|
|
197
|
+
expect(bucketSecondsFromAuditCreditId(buildVideoAuditCreditId({ analysisProvided: true, durationSec: bucketSec }))).toBe(bucketSec)
|
|
198
|
+
expect(bucketSecondsFromAuditCreditId(buildVideoAuditCreditId({ analysisProvided: false, durationSec: bucketSec }))).toBe(bucketSec)
|
|
199
|
+
}
|
|
200
|
+
expect(bucketSecondsFromAuditCreditId("video-audit")).toBeNull() // bare base-family id, no bucket
|
|
201
|
+
expect(bucketSecondsFromAuditCreditId("video-audit:auto")).toBeNull() // bare auto-family id, no bucket
|
|
202
|
+
// The VA-anchored id space must never be mistaken for an audit id, and vice versa.
|
|
203
|
+
expect(bucketSecondsFromAuditCreditId("video-analysis:gemini-3-flash:60s")).toBeNull()
|
|
204
|
+
expect(bucketSecondsFromAuditCreditId("video-analysis:mixed:600s")).toBeNull()
|
|
205
|
+
expect(bucketSecondsFromCreditId("video-audit:60s")).toBeNull()
|
|
206
|
+
expect(bucketSecondsFromCreditId("video-audit:auto:60s")).toBeNull()
|
|
207
|
+
expect(bucketSecondsFromAuditCreditId("garbage")).toBeNull()
|
|
208
|
+
expect(bucketSecondsFromAuditCreditId("")).toBeNull()
|
|
209
|
+
expect(bucketSecondsFromAuditCreditId("video-audit:60seconds")).toBeNull()
|
|
210
|
+
expect(bucketSecondsFromAuditCreditId("video-audit-auto:60s")).toBeNull() // hyphen, not colon — not a legal id
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it("VIDEO_AUDIT_BUCKET_CREDITS: exactly both families × 4 buckets, positive integers, bare-id ceiling pins", () => {
|
|
214
|
+
for (const bucketSec of VIDEO_ANALYSIS_DURATION_BUCKETS) {
|
|
215
|
+
for (const id of [`video-audit:${bucketSec}s`, `video-audit:auto:${bucketSec}s`]) {
|
|
216
|
+
const credits = VIDEO_AUDIT_BUCKET_CREDITS[id]
|
|
217
|
+
expect(credits, `missing entry for ${id}`).toBeDefined()
|
|
218
|
+
expect(Number.isInteger(credits)).toBe(true)
|
|
219
|
+
expect(credits).toBeGreaterThan(0)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Exactly 2 families × 4 buckets — no stray keys, no bare-id keys (bare
|
|
223
|
+
// ids live only in model-catalog.ts's pricing rows, derived from the
|
|
224
|
+
// 600s bucket here, same convention as VIDEO_ANALYSIS_BUCKET_CREDITS).
|
|
225
|
+
expect(Object.keys(VIDEO_AUDIT_BUCKET_CREDITS)).toHaveLength(8)
|
|
226
|
+
// Bare-id values quoted in the task/catalog must equal each family's 600s ceiling.
|
|
227
|
+
expect(VIDEO_AUDIT_BUCKET_CREDITS["video-audit:600s"]).toBe(1066)
|
|
228
|
+
expect(VIDEO_AUDIT_BUCKET_CREDITS["video-audit:auto:600s"]).toBe(1912)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("auto family = base family + the gemini-3-flash (legacy fast tier) row at the same bucket, exactly — single-source, never hand-added", () => {
|
|
232
|
+
// Single-source assertion: reads the fast-tier row straight out of
|
|
233
|
+
// VIDEO_ANALYSIS_BUCKET_CREDITS rather than re-hardcoding 180/185/514/846
|
|
234
|
+
// here, so this test can't silently drift from that table either.
|
|
235
|
+
for (const bucketSec of VIDEO_ANALYSIS_DURATION_BUCKETS) {
|
|
236
|
+
const base = VIDEO_AUDIT_BUCKET_CREDITS[`video-audit:${bucketSec}s`]!
|
|
237
|
+
const auto = VIDEO_AUDIT_BUCKET_CREDITS[`video-audit:auto:${bucketSec}s`]!
|
|
238
|
+
const fastRow = VIDEO_ANALYSIS_BUCKET_CREDITS[`video-analysis:gemini-3-flash:${bucketSec}s`]!
|
|
239
|
+
expect(auto - base, `auto-base mismatch at ${bucketSec}s`).toBe(fastRow)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
})
|
|
@@ -353,6 +353,19 @@ describe("misc", () => {
|
|
|
353
353
|
expect(isOversizedScene(0, 8)).toBe(false)
|
|
354
354
|
expect(isOversizedScene(0, 8.5)).toBe(true)
|
|
355
355
|
})
|
|
356
|
+
|
|
357
|
+
it("does not flag an exactly-8s scene whose float subtraction overshoots", () => {
|
|
358
|
+
// Real job bdc9c8eb: 12.67 → 20.67 computes as 8.000000000000002.
|
|
359
|
+
expect(20.67 - 12.67).toBeGreaterThan(8) // the hazard this guards
|
|
360
|
+
expect(isOversizedScene(12.67, 20.67)).toBe(false)
|
|
361
|
+
// Same hazard at other offsets — none of these are genuinely over the cap.
|
|
362
|
+
expect(isOversizedScene(4.67, 12.67)).toBe(false)
|
|
363
|
+
expect(isOversizedScene(0.1, 8.1)).toBe(false)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it("still flags a real overshoot far smaller than a boundary step", () => {
|
|
367
|
+
expect(isOversizedScene(0, 8.01)).toBe(true)
|
|
368
|
+
})
|
|
356
369
|
it("aspectRatioFromDims snaps to nearest standard, else reduces", () => {
|
|
357
370
|
expect(aspectRatioFromDims(1920, 1080)).toBe("16:9")
|
|
358
371
|
expect(aspectRatioFromDims(1080, 1920)).toBe("9:16")
|
|
@@ -15,9 +15,13 @@ import {
|
|
|
15
15
|
RESOLUTION_DURATION_PRICING,
|
|
16
16
|
VEO_RESOLUTION_TIERED_PROVIDERS,
|
|
17
17
|
VIDEO_DURATION_TIERS,
|
|
18
|
+
PRICING_DEFAULT_DURATION_SEC,
|
|
19
|
+
PRICING_DEFAULT_RESOLUTION,
|
|
18
20
|
MOTION_DURATION_TIERS,
|
|
19
21
|
T2I_TO_I2I_VARIANT,
|
|
20
22
|
isVeoProvider,
|
|
23
|
+
isMinimaxH3Provider,
|
|
24
|
+
normalizeMinimaxH3Resolution,
|
|
21
25
|
getVideoAudioCapability,
|
|
22
26
|
} from "./model-constants.js"
|
|
23
27
|
import { isFlux2Model } from "./flux2-pricing.js"
|
|
@@ -219,8 +223,12 @@ export function buildVideoCreditModelIdentifier(
|
|
|
219
223
|
return effectiveProvider
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
// A named-provider request with NO duration renders the model's own default
|
|
227
|
+
// (kie/models.ts extraParams), so price that default — not the global 5s —
|
|
228
|
+
// for providers whose per-second tiers wouldn't snap 5 up to it (minimax-h3).
|
|
229
|
+
const durationFallback = PRICING_DEFAULT_DURATION_SEC[effectiveProvider] ?? 5
|
|
230
|
+
const parsed = typeof duration === "string" ? parseInt(duration, 10) : (duration ?? durationFallback)
|
|
231
|
+
const durationSec = Number.isNaN(parsed) ? durationFallback : parsed
|
|
224
232
|
const tiers = VIDEO_DURATION_TIERS[effectiveProvider]
|
|
225
233
|
if (!tiers) return effectiveProvider
|
|
226
234
|
|
|
@@ -253,7 +261,13 @@ export function buildVideoCreditModelIdentifier(
|
|
|
253
261
|
// maps to its top priced tier instead of emitting an unpriced composite
|
|
254
262
|
// (which the hard-fail credit guard would 503 on at runtime).
|
|
255
263
|
const supported = MODEL_CATALOG[effectiveProvider]?.resolutions ?? ["480p", "720p", "1080p"]
|
|
256
|
-
|
|
264
|
+
// An OMITTED resolution renders the model's KIE-side default, so price that
|
|
265
|
+
// default rather than the cheapest tier — otherwise an intent-less request
|
|
266
|
+
// reserves 480p against a 720p render and commit_credits (refund-only) can
|
|
267
|
+
// never collect the delta. Providers without an entry keep the historical
|
|
268
|
+
// 480p fallback, so this cannot reprice anything already live.
|
|
269
|
+
const requested = resolution ?? PRICING_DEFAULT_RESOLUTION[effectiveProvider]
|
|
270
|
+
const want = requested === "4k" ? "4k" : requested === "1080p" ? "1080p" : requested === "720p" ? "720p" : "480p"
|
|
257
271
|
// Unsupported (e.g. a stale 1080p on seedance-2-mini) clamps to the model's
|
|
258
272
|
// top priced tier so the emitted composite is always seeded.
|
|
259
273
|
const res = supported.includes(want) ? want : (supported[supported.length - 1] ?? "480p")
|
|
@@ -271,6 +285,16 @@ export function buildVideoCreditModelIdentifier(
|
|
|
271
285
|
identifier += `:${res}`
|
|
272
286
|
}
|
|
273
287
|
|
|
288
|
+
// MiniMax Hailuo 3 (768P lever, 2026-08-03): bare duration composites stay
|
|
289
|
+
// the 2K (default) rate — byte-identical to the pre-lever seeded rows, so
|
|
290
|
+
// existing workflows and admin overrides keep their ids — and only a
|
|
291
|
+
// verified 768P selection appends ":768p". normalizeMinimaxH3Resolution
|
|
292
|
+
// collapses anything else to 2K, matching what KIE renders for an
|
|
293
|
+
// omitted/unknown value, so billing can never undercut the render.
|
|
294
|
+
if (isMinimaxH3Provider(effectiveProvider) && normalizeMinimaxH3Resolution(resolution) === "768P") {
|
|
295
|
+
identifier += ":768p"
|
|
296
|
+
}
|
|
297
|
+
|
|
274
298
|
return identifier
|
|
275
299
|
}
|
|
276
300
|
|
package/src/film-pricing.ts
CHANGED
|
@@ -56,6 +56,7 @@ export const VIDEO_CLIP_CREDITS: Record<string, VideoClipCost> = {
|
|
|
56
56
|
"seedance-2": { credits: 50, clipSeconds: 8 }, // seedance-2:8s:720p-ref
|
|
57
57
|
"seedance-2-fast": { credits: 40, clipSeconds: 8 }, // seedance-2-fast:8s:720p-ref
|
|
58
58
|
"seedance-2-mini": { credits: 25, clipSeconds: 8 }, // seedance-2-mini:8s:720p-ref
|
|
59
|
+
"seedance-2-5": { credits: 76, clipSeconds: 8 }, // seedance-2-5:8s:720p-ref
|
|
59
60
|
"veo3": { credits: 79, clipSeconds: 8 }, // flat per generation (VEO 3.1 Quality)
|
|
60
61
|
"veo3.1": { credits: 19, clipSeconds: 6 }, // veo3.1 @ 720p
|
|
61
62
|
"veo3_lite": { credits: 10, clipSeconds: 6 }, // veo3_lite @ 720p
|
package/src/index.ts
CHANGED
|
@@ -97,10 +97,12 @@ export {
|
|
|
97
97
|
GUIDANCE_SCALE_SUPPORT,
|
|
98
98
|
SEEDANCE_2_PROVIDERS,
|
|
99
99
|
SEEDANCE_2_REF_LIMITS,
|
|
100
|
+
SEEDANCE_2_5_REF_LIMITS,
|
|
100
101
|
SEEDANCE_2_EXTEND_STITCH,
|
|
101
102
|
SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC,
|
|
102
103
|
SEEDANCE_2_CONTINUATION_REF_SEC,
|
|
103
104
|
NATIVE_ADAPTIVE_ASPECT,
|
|
105
|
+
FRAME_MODE_ADAPTIVE_ONLY_ASPECT,
|
|
104
106
|
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
105
107
|
VIDEO_PROVIDERS_REQUIRING_IMAGE,
|
|
106
108
|
videoProviderRequiresImage,
|
|
@@ -108,8 +110,25 @@ export {
|
|
|
108
110
|
VIDEO_GEN_COLLAPSED_T2V_IDS,
|
|
109
111
|
resolveVideoProviderForMode,
|
|
110
112
|
isSeedance2Provider,
|
|
113
|
+
MINIMAX_H3_PROVIDERS,
|
|
114
|
+
isMinimaxH3Provider,
|
|
115
|
+
MINIMAX_H3_DEFAULT_RESOLUTION,
|
|
116
|
+
normalizeMinimaxH3Resolution,
|
|
117
|
+
VIDEO_PROVIDERS_WITHOUT_DISPATCH,
|
|
118
|
+
GVP_DEFAULT_PROVIDER,
|
|
111
119
|
GVP_SUPPORTED_PROVIDERS,
|
|
112
120
|
isGvpSupportedProvider,
|
|
121
|
+
GVP_EXTEND_PROVIDERS,
|
|
122
|
+
supportsExtendRender,
|
|
123
|
+
GVP_END_FRAME_PROVIDERS,
|
|
124
|
+
supportsEndAnchor,
|
|
125
|
+
GVP_ANCHOR_CHOICES,
|
|
126
|
+
resolveGvpAnchorWire,
|
|
127
|
+
segmentDurationsFor,
|
|
128
|
+
minSegmentSecFor,
|
|
129
|
+
maxSegmentSecFor,
|
|
130
|
+
hasContiguousSegmentDurations,
|
|
131
|
+
maxSegmentsFor,
|
|
113
132
|
defaultVideoAspectRatio,
|
|
114
133
|
CHARACTER_MOTION_PROVIDERS,
|
|
115
134
|
LOCATION_ATMOSPHERE_PROVIDERS,
|
|
@@ -126,6 +145,8 @@ export {
|
|
|
126
145
|
DEFAULT_VIDEO_PROVIDER,
|
|
127
146
|
DEFAULT_VIDEO_DURATION_SEC,
|
|
128
147
|
applyDefaultVideoSelection,
|
|
148
|
+
PRICING_DEFAULT_DURATION_SEC,
|
|
149
|
+
PRICING_DEFAULT_RESOLUTION,
|
|
129
150
|
} from "./model-constants.js"
|
|
130
151
|
|
|
131
152
|
export {
|
|
@@ -177,6 +198,8 @@ export type {
|
|
|
177
198
|
VideoModelCapabilities,
|
|
178
199
|
VideoAudioMode,
|
|
179
200
|
VideoAudioCapability,
|
|
201
|
+
GvpAnchorChoice,
|
|
202
|
+
GvpAnchorWireMode,
|
|
180
203
|
} from "./model-constants.js"
|
|
181
204
|
|
|
182
205
|
|
|
@@ -895,6 +918,8 @@ export * from "./reference-roles.js"
|
|
|
895
918
|
export * from "./video-analysis.js"
|
|
896
919
|
|
|
897
920
|
// --- Video-analysis pricing (duration buckets + structural credit formula) ---
|
|
921
|
+
// Also hosts video-audit's sibling pricing (VIDEO_AUDIT_BUCKET_CREDITS +
|
|
922
|
+
// buildVideoAuditCreditId / videoAuditCreditsForBucket / bucketSecondsFromAuditCreditId).
|
|
898
923
|
export * from "./video-analysis-pricing.js"
|
|
899
924
|
|
|
900
925
|
// --- Smart-cut best-pair search windows (shared bound + clamp) ---
|
package/src/llm-models.ts
CHANGED
|
@@ -712,14 +712,17 @@ export const VIDEO_ANALYSIS_MIXED_TIERS = ["mixed", "mixed-fast", "smart"] as co
|
|
|
712
712
|
export type VideoAnalysisMixedTier = (typeof VIDEO_ANALYSIS_MIXED_TIERS)[number]
|
|
713
713
|
/**
|
|
714
714
|
* `smart` is an ENGINE-PLAN sentinel like the mixed tiers — it names a plan, not a
|
|
715
|
-
* model — but it is
|
|
715
|
+
* model — but it is a different SHAPE of plan, and the distinction is the whole
|
|
716
716
|
* reason it exists as a separate tier rather than a repricing of the others.
|
|
717
717
|
*
|
|
718
718
|
* The economy tiers (`fast`, `pro`, `mixed`, `mixed-fast`) run several passes on
|
|
719
|
-
* the cheaper proxied transport and vote. That transport is
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* native
|
|
719
|
+
* the cheaper proxied transport and vote. That transport is cheaper per token and
|
|
720
|
+
* additionally does no deep reasoning, which is why they cost less — and also why
|
|
721
|
+
* they are less accurate. `smart` is a HYBRID plan (2026-08-03 re-plan): one
|
|
722
|
+
* native-transport skeleton pass with everything turned up, blended with several
|
|
723
|
+
* economy-transport donor rolls, and the merged result is always refined —
|
|
724
|
+
* `selectionMode` (the `choose`/`combine` toggle the other tiers expose) does not
|
|
725
|
+
* apply to `smart`; it always applies the equivalent of `combine`.
|
|
723
726
|
*
|
|
724
727
|
* As with the mixed sentinels, what the plan does internally is deliberately not
|
|
725
728
|
* published here; only the wire vocabulary is contract.
|