@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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) AND Gemini (response_format) vendors", () => {
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 { VIDEO_ANALYSIS_LLM_MODELS } from "../llm-models.js"
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 moved to backend/src/lib/pricing/video-analysis-cost.ts (S5) — its
12
- // tests (including the worked-example bucket-credit values and the
13
- // cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS below) live in
14
- // backend/src/lib/pricing/__tests__/video-analysis-cost.test.ts. This file
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 ambient (sound toggle, cost-affecting) for Kling", () => {
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("ambient")
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 Kling AUDIO_ADDON provider is ambient + affectsCost", () => {
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("ambient")
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
- if (AUDIO_ADDON_PROVIDERS.has(effectiveProvider) && sound) {
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" direct Anthropic SDK forced tool_choice (guaranteed).
41
- * - "kie-response-format" → KIE chat-completions `response_format: json_schema`
42
- * (verified enforced for Gemini via KIE).
43
- * - undefined no native structured mode (GPT-via-KIE doesn't honor
44
- * response_format); callers fall back to parse + retry.
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) + Gemini (KIE `response_format`). GPT-via-KIE has no native structured
242
- * mode (parse+retry only), so it's excluded. Single source of truth so the
243
- * picker, the config panel, and the route gate can't drift. */
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
+ }
@@ -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-lite-i2i"], note: "Flux Kontext preserves identity; Ideogram Remix is character-aware; Seedream 5 Lite for instruction-based edits." },
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: "Google",
1758
- label: "Video Analysis (Gemini 3 Flash)",
1759
- series: "Gemini",
1760
- description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast Gemini tier. Billed per duration bucket.",
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: "Google",
1775
- label: "Video Analysis (Gemini 3.1 Pro)",
1776
- series: "Gemini",
1777
- description: "Analyze a video into a structured shot list (scenes, camera, audio) — high-fidelity Gemini tier. Billed per duration bucket.",
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)" },