@nodaro/shared 2.19.0 → 2.21.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 +548 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1043 -386
- package/dist/index.d.ts +1043 -386
- package/dist/index.js +529 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +133 -0
- package/src/__tests__/image-pricing-catalog-coverage.test.ts +139 -0
- package/src/__tests__/normalize-node-params.test.ts +36 -0
- package/src/__tests__/organizations-types.test.ts +29 -1
- package/src/__tests__/prompt-length-limits.test.ts +36 -0
- package/src/__tests__/safety-retry-policy.test.ts +47 -0
- package/src/__tests__/suno-credit-type.test.ts +54 -0
- package/src/__tests__/topaz-upscale.test.ts +132 -0
- package/src/__tests__/unresolved-ref-tokens.test.ts +66 -0
- package/src/__tests__/video-analysis-brief.test.ts +61 -0
- package/src/__tests__/video-analysis.test.ts +83 -0
- package/src/__tests__/video-audio-capability.test.ts +38 -4
- package/src/__tests__/video-catalog-totality.test.ts +85 -0
- package/src/__tests__/video-collapse-parity.test.ts +76 -0
- package/src/__tests__/video-ref-video-duration-limits.test.ts +69 -0
- package/src/__tests__/video-request-normalize.test.ts +239 -0
- package/src/credit-identifiers.ts +264 -20
- package/src/index.ts +30 -2
- package/src/model-catalog.ts +287 -6
- package/src/model-constants.ts +185 -13
- package/src/node-refs.ts +82 -0
- package/src/node-runtime-keys.ts +5 -0
- package/src/normalize-node-params.ts +8 -0
- package/src/organizations/types.ts +12 -0
- package/src/organizations/views.ts +114 -0
- package/src/safety-retry-policy.ts +37 -0
- package/src/topaz-upscale.ts +163 -0
- package/src/video-analysis.ts +115 -5
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
normalizeVideoRequestParams,
|
|
4
|
+
pricedVideoSelection,
|
|
5
|
+
buildVideoCreditModelIdentifier,
|
|
6
|
+
MODEL_CATALOG,
|
|
7
|
+
VIDEO_GEN_PROVIDERS,
|
|
8
|
+
} from "../index.js"
|
|
9
|
+
|
|
10
|
+
describe("normalizeVideoRequestParams", () => {
|
|
11
|
+
// R6: NEAREST, not allowed[0]. A portrait request must not become landscape.
|
|
12
|
+
it("snaps an off-list ratio to the NEAREST member of the model's list", () => {
|
|
13
|
+
const r = normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "9:21" })
|
|
14
|
+
expect(r.aspectRatio).toBe("9:16")
|
|
15
|
+
expect(r.adjustments).toHaveLength(1)
|
|
16
|
+
})
|
|
17
|
+
it("snaps 4:5 and 5:4 the same way the provider adapter does", () => {
|
|
18
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "4:5" }).aspectRatio).toBe("3:4")
|
|
19
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "5:4" }).aspectRatio).toBe("4:3")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("passes 'Auto' and 'adaptive' through untouched (the provider decides)", () => {
|
|
23
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "Auto" }).aspectRatio).toBe("Auto")
|
|
24
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "adaptive" }).aspectRatio).toBe("adaptive")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// R7: nearest band, not the cheapest. A 4k request on a 1080p-max model is
|
|
28
|
+
// 1080p, never 480p.
|
|
29
|
+
it("snaps an off-list resolution to the NEAREST declared band", () => {
|
|
30
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: "4k" }).resolution).toBe("1080p")
|
|
31
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: "2k" }).resolution).toBe("1080p")
|
|
32
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: "360p" }).resolution).toBe("480p")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// §11.3 log-pull: two t2v rows failed createTask with "resolution is not
|
|
36
|
+
// within the range of allowed options". KIE exposes only 480p/720p/1080p for
|
|
37
|
+
// seedance-2-5 (providers/kie/models.ts:664-680, live probes 2026-08-08 and
|
|
38
|
+
// 08-17) while the route types `resolution` as a bare string. Asserts the
|
|
39
|
+
// exact snapped value, not membership — R7: the nearest band is 1080p, and a
|
|
40
|
+
// `toContain` pin would have hidden a silent downgrade to 480p.
|
|
41
|
+
it("snaps the off-list seedance-2-5 resolutions the t2v route admits, to the NEAREST band", () => {
|
|
42
|
+
for (const bad of ["2k", "4k", "1440p"]) {
|
|
43
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: bad }).resolution).toBe("1080p")
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// R3: the one place a case variant is canonicalised, upstream of every
|
|
48
|
+
// credit identifier (which key their tables case-sensitively).
|
|
49
|
+
it("canonicalises a case variant to the catalog's own spelling", () => {
|
|
50
|
+
expect(normalizeVideoRequestParams("ltx-2.3-fast", { resolution: "4K" }).resolution).toBe("4k")
|
|
51
|
+
expect(normalizeVideoRequestParams("ltx-2.3-fast", { resolution: "1080P" }).resolution).toBe("1080p")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("snaps a resolution onto the model's own list", () => {
|
|
55
|
+
expect(normalizeVideoRequestParams("ltx-2.3-fast", { resolution: "720p" }).resolution).toBe("1080p")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("NEVER drops a resolution for a model that declares none — dropping would lower the reserved tier", () => {
|
|
59
|
+
const r = normalizeVideoRequestParams("kling-turbo", { resolution: "1080p" })
|
|
60
|
+
expect(r.resolution).toBe("1080p")
|
|
61
|
+
expect(r.adjustments).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("leaves an unknown model completely alone", () => {
|
|
65
|
+
const r = normalizeVideoRequestParams("not-a-model", { aspectRatio: "9:21", resolution: "720p" })
|
|
66
|
+
expect(r).toMatchObject({ aspectRatio: "9:21", resolution: "720p", adjustments: [] })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("is idempotent", () => {
|
|
70
|
+
const once = normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "9:21", resolution: "1080p" })
|
|
71
|
+
const twice = normalizeVideoRequestParams("seedance-2-5", once)
|
|
72
|
+
expect(twice.aspectRatio).toBe(once.aspectRatio)
|
|
73
|
+
expect(twice.resolution).toBe(once.resolution)
|
|
74
|
+
expect(twice.adjustments).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// The normalizer runs in the creditGuard preHandler (BEFORE the route's Zod
|
|
78
|
+
// parse) and in every buildPayload branch (whose `data` is unvalidated
|
|
79
|
+
// persisted workflow JSON, and whose aspectRatio can be FieldMapping-injected
|
|
80
|
+
// at run time). A non-string lever must therefore coerce, never throw: a throw
|
|
81
|
+
// is a 500 where the route used to return a clean Zod 400, and in the DAG it
|
|
82
|
+
// takes the whole run down after sibling nodes have already reserved.
|
|
83
|
+
it("coerces a NUMERIC lever exactly like its string form, instead of throwing", () => {
|
|
84
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: 1080 as never }).resolution)
|
|
85
|
+
.toBe(normalizeVideoRequestParams("seedance-2-5", { resolution: "1080" }).resolution)
|
|
86
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: 16 as never }).aspectRatio)
|
|
87
|
+
.toBe(normalizeVideoRequestParams("seedance-2-5", { aspectRatio: "16" }).aspectRatio)
|
|
88
|
+
// "1080" is not "1080p", so it snaps to the nearest band rather than matching.
|
|
89
|
+
expect(normalizeVideoRequestParams("seedance-2-5", { resolution: 1080 as never }).resolution).toBe("1080p")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("snaps a non-string lever FAIL-SAFE rather than throwing", () => {
|
|
93
|
+
for (const junk of [{}, [1, 2], true, () => {}]) {
|
|
94
|
+
const r = () => normalizeVideoRequestParams("seedance-2-5", { resolution: junk as never, aspectRatio: junk as never })
|
|
95
|
+
expect(r, `resolution/aspectRatio = ${String(junk)}`).not.toThrow()
|
|
96
|
+
const out = r()
|
|
97
|
+
// Unparseable ⇒ the highest declared band (never the cheapest, R7) and a
|
|
98
|
+
// concrete ratio — always a value the model actually accepts.
|
|
99
|
+
expect(MODEL_CATALOG["seedance-2-5"]!.resolutions).toContain(out.resolution)
|
|
100
|
+
expect(MODEL_CATALOG["seedance-2-5"]!.aspectRatios).toContain(out.aspectRatio)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("reads null / blank as ABSENT, and never hands the raw value back", () => {
|
|
105
|
+
// The return type promises `string | undefined` and its callers feed it
|
|
106
|
+
// straight to the credit identifier and the provider wire, so a `null` or
|
|
107
|
+
// `""` must come back as `undefined` (the priced fill then supplies the band
|
|
108
|
+
// the identifier assumes) rather than as the caller's own value.
|
|
109
|
+
for (const blank of [null, "", " "]) {
|
|
110
|
+
const r = normalizeVideoRequestParams("seedance-2-5", { resolution: blank as never, aspectRatio: blank as never })
|
|
111
|
+
expect(r.resolution, `resolution = ${JSON.stringify(blank)}`).toBeUndefined()
|
|
112
|
+
expect(r.aspectRatio, `aspectRatio = ${JSON.stringify(blank)}`).toBeUndefined()
|
|
113
|
+
expect(r.adjustments).toEqual([])
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("never returns a non-string lever, even for an unknown model", () => {
|
|
118
|
+
const r = normalizeVideoRequestParams("not-a-model", { resolution: 1080 as never, aspectRatio: 16 as never })
|
|
119
|
+
expect(r.resolution).toBe("1080")
|
|
120
|
+
expect(r.aspectRatio).toBe("16")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("leaves an omitted lever omitted — the pricing fill is a separate, deliberate step", () => {
|
|
124
|
+
const r = normalizeVideoRequestParams("ltx-2.3-pro", {})
|
|
125
|
+
expect(r.resolution).toBeUndefined()
|
|
126
|
+
expect(r.aspectRatio).toBeUndefined()
|
|
127
|
+
expect(r.adjustments).toEqual([])
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("pricedVideoSelection", () => {
|
|
132
|
+
// (A) The identifier prices an ABSENT resolution as a concrete band. Where
|
|
133
|
+
// that band is the platform's DECLARED provider default, it must also be the
|
|
134
|
+
// value we send — reserving 1080p and sending no key at all let Replicate
|
|
135
|
+
// pick its own undocumented default.
|
|
136
|
+
it("fills the declared default band for LTX", () => {
|
|
137
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro" }).resolution).toBe("1080p")
|
|
138
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-fast" }).resolution).toBe("1080p")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("fills the declared default band for the providers that declare one", () => {
|
|
142
|
+
expect(pricedVideoSelection({ provider: "seedance-2-5" }).resolution).toBe("720p")
|
|
143
|
+
expect(pricedVideoSelection({ provider: "wan-3" }).resolution).toBe("720p")
|
|
144
|
+
expect(pricedVideoSelection({ provider: "wan-3-prime" }).resolution).toBe("720p")
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("fills NOTHING for a provider with no declared default — its identifier fallback is a hedge, not a verified provider default", () => {
|
|
148
|
+
// seedance-2 / -fast / -mini pin resolution 720p KIE-side but have no
|
|
149
|
+
// PRICING_DEFAULT_RESOLUTION row, so the identifier prices 480p. Filling
|
|
150
|
+
// 480p on the wire would DOWNGRADE the render to match a known-wrong price.
|
|
151
|
+
expect(pricedVideoSelection({ provider: "seedance-2" }).resolution).toBeUndefined()
|
|
152
|
+
expect(pricedVideoSelection({ provider: "seedance-2-mini" }).resolution).toBeUndefined()
|
|
153
|
+
expect(pricedVideoSelection({ provider: "kling-turbo" }).resolution).toBeUndefined()
|
|
154
|
+
expect(pricedVideoSelection({ provider: "veo3" }).resolution).toBeUndefined()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("never overrides an explicit resolution", () => {
|
|
158
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro", resolution: "4k" }).resolution).toBe("4k")
|
|
159
|
+
expect(pricedVideoSelection({ provider: "seedance-2-5", resolution: "480p" }).resolution).toBe("480p")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// (B) LTX duration: the identifier snaps onto a SEEDED per-band tier, so the
|
|
163
|
+
// wire must carry that tier. 7s prices as 6s — send 6s.
|
|
164
|
+
it("carries the seeded LTX duration tier the identifier priced", () => {
|
|
165
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro", duration: 7 }).duration).toBe(6)
|
|
166
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro", resolution: "4k", duration: 20 }).duration).toBe(10)
|
|
167
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-fast", duration: 20 }).duration).toBe(20)
|
|
168
|
+
// 20s exists only at 1080p — a 2k request snaps back onto that band's ladder.
|
|
169
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-fast", resolution: "2k", duration: 20 }).duration).toBe(10)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("reports the LTX duration snap as an adjustment, but never the omitted-value fill", () => {
|
|
173
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro", duration: 7 }).adjustments).toHaveLength(1)
|
|
174
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro", duration: 6 }).adjustments).toEqual([])
|
|
175
|
+
expect(pricedVideoSelection({ provider: "ltx-2.3-pro" }).adjustments).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("leaves duration alone for every non-LTX provider (legality is not a flat catalog list)", () => {
|
|
179
|
+
expect(pricedVideoSelection({ provider: "seedance-2-5", duration: 7 }).duration).toBeUndefined()
|
|
180
|
+
expect(pricedVideoSelection({ provider: "kling-3.0", duration: 7 }).duration).toBeUndefined()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The invariant that makes the FILL safe: carrying the priced selection to the
|
|
186
|
+
* wire can never move the reserved tier. (The catalog snap that runs before it
|
|
187
|
+
* legitimately can — an off-list "4K" on a 1080p-max model is priced at the
|
|
188
|
+
* band we will actually render, which is the whole point. This pins the second
|
|
189
|
+
* step only: given the snapped value, filling what the identifier assumed is a
|
|
190
|
+
* disclosure of the price already being charged, never a repricing.)
|
|
191
|
+
*/
|
|
192
|
+
describe("pricedVideoSelection cannot move the credit identifier", () => {
|
|
193
|
+
const RESOLUTIONS = [undefined, "480p", "720p", "1080p", "2k", "4k", "4K", "720P"]
|
|
194
|
+
const DURATIONS = [undefined, 4, 5, 6, 7, 8, 10, 12, 20, 30]
|
|
195
|
+
|
|
196
|
+
it("covers every catalogued video provider", () => {
|
|
197
|
+
expect(VIDEO_GEN_PROVIDERS.length).toBeGreaterThan(20)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
for (const nodeType of ["text-to-video", "image-to-video"] as const) {
|
|
201
|
+
it(`${nodeType}: id(priced) === id(raw) for every provider × resolution × duration`, () => {
|
|
202
|
+
const drift: string[] = []
|
|
203
|
+
for (const provider of VIDEO_GEN_PROVIDERS) {
|
|
204
|
+
for (const rawRes of RESOLUTIONS) {
|
|
205
|
+
for (const rawDur of DURATIONS) {
|
|
206
|
+
for (const hasVideoRef of [false, true]) {
|
|
207
|
+
const norm = normalizeVideoRequestParams(provider, { resolution: rawRes })
|
|
208
|
+
const priced = pricedVideoSelection({ provider, resolution: norm.resolution, duration: rawDur })
|
|
209
|
+
const rawId = buildVideoCreditModelIdentifier(provider, rawDur, undefined, nodeType, undefined, norm.resolution, hasVideoRef)
|
|
210
|
+
const pricedId = buildVideoCreditModelIdentifier(
|
|
211
|
+
provider,
|
|
212
|
+
priced.duration ?? rawDur,
|
|
213
|
+
undefined,
|
|
214
|
+
nodeType,
|
|
215
|
+
undefined,
|
|
216
|
+
norm.resolution ?? priced.resolution,
|
|
217
|
+
hasVideoRef,
|
|
218
|
+
)
|
|
219
|
+
if (rawId !== pricedId) {
|
|
220
|
+
drift.push(`${provider} res=${rawRes} dur=${rawDur} ref=${hasVideoRef}: ${rawId} → ${pricedId}`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
expect(drift, `the normalize+fill pair moved the reserved tier:\n${drift.join("\n")}`).toEqual([])
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
it("only fills a resolution the model's own catalog declares", () => {
|
|
231
|
+
for (const provider of VIDEO_GEN_PROVIDERS) {
|
|
232
|
+
const filled = pricedVideoSelection({ provider }).resolution
|
|
233
|
+
if (filled === undefined) continue
|
|
234
|
+
const declared = MODEL_CATALOG[provider]?.resolutions as readonly string[] | undefined
|
|
235
|
+
expect(declared, `${provider} fills "${filled}" but declares no resolutions`).toBeDefined()
|
|
236
|
+
expect(declared, `${provider} fills "${filled}", which is not in its catalog list`).toContain(filled)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
})
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
getVideoAudioCapability,
|
|
29
29
|
} from "./model-constants.js"
|
|
30
30
|
import { isFlux2Model } from "./flux2-pricing.js"
|
|
31
|
-
import { MODEL_CATALOG } from "./model-catalog.js"
|
|
31
|
+
import { MODEL_CATALOG, normalizeModelInput, type ModelInputAdjustment } from "./model-catalog.js"
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Compute composite model identifier for variable credit pricing.
|
|
@@ -80,6 +80,90 @@ export function buildCreditModelIdentifier(
|
|
|
80
80
|
return provider
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* An image request's catalog-snapped parameters PLUS the credit identifier
|
|
85
|
+
* priced off them.
|
|
86
|
+
*
|
|
87
|
+
* `adjustments` is the disclosure channel: every lever this changed, why, and
|
|
88
|
+
* to what. Routes return it in the 200 body so a caller learns their value was
|
|
89
|
+
* corrected instead of silently getting something else.
|
|
90
|
+
*/
|
|
91
|
+
export interface NormalizedImageGen {
|
|
92
|
+
/** Credit identifier, priced off the SNAPPED `resolution` / `quality`. */
|
|
93
|
+
identifier: string
|
|
94
|
+
/** The catalog model id the params were snapped against (post T2I→I2I swap). */
|
|
95
|
+
modelId: string
|
|
96
|
+
aspectRatio?: string
|
|
97
|
+
resolution?: string
|
|
98
|
+
quality?: string
|
|
99
|
+
/** Empty when the caller's values were already valid for `modelId`. */
|
|
100
|
+
adjustments: ModelInputAdjustment[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Snap an image request's catalog-governed levers to a combination the model
|
|
105
|
+
* actually accepts, and price the credit identifier off the SNAPPED values.
|
|
106
|
+
*
|
|
107
|
+
* WHY THE SNAP LIVES HERE and not at the call site. Both image routes compute
|
|
108
|
+
* their credit identifier TWICE — once in the `creditGuard` preHandler (the
|
|
109
|
+
* CHECK) and once at the handler's `reserveCreditsForJob` (the DEBIT) — and a
|
|
110
|
+
* dedicated test asserts the two stay byte-identical (see
|
|
111
|
+
* `backend/src/routes/__tests__/generate-image.test.ts`, "CHECK === DEBIT").
|
|
112
|
+
* `resolution` and `quality` are pricing dimensions, so a snap applied at only
|
|
113
|
+
* one of those sites breaks that invariant, and `commit_credits` never collects
|
|
114
|
+
* an upward delta — the reserve IS the charge. Putting the snap inside the
|
|
115
|
+
* primitive makes CHECK, DEBIT and the workflow orchestrator agree by
|
|
116
|
+
* construction rather than by everyone remembering to call a normalizer.
|
|
117
|
+
*
|
|
118
|
+
* Inputs are typed `unknown` on purpose: the preHandler runs BEFORE Zod, so a
|
|
119
|
+
* caller can put a number or an object in `resolution`. Non-strings become
|
|
120
|
+
* `undefined` rather than reaching `normalizeModelInput` as a lie.
|
|
121
|
+
*
|
|
122
|
+
* Unknown model ids pass through untouched (same contract as
|
|
123
|
+
* `normalizeModelInput`) — the route's provider enum is the gate for those.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveNormalizedImageGen(opts: {
|
|
126
|
+
provider: string | undefined
|
|
127
|
+
aspectRatio?: unknown
|
|
128
|
+
quality?: unknown
|
|
129
|
+
resolution?: unknown
|
|
130
|
+
renderingSpeed?: unknown
|
|
131
|
+
refCount: number
|
|
132
|
+
swapToI2i?: boolean
|
|
133
|
+
}): NormalizedImageGen {
|
|
134
|
+
const str = (v: unknown): string | undefined =>
|
|
135
|
+
typeof v === "string" && v.length > 0 ? v : undefined
|
|
136
|
+
|
|
137
|
+
const provider = str(opts.provider) ?? "nano-banana"
|
|
138
|
+
// Same swap `resolveEffectiveProvider` applies in the route: refs attached to
|
|
139
|
+
// a bare T2I provider route the run to its i2i sibling, which has its OWN
|
|
140
|
+
// catalog entry and its own lever lists.
|
|
141
|
+
const modelId =
|
|
142
|
+
opts.swapToI2i && opts.refCount > 0 ? (T2I_TO_I2I_VARIANT[provider] ?? provider) : provider
|
|
143
|
+
|
|
144
|
+
const n = normalizeModelInput(modelId, {
|
|
145
|
+
aspectRatio: str(opts.aspectRatio),
|
|
146
|
+
resolution: str(opts.resolution),
|
|
147
|
+
quality: str(opts.quality),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
identifier: buildCreditModelIdentifier(
|
|
152
|
+
modelId,
|
|
153
|
+
n.quality,
|
|
154
|
+
n.resolution,
|
|
155
|
+
str(opts.renderingSpeed),
|
|
156
|
+
undefined,
|
|
157
|
+
opts.refCount,
|
|
158
|
+
),
|
|
159
|
+
modelId,
|
|
160
|
+
aspectRatio: n.aspectRatio,
|
|
161
|
+
resolution: n.resolution,
|
|
162
|
+
quality: n.quality,
|
|
163
|
+
adjustments: n.adjustments,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
83
167
|
/**
|
|
84
168
|
* Reference-aware image-generation credit identifier — the SINGLE source of
|
|
85
169
|
* truth shared by the single-node routes (`/v1/generate-image`,
|
|
@@ -107,17 +191,7 @@ export function resolveImageGenCreditIdentifier(opts: {
|
|
|
107
191
|
refCount: number
|
|
108
192
|
swapToI2i?: boolean
|
|
109
193
|
}): string {
|
|
110
|
-
|
|
111
|
-
const effectiveProvider =
|
|
112
|
-
opts.swapToI2i && opts.refCount > 0 ? (T2I_TO_I2I_VARIANT[provider] ?? provider) : provider
|
|
113
|
-
return buildCreditModelIdentifier(
|
|
114
|
-
effectiveProvider,
|
|
115
|
-
opts.quality,
|
|
116
|
-
opts.resolution,
|
|
117
|
-
opts.renderingSpeed,
|
|
118
|
-
undefined,
|
|
119
|
-
opts.refCount,
|
|
120
|
-
)
|
|
194
|
+
return resolveNormalizedImageGen(opts).identifier
|
|
121
195
|
}
|
|
122
196
|
|
|
123
197
|
// T2V-specific credit overrides: some providers have different costs for T2V
|
|
@@ -161,6 +235,32 @@ const LTX_DURATION_TIERS: Record<string, Record<string, number[]>> = {
|
|
|
161
235
|
"ltx-2.3-fast": { "1080p": [6, 8, 10, 12, 14, 16, 18, 20], "2k": [6, 8, 10], "4k": [6, 8, 10] },
|
|
162
236
|
}
|
|
163
237
|
|
|
238
|
+
/**
|
|
239
|
+
* The (band, duration) tier the LTX pricing ladder charges for a request, or
|
|
240
|
+
* `undefined` for a non-LTX provider. Extracted so
|
|
241
|
+
* `buildVideoCreditModelIdentifier` and `pricedVideoSelection` read the ladder
|
|
242
|
+
* ONCE — the routes must send the tier they reserved, and a second copy of this
|
|
243
|
+
* math is exactly how the two drift apart.
|
|
244
|
+
*
|
|
245
|
+
* An unknown/absent band falls back to 1080p (LTX's own default, and the band
|
|
246
|
+
* `snapLtxInput` snaps to at the Replicate call), and an off-tier duration
|
|
247
|
+
* snaps to the NEAREST seeded one so the emitted composite always prices.
|
|
248
|
+
*/
|
|
249
|
+
function ltxPricedTier(
|
|
250
|
+
provider: string,
|
|
251
|
+
resolution?: string,
|
|
252
|
+
duration?: number | string,
|
|
253
|
+
): { band: string; duration: number } | undefined {
|
|
254
|
+
const bands = LTX_DURATION_TIERS[provider]
|
|
255
|
+
if (!bands) return undefined
|
|
256
|
+
const band = bands[String(resolution)] ? String(resolution) : "1080p"
|
|
257
|
+
const allowed = bands[band]!
|
|
258
|
+
const raw = typeof duration === "string" ? parseInt(duration, 10) : (duration ?? allowed[0]!)
|
|
259
|
+
const want = Number.isNaN(raw) ? allowed[0]! : raw
|
|
260
|
+
const dur = allowed.reduce((b, a) => (Math.abs(a - want) < Math.abs(b - want) ? a : b))
|
|
261
|
+
return { band, duration: dur }
|
|
262
|
+
}
|
|
263
|
+
|
|
164
264
|
export function buildVideoCreditModelIdentifier(
|
|
165
265
|
provider: string,
|
|
166
266
|
duration?: number | string,
|
|
@@ -218,14 +318,9 @@ export function buildVideoCreditModelIdentifier(
|
|
|
218
318
|
// (actual < reserved) and NEVER collects an upward delta, so an under-reserved
|
|
219
319
|
// LTX run (the bare-id default = cheapest 1080p:6s tier) stays under-charged
|
|
220
320
|
// even with meteredCost:true. Snap to a seeded tier so the id always prices.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const allowed = bands[band]
|
|
225
|
-
const raw = typeof duration === "string" ? parseInt(duration, 10) : (duration ?? allowed[0])
|
|
226
|
-
const want = Number.isNaN(raw) ? allowed[0] : raw
|
|
227
|
-
const dur = allowed.reduce((b, a) => (Math.abs(a - want) < Math.abs(b - want) ? a : b))
|
|
228
|
-
return `${effectiveProvider}:${band}:${dur}s`
|
|
321
|
+
const ltxTier = ltxPricedTier(effectiveProvider, resolution, duration)
|
|
322
|
+
if (ltxTier) {
|
|
323
|
+
return `${effectiveProvider}:${ltxTier.band}:${ltxTier.duration}s`
|
|
229
324
|
}
|
|
230
325
|
|
|
231
326
|
if (!DURATION_PRICED_PROVIDERS.has(effectiveProvider)) {
|
|
@@ -331,6 +426,92 @@ export function buildVideoCreditModelIdentifier(
|
|
|
331
426
|
return identifier
|
|
332
427
|
}
|
|
333
428
|
|
|
429
|
+
/** What the video credit identifier PRICES for a request, for the levers whose
|
|
430
|
+
* priced value must also be the value we SEND. */
|
|
431
|
+
export interface PricedVideoSelection {
|
|
432
|
+
/** The resolution band the reservation is priced at when the request omitted
|
|
433
|
+
* one — and only where the platform DECLARES that band as the provider's own
|
|
434
|
+
* default. `undefined` means "leave `resolution` exactly as the request had
|
|
435
|
+
* it": either the caller supplied one, or the provider's real default is not
|
|
436
|
+
* known to be the band the identifier assumes. */
|
|
437
|
+
resolution?: string
|
|
438
|
+
/** The seeded duration tier the reservation is priced at (LTX only — the one
|
|
439
|
+
* family whose duration ladder is per-band and case-sensitively seeded).
|
|
440
|
+
* `undefined` for every other provider: their duration passes through. */
|
|
441
|
+
duration?: number
|
|
442
|
+
/** Non-empty only for a lever the caller ASKED for and did not get (an LTX
|
|
443
|
+
* 7s snapped to the 6s tier). Filling an omitted lever is a disclosure of
|
|
444
|
+
* the price already being charged, not a correction, so it reports nothing. */
|
|
445
|
+
adjustments: ModelInputAdjustment[]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The other half of the video money path: `normalizeVideoRequestParams` snaps a
|
|
450
|
+
* value the CATALOG governs; this returns the value the PRICE ladder governs.
|
|
451
|
+
*
|
|
452
|
+
* Two defects it closes, both of the same shape — the identifier prices one
|
|
453
|
+
* thing and the wire carries another, and `commit_credits` (migration 176) only
|
|
454
|
+
* ever refunds a surplus, so the reservation is the final charge:
|
|
455
|
+
*
|
|
456
|
+
* 1. **Absent resolution.** `buildVideoCreditModelIdentifier` prices an omitted
|
|
457
|
+
* `resolution` as a concrete band (LTX → 1080p; a
|
|
458
|
+
* {@link PRICING_DEFAULT_RESOLUTION} member → its declared default). Leaving
|
|
459
|
+
* the key unset then lets the provider pick — documented for the KIE members
|
|
460
|
+
* (their `extraParams` pin the same band) but UNDOCUMENTED for LTX on
|
|
461
|
+
* Replicate. Sending the band we priced makes the two agree by construction.
|
|
462
|
+
*
|
|
463
|
+
* The fill is deliberately limited to providers with a DECLARED default.
|
|
464
|
+
* `buildVideoCreditModelIdentifier` also has un-declared fallbacks — the
|
|
465
|
+
* cheapest tier for a resolution-priced provider with no
|
|
466
|
+
* `PRICING_DEFAULT_RESOLUTION` row — and those are a HEDGE, not a verified
|
|
467
|
+
* provider default: seedance-2 / -fast / -mini pin `resolution: "720p"`
|
|
468
|
+
* KIE-side while the identifier prices 480p, so filling 480p would downgrade
|
|
469
|
+
* the render to match a price we already know is wrong. That mismatch is a
|
|
470
|
+
* pre-existing identifier bug and is fixed by seeding the row, not here.
|
|
471
|
+
*
|
|
472
|
+
* 2. **LTX duration.** The LTX ladder is seeded per (band × seconds), so the
|
|
473
|
+
* identifier snaps 7s onto the 6s tier. Sending 7s bills six and renders
|
|
474
|
+
* seven. Most other providers' durations pass through untouched: their
|
|
475
|
+
* legality is a flat catalog list the caller already sees, and their tiers
|
|
476
|
+
* are ranges (`durationSec <= maxSeconds`), not seeded points. Gemini Omni
|
|
477
|
+
* is the one other seeded ladder — `GEMINI_OMNI_DURATIONS` ([4, 6, 8, 10])
|
|
478
|
+
* is nearest-snapped by `buildVideoCreditModelIdentifier`, the same shape
|
|
479
|
+
* as LTX — but this function does not carry it yet: a 7s request prices
|
|
480
|
+
* `:6` while 7s is still what gets sent. Pre-existing, out of scope here,
|
|
481
|
+
* and ticketed as a follow-up (`geminiOmniPricedTier`, mirroring
|
|
482
|
+
* `ltxPricedTier`).
|
|
483
|
+
*
|
|
484
|
+
* Pure, and IDEMPOTENT against the identifier: feeding its output back in
|
|
485
|
+
* cannot move the reserved tier. `video-request-normalize.test.ts` proves that
|
|
486
|
+
* over every provider × resolution × duration.
|
|
487
|
+
*/
|
|
488
|
+
export function pricedVideoSelection(opts: {
|
|
489
|
+
provider: string
|
|
490
|
+
resolution?: string
|
|
491
|
+
duration?: number | string
|
|
492
|
+
}): PricedVideoSelection {
|
|
493
|
+
const adjustments: ModelInputAdjustment[] = []
|
|
494
|
+
const ltx = ltxPricedTier(opts.provider, opts.resolution, opts.duration)
|
|
495
|
+
|
|
496
|
+
// The band the request will be priced AND rendered at. An explicit value
|
|
497
|
+
// always wins — this only ever fills an omission.
|
|
498
|
+
const resolution = opts.resolution
|
|
499
|
+
?? (ltx ? ltx.band : PRICING_DEFAULT_RESOLUTION[opts.provider])
|
|
500
|
+
|
|
501
|
+
if (!ltx) return { resolution, adjustments }
|
|
502
|
+
|
|
503
|
+
const requested = typeof opts.duration === "string" ? parseInt(opts.duration, 10) : opts.duration
|
|
504
|
+
if (requested !== undefined && !Number.isNaN(requested) && requested !== ltx.duration) {
|
|
505
|
+
adjustments.push({
|
|
506
|
+
field: "duration",
|
|
507
|
+
from: requested,
|
|
508
|
+
to: ltx.duration,
|
|
509
|
+
reason: `LTX renders ${ltx.band} in ${ltx.duration}s steps — using ${ltx.duration}s instead of ${requested}s.`,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
return { resolution, duration: ltx.duration, adjustments }
|
|
513
|
+
}
|
|
514
|
+
|
|
334
515
|
/**
|
|
335
516
|
* Compute composite model identifier for motion control with duration-tiered pricing.
|
|
336
517
|
* Examples: "kling-3.0-motion:10s", "kling-3.0-motion:1080p:15s", "motion-transfer:5s"
|
|
@@ -365,3 +546,66 @@ export function buildMotionCreditModelIdentifier(
|
|
|
365
546
|
const resSuffix = resolution === "1080p" ? ":1080p" : ""
|
|
366
547
|
return `${base}${resSuffix}:${tier.suffix}`
|
|
367
548
|
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The Suno operations whose credit key depends on the model VERSION.
|
|
552
|
+
*
|
|
553
|
+
* `/v1/suno/generate`, `/cover` and `/extend` resolve their creditGuard
|
|
554
|
+
* identifier through `sunoCreditType(model, <operation>)` (routes/suno.ts
|
|
555
|
+
* :247-249, :335-337, :407-409). EVERY other Suno route charges a flat
|
|
556
|
+
* per-operation key and ignores the version the node carries — mashup (:648),
|
|
557
|
+
* add-instrumental (:852), add-vocals (:907), upload-extend (:1016),
|
|
558
|
+
* replace-section (:710), style-boost (:771), convert-wav (:962),
|
|
559
|
+
* lyrics (:482), music-video (:594), voice (:1218).
|
|
560
|
+
*
|
|
561
|
+
* Anything that QUOTES a Suno price — a dropdown row, a node badge, the
|
|
562
|
+
* workflow-credit estimator — must follow the same split, or it displays a
|
|
563
|
+
* credit key the route never charges.
|
|
564
|
+
*/
|
|
565
|
+
export const SUNO_VERSION_PRICED_OPERATIONS = [
|
|
566
|
+
"suno-generate",
|
|
567
|
+
"suno-cover",
|
|
568
|
+
"suno-extend",
|
|
569
|
+
] as const
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* The seven Suno operations that appear behind a select/dropdown UI (model
|
|
573
|
+
* pickers, node badges, the credit estimator) — the three version-priced
|
|
574
|
+
* operations above, plus the four flat-key operations exercised by
|
|
575
|
+
* `sunoCreditType`'s own test suite. A readonly tuple so later tasks (the
|
|
576
|
+
* model dropdowns, node badges) can iterate or count it without redeclaring
|
|
577
|
+
* the list and drifting from this one.
|
|
578
|
+
*/
|
|
579
|
+
export const SUNO_SELECT_OPERATIONS = [
|
|
580
|
+
"suno-generate",
|
|
581
|
+
"suno-cover",
|
|
582
|
+
"suno-extend",
|
|
583
|
+
"suno-mashup",
|
|
584
|
+
"suno-add-instrumental",
|
|
585
|
+
"suno-add-vocals",
|
|
586
|
+
"suno-upload-extend",
|
|
587
|
+
] as const
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* OUR Nodaro credit key for a Suno operation, given the model version and the
|
|
591
|
+
* operation (which is also the node type and the BullMQ job name).
|
|
592
|
+
*
|
|
593
|
+
* This is the single source of truth for the Suno pricing contract, shared by
|
|
594
|
+
* the reservation path (routes/suno.ts creditGuard), the egress seam
|
|
595
|
+
* (workers/handlers/suno.ts `modelKey`, which must match what was reserved),
|
|
596
|
+
* both workflow-credit estimators (ee/billing/credits.ts and the frontend
|
|
597
|
+
* config-panels/helpers.ts), the seven Suno model dropdowns and the three Suno
|
|
598
|
+
* node badges. It is a billing key — NEVER a KIE provider id — which is what
|
|
599
|
+
* satisfies the B3 egress invariant.
|
|
600
|
+
*
|
|
601
|
+
* V5_5 → "suno-v5_5", V5 → "suno-v5", any other version → the operation key.
|
|
602
|
+
* A non-version-priced operation is returned unchanged.
|
|
603
|
+
*/
|
|
604
|
+
export function sunoCreditType(model: string | undefined, operation: string): string {
|
|
605
|
+
if (!(SUNO_VERSION_PRICED_OPERATIONS as readonly string[]).includes(operation)) {
|
|
606
|
+
return operation
|
|
607
|
+
}
|
|
608
|
+
if (model === "V5_5") return "suno-v5_5"
|
|
609
|
+
if (model === "V5") return "suno-v5"
|
|
610
|
+
return operation
|
|
611
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { usdToCredits, creditsToUsd, CREDIT_ROUNDING_RESOLUTION } from "./credit
|
|
|
18
18
|
export {
|
|
19
19
|
CREDIT_BASE_USD,
|
|
20
20
|
IMAGE_PROMPT_MAX,
|
|
21
|
+
IMAGE_ASPECT_RATIO_VALUES,
|
|
21
22
|
MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER,
|
|
22
23
|
getMaxImagePromptChars,
|
|
23
24
|
PROMPT_HARD_CEILING,
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
getMaxSunoStyleChars,
|
|
36
37
|
VIDEO_PROMPT_MAX,
|
|
37
38
|
SUNO_TEXT_MAX,
|
|
39
|
+
SUNO_HARD_CEILING,
|
|
38
40
|
NATIVE_NEGATIVE_PROMPT_MODELS,
|
|
39
41
|
NATIVE_NEGATIVE_VIDEO_PROVIDERS,
|
|
40
42
|
applyVideoNegativePrompt,
|
|
@@ -106,6 +108,8 @@ export {
|
|
|
106
108
|
NATIVE_ADAPTIVE_ASPECT,
|
|
107
109
|
FRAME_MODE_ADAPTIVE_ONLY_ASPECT,
|
|
108
110
|
VIDEO_REF_LIMITS_BY_PROVIDER,
|
|
111
|
+
VIDEO_REF_VIDEO_DURATION_LIMITS,
|
|
112
|
+
checkRefVideoDurations,
|
|
109
113
|
VIDEO_PROVIDERS_REQUIRING_IMAGE,
|
|
110
114
|
videoProviderRequiresImage,
|
|
111
115
|
VIDEO_MODE_ALIASES,
|
|
@@ -173,6 +177,7 @@ export { FEATURED_ENTITIES, getFeaturedEntities } from "./featured-entities.js"
|
|
|
173
177
|
export type { FeaturedEntity } from "./featured-entities.js"
|
|
174
178
|
|
|
175
179
|
export type {
|
|
180
|
+
ImageAspectRatio,
|
|
176
181
|
ImageGenProvider,
|
|
177
182
|
ImageMaskMode,
|
|
178
183
|
ImageI2IProvider,
|
|
@@ -209,6 +214,7 @@ export type {
|
|
|
209
214
|
VideoAudioCapability,
|
|
210
215
|
GvpAnchorChoice,
|
|
211
216
|
GvpAnchorWireMode,
|
|
217
|
+
RefVideoDurationLimit,
|
|
212
218
|
} from "./model-constants.js"
|
|
213
219
|
|
|
214
220
|
|
|
@@ -234,13 +240,28 @@ export type {
|
|
|
234
240
|
export {
|
|
235
241
|
buildCreditModelIdentifier,
|
|
236
242
|
resolveImageGenCreditIdentifier,
|
|
243
|
+
resolveNormalizedImageGen,
|
|
237
244
|
buildVideoCreditModelIdentifier,
|
|
245
|
+
pricedVideoSelection,
|
|
238
246
|
buildMotionCreditModelIdentifier,
|
|
247
|
+
sunoCreditType,
|
|
248
|
+
SUNO_VERSION_PRICED_OPERATIONS,
|
|
249
|
+
SUNO_SELECT_OPERATIONS,
|
|
239
250
|
} from "./credit-identifiers.js"
|
|
251
|
+
export type { NormalizedImageGen, PricedVideoSelection } from "./credit-identifiers.js"
|
|
240
252
|
|
|
241
253
|
export * from "./credit-estimators/index.js"
|
|
242
254
|
export { extractVideoDurationFromNode } from "./video-duration.js"
|
|
243
255
|
|
|
256
|
+
export {
|
|
257
|
+
resolveTopazUpscale,
|
|
258
|
+
TOPAZ_UPSCALE_FACTORS,
|
|
259
|
+
TOPAZ_DEFAULT_UPSCALE_FACTOR,
|
|
260
|
+
type TopazUpscaleFactor,
|
|
261
|
+
type TopazUpscaleAdjustment,
|
|
262
|
+
type TopazUpscaleResolution,
|
|
263
|
+
} from "./topaz-upscale.js"
|
|
264
|
+
|
|
244
265
|
|
|
245
266
|
|
|
246
267
|
|
|
@@ -617,8 +638,8 @@ export type { LottieOverlayCatalogEntry } from "./lottie-overlay-catalog.js"
|
|
|
617
638
|
|
|
618
639
|
export { resolveFieldMappings, resolveLocationFields } from "./resolve-field-mappings.js"
|
|
619
640
|
|
|
620
|
-
export { resolveNodeRefs, parseNodeRef, canonicalVarName, NODE_REF_PATTERN, RESERVED_TEMPLATE_VARS, extractReferencedLabels, combineSameLabelRefs, refHandleCategory, REF_HANDLE_CATEGORY, REFERENCE_HANDLE_MAP, referenceModalityForHandle, FRAME_TARGET_HANDLES, countRefModalityEdges } from "./node-refs.js"
|
|
621
|
-
export type { RefCandidate, ReferenceModality, RefModalityEdge } from "./node-refs.js"
|
|
641
|
+
export { resolveNodeRefs, parseNodeRef, canonicalVarName, NODE_REF_PATTERN, RESERVED_TEMPLATE_VARS, extractReferencedLabels, combineSameLabelRefs, refHandleCategory, REF_HANDLE_CATEGORY, REFERENCE_HANDLE_MAP, referenceModalityForHandle, FRAME_TARGET_HANDLES, countRefModalityEdges, REF_TOKEN_NAMESPACE_PREFIXES, classifyRefToken, unresolvedRefTokens } from "./node-refs.js"
|
|
642
|
+
export type { RefCandidate, ReferenceModality, RefModalityEdge, RefTokenKind } from "./node-refs.js"
|
|
622
643
|
|
|
623
644
|
|
|
624
645
|
export { resolveSourceThroughConnectedList } from "./list-source-resolver.js"
|
|
@@ -719,9 +740,11 @@ export {
|
|
|
719
740
|
modelIdsByKindMode,
|
|
720
741
|
buildModelMenu,
|
|
721
742
|
normalizeModelInput,
|
|
743
|
+
normalizeVideoRequestParams,
|
|
722
744
|
defaultResolutionFor,
|
|
723
745
|
} from "./model-catalog.js"
|
|
724
746
|
export type {
|
|
747
|
+
NormalizedVideoRequest,
|
|
725
748
|
ModelCatalogEntry,
|
|
726
749
|
ModelKind,
|
|
727
750
|
ModelMode,
|
|
@@ -735,6 +758,11 @@ export type {
|
|
|
735
758
|
NormalizedModelInput,
|
|
736
759
|
} from "./model-catalog.js"
|
|
737
760
|
|
|
761
|
+
// Per-model safety-filter retry/fallback policy (derives from
|
|
762
|
+
// `ModelCatalogEntry.safetyFilter` above).
|
|
763
|
+
export { safetyRetryPolicy } from "./safety-retry-policy.js"
|
|
764
|
+
export type { SafetyRetryPolicy } from "./safety-retry-policy.js"
|
|
765
|
+
|
|
738
766
|
export {
|
|
739
767
|
STATIC_CAPTION_STYLES,
|
|
740
768
|
KINETIC_CAPTION_STYLES,
|