@nodaro/shared 3.6.0 → 3.8.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": "3.6.0",
3
+ "version": "3.8.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",
@@ -76,6 +76,38 @@ describe("normalizeModelInput", () => {
76
76
  expect(normalizeModelInput("gpt-image-2", { aspectRatio: "1:1", resolution: "4K" }).resolution).toBe("2K")
77
77
  })
78
78
 
79
+ // GPT Image 2.5 is NOT GPT Image 2. Its KIE docs
80
+ // (docs.kie.ai/market/gpt/gpt-image-2-5-*) state no aspect_ratio x resolution
81
+ // restriction, so the cross-field block above must NOT be widened to these
82
+ // ids "for consistency" — doing so would silently downgrade a paid 4K render
83
+ // to 1K. If GPT Image 2.5 ever documents such a limit, add it deliberately
84
+ // and change this test in the same commit.
85
+ it.each([
86
+ "gpt-image-2-5-flare",
87
+ "gpt-image-2-5-flare-i2i",
88
+ "gpt-image-2-5-sunburst",
89
+ "gpt-image-2-5-sunburst-i2i",
90
+ ])("%s has NO cross-field aspect-ratio x resolution rule", (modelId) => {
91
+ const auto4k = normalizeModelInput(modelId, { aspectRatio: "auto", resolution: "4K" })
92
+ expect(auto4k.resolution).toBe("4K")
93
+ expect(auto4k.adjustments).toEqual([])
94
+
95
+ const square4k = normalizeModelInput(modelId, { aspectRatio: "1:1", resolution: "4K" })
96
+ expect(square4k.resolution).toBe("4K")
97
+ expect(square4k.adjustments).toEqual([])
98
+ })
99
+
100
+ // The four ratios 2.5 introduced are new to the platform vocabulary; the snap
101
+ // must accept them rather than rewrite them to a neighbour.
102
+ it.each(["27:16", "16:27", "9:8", "8:9", "21:9", "3:2"])(
103
+ "gpt-image-2.5 keeps the newly-added ratio %s",
104
+ (ratio) => {
105
+ const out = normalizeModelInput("gpt-image-2-5-flare", { aspectRatio: ratio })
106
+ expect(out.aspectRatio).toBe(ratio)
107
+ expect(out.adjustments).toEqual([])
108
+ },
109
+ )
110
+
79
111
  it("passes unknown model ids through untouched (the Zod enum owns those)", () => {
80
112
  const out = normalizeModelInput("totally-fake-model", { aspectRatio: "21:9" })
81
113
  expect(out.aspectRatio).toBe("21:9")
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ PRO3D_RENDER_QUALITY_PROFILES,
4
+ RENDER_VIDEO_CREDIT_ID,
5
+ SCENE3D_LIMITS,
6
+ SCENE3D_RENDER_BASE_MAX_PX,
7
+ SCENE3D_RENDER_TIERS,
8
+ SCENE3D_RENDER_TIER_MULTIPLIERS,
9
+ SCENE3D_RENDER_XLARGE_MIN_AREA_PX,
10
+ pro3DRenderFrameUnit,
11
+ renderVideoCreditId,
12
+ scene3DRenderTier,
13
+ scene3DRenderTierCredits,
14
+ } from "../index.js"
15
+
16
+ /** The three reference frames the tiers are anchored to (PR #1328). */
17
+ const REFERENCE_FRAMES = {
18
+ base: { width: 1920, height: 1080 },
19
+ large: { width: 2560, height: 1440 },
20
+ xlarge: { width: 2560, height: 2560 },
21
+ } as const
22
+
23
+ describe("scene3DRenderTier", () => {
24
+ it("places each reference frame in its own tier", () => {
25
+ for (const [tier, frame] of Object.entries(REFERENCE_FRAMES)) {
26
+ expect(scene3DRenderTier(frame.width, frame.height)).toBe(tier)
27
+ }
28
+ })
29
+
30
+ it("holds every pre-cap frame at the base price, square ones included", () => {
31
+ // The promise of the 1920 gate: raising maxDimensionPx never reprices a
32
+ // frame that was already renderable. 1920x1920 has the SAME pixel count as
33
+ // 2560x1440 and is still base — area only decides ABOVE the gate.
34
+ expect(scene3DRenderTier(1920, 1080)).toBe("base")
35
+ expect(scene3DRenderTier(1080, 1920)).toBe("base")
36
+ expect(scene3DRenderTier(1920, 1920)).toBe("base")
37
+ expect(scene3DRenderTier(1680, 720)).toBe("base")
38
+ expect(SCENE3D_RENDER_BASE_MAX_PX).toBe(1920)
39
+ })
40
+
41
+ it("tiers every aspect the 2560 cap makes reachable", () => {
42
+ expect(scene3DRenderTier(2560, 1440)).toBe("large") // 16:9
43
+ expect(scene3DRenderTier(1440, 2560)).toBe("large") // 9:16
44
+ expect(scene3DRenderTier(2560, 1097)).toBe("large") // 21:9
45
+ expect(scene3DRenderTier(2048, 2560)).toBe("xlarge") // 4:5
46
+ expect(scene3DRenderTier(2560, 2560)).toBe("xlarge") // 1:1
47
+ })
48
+
49
+ it("splits large from xlarge at the midpoint of the two upper reference frames", () => {
50
+ const midpoint = (REFERENCE_FRAMES.large.width * REFERENCE_FRAMES.large.height + REFERENCE_FRAMES.xlarge.width * REFERENCE_FRAMES.xlarge.height) / 2
51
+ expect(SCENE3D_RENDER_XLARGE_MIN_AREA_PX).toBe(midpoint)
52
+ // One pixel either side of the boundary, at a width past the gate.
53
+ expect(scene3DRenderTier(2560, 2000)).toBe("large") // 5,120,000 exactly
54
+ expect(scene3DRenderTier(2560, 2001)).toBe("xlarge") // 5,122,560
55
+ })
56
+
57
+ it("is base for anything that is not a pair of positive finite numbers", () => {
58
+ expect(scene3DRenderTier(undefined, undefined)).toBe("base")
59
+ expect(scene3DRenderTier("2560", "2560")).toBe("base")
60
+ expect(scene3DRenderTier(Number.NaN, 2560)).toBe("base")
61
+ expect(scene3DRenderTier(Infinity, 2560)).toBe("base")
62
+ expect(scene3DRenderTier(0, 2560)).toBe("base")
63
+ expect(scene3DRenderTier(-2560, -2560)).toBe("base")
64
+ })
65
+
66
+ it("covers the whole admissible range with a declared tier", () => {
67
+ const max = SCENE3D_LIMITS.maxDimensionPx
68
+ for (const w of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
69
+ for (const h of [SCENE3D_LIMITS.minDimensionPx, 640, 1920, 1921, 2048, max]) {
70
+ expect(SCENE3D_RENDER_TIERS).toContain(scene3DRenderTier(w, h))
71
+ }
72
+ }
73
+ })
74
+ })
75
+
76
+ describe("scene3DRenderTierCredits", () => {
77
+ it("is the declared ratio, rounded up", () => {
78
+ // The worked examples the public docs print, from the built-in base of 50.
79
+ expect(scene3DRenderTierCredits(50, "base")).toBe(50)
80
+ expect(scene3DRenderTierCredits(50, "large")).toBe(75)
81
+ expect(scene3DRenderTierCredits(50, "xlarge")).toBe(125)
82
+ })
83
+
84
+ it("never rounds a tier down below the base render it multiplies", () => {
85
+ for (const base of [1, 3, 7, 15, 33, 50, 137]) {
86
+ for (const tier of SCENE3D_RENDER_TIERS) {
87
+ expect(scene3DRenderTierCredits(base, tier)).toBeGreaterThanOrEqual(base)
88
+ expect(scene3DRenderTierCredits(base, tier)).toBe(
89
+ Math.ceil(base * SCENE3D_RENDER_TIER_MULTIPLIERS[tier]),
90
+ )
91
+ }
92
+ }
93
+ })
94
+
95
+ it("keeps the ladder monotonic", () => {
96
+ expect(SCENE3D_RENDER_TIER_MULTIPLIERS.base).toBe(1)
97
+ expect(SCENE3D_RENDER_TIER_MULTIPLIERS.large).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.base)
98
+ expect(SCENE3D_RENDER_TIER_MULTIPLIERS.xlarge).toBeGreaterThan(SCENE3D_RENDER_TIER_MULTIPLIERS.large)
99
+ })
100
+ })
101
+
102
+ describe("renderVideoCreditId", () => {
103
+ it("keeps the bare identifier for a base-sized 3D scene plan", () => {
104
+ expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 1920, height: 1080 } })).toBe(
105
+ RENDER_VIDEO_CREDIT_ID,
106
+ )
107
+ })
108
+
109
+ it("names the tier for a plan past the 1920 gate", () => {
110
+ expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 1440 } })).toBe(
111
+ "render-video:3d-large",
112
+ )
113
+ expect(renderVideoCreditId({ planType: "3d-scene", plan: { width: 2560, height: 2560 } })).toBe(
114
+ "render-video:3d-xlarge",
115
+ )
116
+ })
117
+
118
+ it("leaves every non-3D render on the flat identifier", () => {
119
+ // scene-graph admits frames up to 3840 today at the flat price; tiering it
120
+ // would raise the price of work people already run.
121
+ expect(renderVideoCreditId({ planType: "scene-graph", plan: { width: 3840, height: 2160 } })).toBe(
122
+ RENDER_VIDEO_CREDIT_ID,
123
+ )
124
+ expect(renderVideoCreditId({ planType: "lottie-graphic", plan: { width: 2560, height: 2560 } })).toBe(
125
+ RENDER_VIDEO_CREDIT_ID,
126
+ )
127
+ expect(renderVideoCreditId({ template: "slideshow", aspectRatio: "16:9" })).toBe(RENDER_VIDEO_CREDIT_ID)
128
+ })
129
+
130
+ it("answers the flat identifier for anything unparseable", () => {
131
+ expect(renderVideoCreditId(undefined)).toBe(RENDER_VIDEO_CREDIT_ID)
132
+ expect(renderVideoCreditId(null)).toBe(RENDER_VIDEO_CREDIT_ID)
133
+ expect(renderVideoCreditId("3d-scene")).toBe(RENDER_VIDEO_CREDIT_ID)
134
+ expect(renderVideoCreditId({ planType: "3d-scene" })).toBe(RENDER_VIDEO_CREDIT_ID)
135
+ expect(renderVideoCreditId({ planType: "3d-scene", plan: "{}" })).toBe(RENDER_VIDEO_CREDIT_ID)
136
+ expect(renderVideoCreditId({ planType: "3d-scene", plan: {} })).toBe(RENDER_VIDEO_CREDIT_ID)
137
+ })
138
+ })
139
+
140
+ describe("pro3DRenderFrameUnit", () => {
141
+ it("keeps the configured spelling for base-sized frames", () => {
142
+ for (const quality of PRO3D_RENDER_QUALITY_PROFILES) {
143
+ expect(pro3DRenderFrameUnit(quality)).toBe(`pro-3d-render:render-frame:${quality}`)
144
+ expect(pro3DRenderFrameUnit(quality, "base")).toBe(`pro-3d-render:render-frame:${quality}`)
145
+ }
146
+ })
147
+
148
+ it("suffixes the tier for a frame past the gate", () => {
149
+ expect(pro3DRenderFrameUnit("standard", "large")).toBe("pro-3d-render:render-frame:standard:large")
150
+ expect(pro3DRenderFrameUnit("standard", "xlarge")).toBe("pro-3d-render:render-frame:standard:xlarge")
151
+ })
152
+
153
+ it("produces one distinct unit per tier", () => {
154
+ const units = new Set(SCENE3D_RENDER_TIERS.map((tier) => pro3DRenderFrameUnit("standard", tier)))
155
+ expect(units.size).toBe(SCENE3D_RENDER_TIERS.length)
156
+ })
157
+ })
@@ -108,7 +108,10 @@ describe("scene3d v2 — v1 is untouched", () => {
108
108
  expect(SCENE3D_LIMITS.maxObjects).toBe(100)
109
109
  expect(SCENE3D_LIMITS.maxKeyframes).toBe(240)
110
110
  expect(SCENE3D_LIMITS.maxHierarchyDepth).toBe(8)
111
- expect(SCENE3D_LIMITS.maxDimensionPx).toBe(1920)
111
+ // Widened from 1920 deliberately (measured, both versions together) —
112
+ // every other v1 bound above is still frozen.
113
+ expect(SCENE3D_LIMITS.maxDimensionPx).toBe(2560)
114
+ expect(SCENE3D_V2_LIMITS.maxDimensionPx).toBe(SCENE3D_LIMITS.maxDimensionPx)
112
115
  expect(SCENE3D_LIMITS.maxReferences).toBe(8)
113
116
  })
114
117
 
@@ -16,6 +16,16 @@
16
16
  */
17
17
  import { z } from "zod"
18
18
 
19
+ /** The nine points a layer attaches to on the base image. THE definition —
20
+ * the route's Zod, the compositor, the canvas, the SDK and the CLI all read
21
+ * this one (it used to be spelled out separately in each). */
22
+ export const OVERLAY_ANCHORS = [
23
+ "top-left", "top", "top-right",
24
+ "left", "center", "right",
25
+ "bottom-left", "bottom", "bottom-right",
26
+ ] as const
27
+ export type OverlayAnchor = (typeof OVERLAY_ANCHORS)[number]
28
+
19
29
  export const OVERLAY_LAYER_KINDS = ["image", "text", "qr", "shape"] as const
20
30
  export type OverlayLayerKind = (typeof OVERLAY_LAYER_KINDS)[number]
21
31
 
package/src/index.ts CHANGED
@@ -1066,6 +1066,9 @@ export * from "./scene3d-v2-resources.js"
1066
1066
  export * from "./scene3d-camera-track.js"
1067
1067
  // --- 3D Render Pro: one durable operation, scene + video in one result ---
1068
1068
  export * from "./pro-3d-render.js"
1069
+ // --- Scene3D render pricing: which frame-size tier a render settles at.
1070
+ // The SHAPE of the price (tier, multiplier, identifier); never a rate. ---
1071
+ export * from "./scene3d-render-pricing.js"
1069
1072
 
1070
1073
  // --- transient studio keys — the public share read strips them ---
1071
1074
  export {
@@ -233,6 +233,15 @@ const KONTEXT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"] as const
233
233
  const GROK_RATIOS = ["1:1", "16:9", "9:16", "3:2", "2:3"] as const
234
234
  const GPT_IMAGE_RATIOS = ["1:1", "3:2", "2:3"] as const
235
235
  const GPT_IMAGE_2_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4"] as const
236
+ // GPT Image 2.5 (Flare + Sunburst) widen the set to thirteen — the GPT Image 2
237
+ // six plus 3:2/2:3, ultra-wide 21:9/27:16, ultra-tall 16:27 and near-square
238
+ // 9:8/8:9 (docs.kie.ai/market/gpt/gpt-image-2-5-*). Unlike GPT Image 2, the 2.5
239
+ // docs state NO aspect-ratio x resolution restriction, so no cross-field
240
+ // constraint is registered for these ids in normalizeModelInput below.
241
+ const GPT_IMAGE_2_5_RATIOS = [
242
+ "auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16",
243
+ "21:9", "27:16", "16:27", "9:8", "8:9",
244
+ ] as const
236
245
  const IMAGEN4_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
237
246
  const IDEOGRAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
238
247
  const SEEDREAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"] as const
@@ -591,6 +600,103 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
591
600
  ],
592
601
  },
593
602
 
603
+ "gpt-image-2-5-flare": {
604
+ id: "gpt-image-2-5-flare",
605
+ kind: "image",
606
+ modes: ["t2i"] as const,
607
+ family: "OpenAI",
608
+ label: "GPT Image 2.5 Flare",
609
+ series: "GPT Image",
610
+ description: "Fast everyday GPT Image 2.5 - higher quality than GPT Image 2 at about half the latency. The default of the pair: social and creator content, campaign variants, thumbnails, rapid iteration, high-volume work.",
611
+ useCases: ["typography", "high-res", "general", "draft"],
612
+ features: ["reference-image"],
613
+ aspectRatios: GPT_IMAGE_2_5_RATIOS,
614
+ resolutions: ["1K", "2K", "4K"],
615
+ pricing: [
616
+ { identifier: "gpt-image-2-5-flare", credits: 15, note: "1K default" },
617
+ { identifier: "gpt-image-2-5-flare:2K", credits: 25, note: "2K" },
618
+ { identifier: "gpt-image-2-5-flare:4K", credits: 40, note: "4K" },
619
+ ],
620
+ valueLabels: {
621
+ "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
622
+ "27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
623
+ "9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
624
+ },
625
+ safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
626
+ },
627
+ "gpt-image-2-5-flare-i2i": {
628
+ id: "gpt-image-2-5-flare-i2i",
629
+ kind: "image",
630
+ modes: ["i2i"] as const,
631
+ family: "OpenAI",
632
+ label: "GPT Image 2.5 Flare (I2I)",
633
+ series: "GPT Image",
634
+ description: "Fast GPT Image 2.5 edits (up to 16 source images) - the default when you are iterating rather than finishing.",
635
+ useCases: ["edit", "high-res"],
636
+ features: ["reference-image"],
637
+ aspectRatios: GPT_IMAGE_2_5_RATIOS,
638
+ resolutions: ["1K", "2K", "4K"],
639
+ pricing: [
640
+ { identifier: "gpt-image-2-5-flare-i2i", credits: 15, note: "1K default" },
641
+ { identifier: "gpt-image-2-5-flare-i2i:2K", credits: 25, note: "2K" },
642
+ { identifier: "gpt-image-2-5-flare-i2i:4K", credits: 40, note: "4K" },
643
+ ],
644
+ valueLabels: {
645
+ "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
646
+ "27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
647
+ "9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
648
+ },
649
+ safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
650
+ },
651
+ "gpt-image-2-5-sunburst": {
652
+ id: "gpt-image-2-5-sunburst",
653
+ kind: "image",
654
+ modes: ["t2i"] as const,
655
+ family: "OpenAI",
656
+ label: "GPT Image 2.5 Sunburst",
657
+ series: "GPT Image",
658
+ description: "Precision GPT Image 2.5 - trades generation time for tighter control and detail fidelity. Pick it for brand-sensitive and production work: packaging, diagrams, ecommerce retouching, polished campaign creative.",
659
+ useCases: ["typography", "high-res", "general"],
660
+ features: ["reference-image"],
661
+ aspectRatios: GPT_IMAGE_2_5_RATIOS,
662
+ resolutions: ["1K", "2K", "4K"],
663
+ pricing: [
664
+ { identifier: "gpt-image-2-5-sunburst", credits: 15, note: "1K default" },
665
+ { identifier: "gpt-image-2-5-sunburst:2K", credits: 25, note: "2K" },
666
+ { identifier: "gpt-image-2-5-sunburst:4K", credits: 40, note: "4K" },
667
+ ],
668
+ valueLabels: {
669
+ "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
670
+ "27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
671
+ "9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
672
+ },
673
+ safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
674
+ },
675
+ "gpt-image-2-5-sunburst-i2i": {
676
+ id: "gpt-image-2-5-sunburst-i2i",
677
+ kind: "image",
678
+ modes: ["i2i"] as const,
679
+ family: "OpenAI",
680
+ label: "GPT Image 2.5 Sunburst (I2I)",
681
+ series: "GPT Image",
682
+ description: "Precision GPT Image 2.5 edits (up to 16 source images) - the most controlled edit in the GPT family, at the cost of a longer run.",
683
+ useCases: ["edit", "high-res"],
684
+ features: ["reference-image"],
685
+ aspectRatios: GPT_IMAGE_2_5_RATIOS,
686
+ resolutions: ["1K", "2K", "4K"],
687
+ pricing: [
688
+ { identifier: "gpt-image-2-5-sunburst-i2i", credits: 15, note: "1K default" },
689
+ { identifier: "gpt-image-2-5-sunburst-i2i:2K", credits: 25, note: "2K" },
690
+ { identifier: "gpt-image-2-5-sunburst-i2i:4K", credits: 40, note: "4K" },
691
+ ],
692
+ valueLabels: {
693
+ "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)",
694
+ "27:16": "27:16 (Ultra-wide)", "16:27": "16:27 (Ultra-tall)",
695
+ "9:8": "9:8 (Near-square)", "8:9": "8:9 (Near-square)",
696
+ },
697
+ safetyFilter: { stochastic: true, fallback: "nano-banana-pro" },
698
+ },
699
+
594
700
  // ── Ideogram ──
595
701
  // Ideogram models use a `rendering_speed` (TURBO / BALANCED / QUALITY)
596
702
  // dimension that's distinct from the `quality` lever — frontend exposes
@@ -44,6 +44,9 @@ export const IMAGE_ASPECT_RATIO_VALUES = [
44
44
  // Ultra-wide / ultra-tall banner ratios: Wan 2.7 + Wan 2.7 Pro (8:1, 1:8)
45
45
  // and Nano Banana 2 Lite (4:1, 1:4, 8:1, 1:8).
46
46
  "4:1", "1:4", "8:1", "1:8",
47
+ // GPT Image 2.5 (Flare + Sunburst) additions: near-square and ultra-wide/tall
48
+ // cinema ratios (docs.kie.ai/market/gpt/gpt-image-2-5-*).
49
+ "27:16", "16:27", "9:8", "8:9",
47
50
  ] as const
48
51
 
49
52
  /** A ratio string the image routes accept (pre-snap vocabulary, not a per-model guarantee). */
@@ -76,6 +79,11 @@ export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
76
79
  "nano-banana-2-lite": 20000, // docs.kie.ai/market/google/nano-banana-2-lite
77
80
  "nano-banana-pro": 20000, // docs.kie.ai/market/google/pro-image-to-image
78
81
  "gpt-image-2-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-image-to-image
82
+ // GPT Image 2.5 — all four lanes document a 20000-char prompt ceiling.
83
+ "gpt-image-2-5-flare": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-flare-text-to-image
84
+ "gpt-image-2-5-flare-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-flare-image-to-image
85
+ "gpt-image-2-5-sunburst": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-sunburst-text-to-image
86
+ "gpt-image-2-5-sunburst-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-5-sunburst-image-to-image
79
87
  // ── lower than the 5000 default (over-send risk if left at default) ──
80
88
  "seedream": 3000, // docs.kie.ai/market/seedream/4-5-text-to-image
81
89
  "seedream-edit": 3000, // docs.kie.ai/market/seedream/4-5-edit
@@ -475,6 +483,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
475
483
  // T2I providers that auto-route to their i2i sibling when refs are attached
476
484
  "gpt-image",
477
485
  "gpt-image-2",
486
+ "gpt-image-2-5-flare",
487
+ "gpt-image-2-5-sunburst",
478
488
  "grok",
479
489
  "grok-2",
480
490
  "qwen",
@@ -487,6 +497,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
487
497
  "nano-banana-edit",
488
498
  "gpt-image-i2i",
489
499
  "gpt-image-2-i2i",
500
+ "gpt-image-2-5-flare-i2i",
501
+ "gpt-image-2-5-sunburst-i2i",
490
502
  "grok-2-i2i",
491
503
  "flux-i2i",
492
504
  "flux-pro-i2i",
@@ -525,6 +537,8 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
525
537
  export const T2I_TO_I2I_VARIANT: Record<string, string> = {
526
538
  "gpt-image": "gpt-image-i2i",
527
539
  "gpt-image-2": "gpt-image-2-i2i",
540
+ "gpt-image-2-5-flare": "gpt-image-2-5-flare-i2i",
541
+ "gpt-image-2-5-sunburst": "gpt-image-2-5-sunburst-i2i",
528
542
  "grok": "grok-i2i",
529
543
  // grok-2's t2i takes NO image input; its "i2i" is the segment-map(image_url)
530
544
  // → image-edit(task_id) chain in the KIE provider (single reference).
@@ -560,6 +574,8 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
560
574
  "nano-banana-edit": 8,
561
575
  "gpt-image-i2i": 16,
562
576
  "gpt-image-2-i2i": 16,
577
+ "gpt-image-2-5-flare-i2i": 16,
578
+ "gpt-image-2-5-sunburst-i2i": 16,
563
579
  "flux-i2i": 4,
564
580
  "flux-pro-i2i": 4,
565
581
  "seedream-edit": 16,
@@ -625,6 +641,10 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
625
641
  "gpt-image-i2i": "quality",
626
642
  "gpt-image-2": "resolution",
627
643
  "gpt-image-2-i2i": "resolution",
644
+ "gpt-image-2-5-flare": "resolution",
645
+ "gpt-image-2-5-flare-i2i": "resolution",
646
+ "gpt-image-2-5-sunburst": "resolution",
647
+ "gpt-image-2-5-sunburst-i2i": "resolution",
628
648
  "nano-banana-pro": "resolution",
629
649
  "nano-banana-2": "resolution",
630
650
  "flux": "resolution",
@@ -658,6 +678,10 @@ export const RESOLUTION_2K_4K_TIERED_PROVIDERS = new Set([
658
678
  "nano-banana-2",
659
679
  "gpt-image-2",
660
680
  "gpt-image-2-i2i",
681
+ "gpt-image-2-5-flare",
682
+ "gpt-image-2-5-flare-i2i",
683
+ "gpt-image-2-5-sunburst",
684
+ "gpt-image-2-5-sunburst-i2i",
661
685
  "wan-2.7",
662
686
  "wan-2.7-pro",
663
687
  ])
@@ -680,6 +704,8 @@ export const IMAGE_GEN_PROVIDERS = [
680
704
  "grok-2",
681
705
  "gpt-image",
682
706
  "gpt-image-2",
707
+ "gpt-image-2-5-flare",
708
+ "gpt-image-2-5-sunburst",
683
709
  "imagen4",
684
710
  "imagen4-fast",
685
711
  "imagen4-ultra",
@@ -711,6 +737,8 @@ export const IMAGE_I2I_PROVIDERS = [
711
737
  "flux-pro-i2i",
712
738
  "gpt-image-i2i",
713
739
  "gpt-image-2-i2i",
740
+ "gpt-image-2-5-flare-i2i",
741
+ "gpt-image-2-5-sunburst-i2i",
714
742
  "grok-2-i2i",
715
743
  "ideogram-edit",
716
744
  "ideogram-remix",
@@ -1250,6 +1278,8 @@ export const IMAGE_MASK_MODE: Record<ImageGenProvider, ImageMaskMode> = {
1250
1278
  "nano-banana-2-lite": "prompt",
1251
1279
  "gpt-image": "prompt",
1252
1280
  "gpt-image-2": "prompt",
1281
+ "gpt-image-2-5-flare": "prompt",
1282
+ "gpt-image-2-5-sunburst": "prompt",
1253
1283
  "seedream": "prompt",
1254
1284
  "seedream-5-lite": "prompt",
1255
1285
  "seedream-5-pro": "prompt",
@@ -167,6 +167,10 @@ const QUALITY_MAP: Record<string, QualityMapping> = {
167
167
  "flux-pro-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "2K" } },
168
168
  "gpt-image-2": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
169
169
  "gpt-image-2-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
170
+ "gpt-image-2-5-flare": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
171
+ "gpt-image-2-5-flare-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
172
+ "gpt-image-2-5-sunburst": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
173
+ "gpt-image-2-5-sunburst-i2i": { field: "resolution", values: { low: "1K", mid: "2K", high: "4K" } },
170
174
  // Image gen — quality-style (medium/high or basic/high)
171
175
  "gpt-image": { field: "quality", values: { low: "medium", mid: "medium", high: "high" } },
172
176
  "gpt-image-i2i": { field: "quality", values: { low: "medium", mid: "medium", high: "high" } },
@@ -54,6 +54,8 @@ export const PIPELINE_PINNABLE_IMAGE_MODELS = [
54
54
  "flux",
55
55
  "gpt-image",
56
56
  "gpt-image-2",
57
+ "gpt-image-2-5-flare",
58
+ "gpt-image-2-5-sunburst",
57
59
  ] as const
58
60
  export type PipelinePinnableImageModel = (typeof PIPELINE_PINNABLE_IMAGE_MODELS)[number]
59
61
 
@@ -271,6 +271,23 @@ export interface Pro3DRenderValidationWarning {
271
271
  shotId?: string
272
272
  }
273
273
 
274
+ /**
275
+ * One still per shot of the exported composition.
276
+ *
277
+ * A render's contact sheet: the frame a shot OPENS on, which is the frame that
278
+ * says what the shot is of. `shotIndex` is the 0-based position in the v2
279
+ * composition's `shots` array — a v1 (single-shot) scene has exactly one still,
280
+ * index 0 at frame 0 — and `frame` is that shot's own first frame in the
281
+ * composition's frame space, so a caller can line a still up against the MP4
282
+ * without re-deriving shot boundaries.
283
+ */
284
+ export interface Pro3DRenderShotStill {
285
+ shotIndex: number
286
+ frame: number
287
+ assetId: string
288
+ url: string
289
+ }
290
+
274
291
  export interface Pro3DRenderResultMetadata {
275
292
  width: number
276
293
  height: number
@@ -299,6 +316,12 @@ export interface Pro3DRenderJobOutput {
299
316
  scenePlan: Scene3DPlan
300
317
  sceneRevisionId: string
301
318
  posterAssetId: string
319
+ /**
320
+ * One still per shot, ordered by `shotIndex`. Optional and additive: a
321
+ * runtime that does not render stills yet returns a complete result without
322
+ * them, and a result that HAS them has one for every shot.
323
+ */
324
+ shotStills?: Pro3DRenderShotStill[]
302
325
  /** Present when an editable native source was retained for this revision. */
303
326
  sourceArtifactId?: string
304
327
  validation: {
@@ -324,12 +347,22 @@ export interface Pro3DRenderJobOutput {
324
347
  * them to satisfy the schema; a runtime that has not produced them yet simply
325
348
  * does not parse as complete, which is the honest answer.
326
349
  */
350
+ export const pro3DRenderShotStillSchema = z
351
+ .object({
352
+ shotIndex: z.number().int().min(0),
353
+ frame: z.number().int().min(0),
354
+ assetId: z.string().min(1),
355
+ url: z.string().min(1),
356
+ })
357
+ .passthrough()
358
+
327
359
  export const pro3DRenderJobOutputSchema = z
328
360
  .object({
329
361
  videoUrl: z.string().min(1),
330
362
  scenePlan: scene3DAnyPlanSchema,
331
363
  sceneRevisionId: z.string().min(1),
332
364
  posterAssetId: z.string().min(1),
365
+ shotStills: z.array(pro3DRenderShotStillSchema).optional(),
333
366
  sourceArtifactId: z.string().min(1).optional(),
334
367
  validation: z
335
368
  .object({
@@ -364,6 +397,28 @@ export function isPro3DRenderJobOutput(value: unknown): value is Pro3DRenderJobO
364
397
  return pro3DRenderJobOutputSchema.safeParse(value).success
365
398
  }
366
399
 
400
+ /**
401
+ * The stills on a result, in shot order — the ONE reader every surface uses.
402
+ *
403
+ * Tolerant on purpose: this runs on canvas node data, on a job row read back
404
+ * from the database and on an MCP result envelope, and in every one of those a
405
+ * missing or malformed list means "this result has no stills", never "refuse
406
+ * the whole result". Sorting here is what lets `shotStills[i]` mean shot `i`
407
+ * at every call site without each one remembering to sort.
408
+ */
409
+ export function pro3DRenderShotStills(output: unknown): Pro3DRenderShotStill[] {
410
+ const list = (output as { shotStills?: unknown } | null | undefined)?.shotStills
411
+ if (!Array.isArray(list)) return []
412
+ const parsed = list.flatMap((entry) => {
413
+ const result = pro3DRenderShotStillSchema.safeParse(entry)
414
+ return result.success
415
+ ? [{ shotIndex: result.data.shotIndex, frame: result.data.frame,
416
+ assetId: result.data.assetId, url: result.data.url }]
417
+ : []
418
+ })
419
+ return parsed.sort((a, b) => a.shotIndex - b.shotIndex)
420
+ }
421
+
367
422
  /**
368
423
  * The two fields every EXECUTION SURFACE must be able to resolve, whatever
369
424
  * else a runtime does or does not attach yet.
@@ -1,6 +1,11 @@
1
1
  /** Gen providers for the board: strict subset of IMAGE_GEN_PROVIDERS that
2
2
  * renders legible in-image text + accepts reference conditioning. */
3
- export const REFERENCE_BOARD_PROVIDERS = ["nano-banana-pro", "gpt-image-2"] as const
3
+ export const REFERENCE_BOARD_PROVIDERS = [
4
+ "nano-banana-pro",
5
+ "gpt-image-2",
6
+ "gpt-image-2-5-flare",
7
+ "gpt-image-2-5-sunburst",
8
+ ] as const
4
9
  export type ReferenceBoardProvider = (typeof REFERENCE_BOARD_PROVIDERS)[number]
5
10
 
6
11
  export type BoardEntityKind = "character" | "location" | "object"