@nodaro/shared 2.8.0 → 2.10.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": "2.8.0",
3
+ "version": "2.10.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",
@@ -392,10 +392,11 @@ describe("LLM_FEATURE_DEFAULTS", () => {
392
392
  "translate",
393
393
  "image-critic",
394
394
  "pick-best-llm",
395
+ "workflow-copilot",
395
396
  ]
396
397
 
397
- it("has entries for all 16 features", () => {
398
- expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(16)
398
+ it("has entries for all 17 features", () => {
399
+ expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(17)
399
400
  for (const feature of ALL_FEATURES) {
400
401
  expect(LLM_FEATURE_DEFAULTS).toHaveProperty(feature)
401
402
  }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ ACCESS_LEVELS,
4
+ COLLABORATOR_ROLES,
5
+ MEMBER_STATUSES,
6
+ ORG_ERROR_CODES,
7
+ ORG_KINDS,
8
+ ORG_ROLES,
9
+ OrgSettingsSchema,
10
+ PRESET_SETTING_KEYS,
11
+ PresetSettingsSchema,
12
+ WORKSPACE_ROLES,
13
+ WorkspaceSettingsSchema,
14
+ } from "../organizations/index.js"
15
+
16
+ describe("organizations wire contract — enums", () => {
17
+ it("every enum is non-empty and duplicate-free", () => {
18
+ for (const list of [ORG_KINDS, ORG_ROLES, WORKSPACE_ROLES, MEMBER_STATUSES, COLLABORATOR_ROLES, ACCESS_LEVELS, ORG_ERROR_CODES]) {
19
+ expect(list.length).toBeGreaterThan(0)
20
+ expect(new Set(list).size).toBe(list.length)
21
+ }
22
+ })
23
+
24
+ it("error codes are snake_case identifiers", () => {
25
+ for (const code of ORG_ERROR_CODES) expect(code).toMatch(/^[a-z][a-z_]*[a-z]$/)
26
+ })
27
+ })
28
+
29
+ describe("organizations wire contract — settings request schemas", () => {
30
+ it("the preset shape has exactly the nine resolvable keys", () => {
31
+ expect([...PRESET_SETTING_KEYS].sort()).toEqual(
32
+ [
33
+ "admin_access",
34
+ "collaborators_can_invite",
35
+ "default_workflow_visibility",
36
+ "member_access_to_shared",
37
+ "member_caps_enabled",
38
+ "members_can_create_projects",
39
+ "personal_space_enabled",
40
+ "policy_survives_suspension",
41
+ "workspace_admins_can_invite",
42
+ ].sort(),
43
+ )
44
+ expect(PresetSettingsSchema.safeParse({}).success).toBe(false)
45
+ })
46
+
47
+ it("workspace settings are a partial of the preset; unknown keys are stripped; bad values rejected", () => {
48
+ expect(WorkspaceSettingsSchema.parse({ admin_access: "edit", rogue: 1 })).toEqual({ admin_access: "edit" })
49
+ expect(WorkspaceSettingsSchema.safeParse({}).success).toBe(true)
50
+ expect(WorkspaceSettingsSchema.safeParse({ admin_access: "owner" }).success).toBe(false)
51
+ expect(WorkspaceSettingsSchema.safeParse({ member_caps_enabled: "yes" }).success).toBe(false)
52
+ })
53
+
54
+ it("org settings accept lower-case domains only and string vocabulary overrides", () => {
55
+ expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["school.edu", "sub.school.ac.il"] }).success).toBe(true)
56
+ expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["School.edu"] }).success).toBe(false)
57
+ expect(OrgSettingsSchema.safeParse({ allowed_email_domains: ["not a domain"] }).success).toBe(false)
58
+ expect(OrgSettingsSchema.safeParse({ vocabulary_overrides: { workspace: "Cohort" } }).success).toBe(true)
59
+ expect(OrgSettingsSchema.safeParse({ vocabulary_overrides: { workspace: 3 } }).success).toBe(false)
60
+ })
61
+ })
@@ -23,6 +23,17 @@ describe("producer-types", () => {
23
23
  DYNAMIC_PRODUCER_TYPES,
24
24
  } as const
25
25
 
26
+ // still-to-video (still image + audio → MP4 via local FFmpeg) has a video
27
+ // OUTPUT handle — absent from this set, every downstream video input would
28
+ // hard-reject its edges (the "cannot connect the outputs" bug class).
29
+ it("registers still-to-video as a video producer", () => {
30
+ expect(VIDEO_PRODUCER_TYPES.has("still-to-video")).toBe(true)
31
+ })
32
+
33
+ it("registers slideshow as a video producer (still-to-video's N-image companion)", () => {
34
+ expect(VIDEO_PRODUCER_TYPES.has("slideshow")).toBe(true)
35
+ })
36
+
26
37
  it("registers voice-changer-pro as an audio producer (its default output)", () => {
27
38
  expect(AUDIO_PRODUCER_TYPES.has("voice-changer-pro")).toBe(true)
28
39
  })
@@ -102,9 +102,13 @@ describe("per-model prompt length limits", () => {
102
102
  })
103
103
 
104
104
  describe("Suno per-version caps", () => {
105
- it("prompt: 500 non-custom; 3000 V4 custom; 5000 V4.5+/V5 custom", () => {
106
- expect(getMaxSunoPromptChars("V4", false)).toBe(500)
107
- expect(getMaxSunoPromptChars("V5", false)).toBe(500)
105
+ // Non-custom was 500 until 2026-08-19 six times under the provider's
106
+ // documented 3000, and the route truncates rather than rejects, so a
107
+ // 950-char recast score brief reached Suno cut to exactly 500 mid-word.
108
+ it("prompt: 3000 non-custom (all models); 3000 V4 custom; 5000 V4.5+/V5 custom", () => {
109
+ expect(getMaxSunoPromptChars("V4", false)).toBe(3000)
110
+ expect(getMaxSunoPromptChars("V5", false)).toBe(3000)
111
+ expect(getMaxSunoPromptChars("V5_5", false)).toBe(3000)
108
112
  expect(getMaxSunoPromptChars("V4", true)).toBe(3000)
109
113
  expect(getMaxSunoPromptChars("V4_5", true)).toBe(5000)
110
114
  expect(getMaxSunoPromptChars("V5", true)).toBe(5000)
@@ -11,9 +11,8 @@ import {
11
11
  * job dbf95612 — the original 1.0s tails made every generate-video-pro
12
12
  * continuation segment fail deterministically). Everything this platform
13
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. */
14
+ * seconds; do NOT "optimize" it back below the floor. The plugin repo
15
+ * carries twin constants guarded by its own tests — keep the two in sync. */
17
16
  describe("seedance-2 continuation-reference floor (do not regress)", () => {
18
17
  it("the platform's continuation-ref length clears KIE's r2v minimum", () => {
19
18
  expect(SEEDANCE_2_CONTINUATION_REF_SEC).toBeGreaterThanOrEqual(SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC)
@@ -157,7 +157,7 @@ describe("bare video-analysis node-type credit id", () => {
157
157
  // 24 and the schedule regenerated ~47% lower at the ceiling bucket, and 300
158
158
  // wrote 2064: the V1 hybrid-smart reprice (task A3) moved `smart` to a
159
159
  // multi-roll plan (native skeleton + donor rolls, always refined) and
160
- // trued up every tier's judge/refine terms from staging measurement).
160
+ // trued up every tier's judge/refine terms).
161
161
  expect(ceiling).toBe(2064)
162
162
  })
163
163
 
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  // The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
17
17
  // formula are PRIVATE, in @nodaroai/cloud-plugins
18
- // (src/plugins/video-analysis/cost.ts) — its tests (the worked-example
18
+ // (in the plugin repo) — its tests (the worked-example
19
19
  // bucket-credit values and the cross-check against VIDEO_ANALYSIS_BUCKET_CREDITS
20
20
  // below) live in that private package's __tests__/cost.test.ts. This file
21
21
  // covers only the NON-monetary duration-bucketing, window-batching, and
@@ -121,7 +121,7 @@ describe("video-analysis-pricing", () => {
121
121
  })
122
122
 
123
123
  // Full drift-detection against the live $-formula lives in the PRIVATE
124
- // plugin repo (src/plugins/video-analysis/__tests__/cost.test.ts) — the
124
+ // plugin repo — the
125
125
  // formula moved there in 2026-07 and the app-side test was deleted with it,
126
126
  // so nothing in THIS repo can recompute these numbers. This is a lightweight
127
127
  // shape check that the precomputed table covers every legal id.
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import {
3
+ VIDEO_MODE_ALIASES,
4
+ VIDEO_REF_LIMITS_BY_PROVIDER,
5
+ resolveVideoModeForInputs,
6
+ resolveVideoProviderForMode,
7
+ } from "../model-constants.js"
8
+
9
+ /**
10
+ * #861 — reference images wired to Grok Imagine 1 without a start frame were
11
+ * silently dropped: the run resolved to text-to-video, the t2v twin (`grok`)
12
+ * has no image parameter, and nothing said so. The mode must follow what the
13
+ * model can CARRY, derived from the catalog's reference caps — not a name.
14
+ */
15
+ const I2V = "image-to-video"
16
+ const T2V = "text-to-video"
17
+
18
+ describe("resolveVideoModeForInputs", () => {
19
+ it("a start frame is image-to-video for every provider", () => {
20
+ for (const id of ["grok-i2v", "grok", "seedance-2", "kling", "veo3", "wan-i2v", undefined]) {
21
+ expect(resolveVideoModeForInputs(id, { hasStartFrame: true, hasImageRefs: false })).toBe(I2V)
22
+ expect(resolveVideoModeForInputs(id, { hasStartFrame: true, hasImageRefs: true })).toBe(I2V)
23
+ }
24
+ })
25
+
26
+ it("nothing wired is text-to-video", () => {
27
+ expect(resolveVideoModeForInputs("grok-i2v", { hasStartFrame: false, hasImageRefs: false })).toBe(T2V)
28
+ expect(resolveVideoModeForInputs(undefined, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
29
+ })
30
+
31
+ it("refs alone route Grok Imagine 1 to its i2v twin — the only one that can carry them (#861)", () => {
32
+ // Whichever member id the node stored (base/i2v/t2v all appear in old workflows).
33
+ for (const id of ["grok-i2v", "grok"]) {
34
+ expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(I2V)
35
+ }
36
+ // The premise, pinned: i2v carries refs, t2v does not.
37
+ expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok-i2v"]?.images).toBeGreaterThan(0)
38
+ expect(VIDEO_REF_LIMITS_BY_PROVIDER["grok"]).toBeUndefined()
39
+ })
40
+
41
+ it("single-id models are untouched: the same id serves both modes, so there is nothing to re-route", () => {
42
+ // seedance-2 / gemini-omni-video / veo forward refs on their t2v path
43
+ // (generate-video.md mode table). kling-3-omni is i2v-only and keeps its
44
+ // documented orchestrator behavior (image_required on the t2v path) —
45
+ // whether refs alone should satisfy it is a separate question from #861.
46
+ for (const id of ["seedance-2", "gemini-omni-video", "veo3.1", "kling-3-omni"]) {
47
+ expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
48
+ }
49
+ })
50
+
51
+ it("a split-id model with no ref support on either twin is unaffected (wan, happyhorse)", () => {
52
+ for (const id of ["wan-i2v", "wan-2.7-i2v", "happyhorse-i2v"]) {
53
+ expect(resolveVideoModeForInputs(id, { hasStartFrame: false, hasImageRefs: true })).toBe(T2V)
54
+ }
55
+ })
56
+
57
+ it("every alias group is classified — a new split-id model lands in one of the two pinned outcomes", () => {
58
+ // Explicit per-group expectations (not a re-derivation of the formula):
59
+ // grok is the one group whose twins differ on ref support today.
60
+ const expectedByBase: Record<string, "image-to-video" | "text-to-video"> = {
61
+ "grok-i2v": I2V,
62
+ "wan-i2v": T2V,
63
+ "wan-2.7-i2v": T2V,
64
+ "happyhorse-i2v": T2V,
65
+ }
66
+ expect(VIDEO_MODE_ALIASES.map((g) => g.base).sort()).toEqual(Object.keys(expectedByBase).sort())
67
+ for (const g of VIDEO_MODE_ALIASES) {
68
+ const expected = expectedByBase[g.base]!
69
+ expect(resolveVideoModeForInputs(g.base, { hasStartFrame: false, hasImageRefs: true }), g.base).toBe(expected)
70
+ // The mode it picks resolves to the twin that can actually run it.
71
+ if (expected === I2V) expect(resolveVideoProviderForMode(g.base, expected)).toBe(g.i2v)
72
+ }
73
+ })
74
+ })
@@ -480,3 +480,41 @@ export function resolveXfadeName(id: string): string | null {
480
480
  }
481
481
  return entry.xfade
482
482
  }
483
+
484
+ /**
485
+ * Map a `transition` PARAMETER-NODE pick (the @nodaro/prompts creative
486
+ * catalog — prose-hint transitions for AI video prompts) onto the ffmpeg
487
+ * xfade vocabulary above. The two vocabularies are deliberately different
488
+ * (the picker describes shots, this table describes blends); only the
489
+ * visually-faithful subset maps — everything else falls back to `cut`.
490
+ *
491
+ * Consumed by the slideshow node (its transition TYPE comes from a wired
492
+ * transition parameter node; unwired defaults to cut). Kept here — not in
493
+ * @nodaro/prompts — because the dependency points prompts → shared, and the
494
+ * ids are plain strings on both sides.
495
+ */
496
+ export const PICKER_TO_COMBINE_TRANSITION: Readonly<Record<string, string>> = {
497
+ "none": "cut",
498
+ "auto": "dissolve", // an explicitly wired "auto" wants A blend — the classic slideshow default
499
+ "cross-dissolve": "dissolve",
500
+ "fade-to-black": "dip-to-black",
501
+ "fade-to-white": "dip-to-white",
502
+ "iris": "circle-open",
503
+ "wipe": "wipe-left",
504
+ "pixelate-reform": "pixelize",
505
+ "dissolve-to-mist": "hblur",
506
+ "match-cut": "cut",
507
+ "smash-cut": "cut",
508
+ "freeze-frame-jump": "cut",
509
+ }
510
+
511
+ /**
512
+ * Resolve ANY transition string a slideshow may receive — a combine id
513
+ * (already valid), a picker id (mapped), or anything else (→ cut).
514
+ */
515
+ export function resolveSlideshowTransition(value: string | undefined | null): string {
516
+ if (!value) return "cut"
517
+ if (COMBINE_TRANSITION_IDS.includes(value)) return value
518
+ return PICKER_TO_COMBINE_TRANSITION[value] ?? "cut"
519
+ }
520
+
@@ -155,7 +155,7 @@ export function estimateLoopTrimAddonCredits(
155
155
  Math.ceil(frames / VIDEO_UTIL_PRICING.FRAMES_PER_CREDIT)
156
156
  }
157
157
 
158
- /** BASE credits (at-cost) for assemble-narrated-video: 3 flat + 1 per 6
158
+ /** BASE credits for assemble-narrated-video: 3 flat + 1 per 6
159
159
  * blocks. 6→4, 24→7, 60→13. Single source of truth shared by the backend
160
160
  * route/creditGuard (`backend/src/providers/video/narrated-block-fit.ts`
161
161
  * re-exports this) and the frontend pre-run estimate
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Featured entity starter catalog (north-star §6 ③ — the "Featured" tab of each
2
+ * Featured entity starter catalog (the "Featured" tab of each
3
3
  * entity Library).
4
4
  *
5
5
  * These are app-provided starter presets: a curated visual description per
package/src/i18n/types.ts CHANGED
@@ -52,8 +52,6 @@ export interface LanguageDefinition {
52
52
  readonly shortCode: string
53
53
  /** Reading direction. */
54
54
  readonly dir: LocaleDirection
55
- /** Optional flag emoji for visual recognition. */
56
- readonly flag: string
57
55
  }
58
56
 
59
57
  /**
@@ -62,18 +60,18 @@ export interface LanguageDefinition {
62
60
  * languages, then RTL languages last.
63
61
  */
64
62
  export const LANGUAGES: ReadonlyArray<LanguageDefinition> = [
65
- { id: "en", englishName: "English", nativeName: "English", shortCode: "EN", dir: "ltr", flag: "🇬🇧" },
66
- { id: "es", englishName: "Spanish", nativeName: "Español", shortCode: "ES", dir: "ltr", flag: "🇪🇸" },
67
- { id: "fr", englishName: "French", nativeName: "Français", shortCode: "FR", dir: "ltr", flag: "🇫🇷" },
68
- { id: "de", englishName: "German", nativeName: "Deutsch", shortCode: "DE", dir: "ltr", flag: "🇩🇪" },
69
- { id: "pt-BR", englishName: "Portuguese (Brazil)", nativeName: "Português", shortCode: "PT", dir: "ltr", flag: "🇧🇷" },
70
- { id: "ru", englishName: "Russian", nativeName: "Русский", shortCode: "RU", dir: "ltr", flag: "🇷🇺" },
71
- { id: "hi", englishName: "Hindi", nativeName: "हिन्दी", shortCode: "HI", dir: "ltr", flag: "🇮🇳" },
72
- { id: "ja", englishName: "Japanese", nativeName: "日本語", shortCode: "JA", dir: "ltr", flag: "🇯🇵" },
73
- { id: "ko", englishName: "Korean", nativeName: "한국어", shortCode: "KO", dir: "ltr", flag: "🇰🇷" },
74
- { id: "zh-CN", englishName: "Chinese (Simplified)", nativeName: "简体中文", shortCode: "ZH", dir: "ltr", flag: "🇨🇳" },
75
- { id: "he", englishName: "Hebrew", nativeName: "עברית", shortCode: "HE", dir: "rtl", flag: "🇮🇱" },
76
- { id: "ar", englishName: "Arabic", nativeName: "العربية", shortCode: "AR", dir: "rtl", flag: "🇸🇦" },
63
+ { id: "en", englishName: "English", nativeName: "English", shortCode: "EN", dir: "ltr" },
64
+ { id: "es", englishName: "Spanish", nativeName: "Español", shortCode: "ES", dir: "ltr" },
65
+ { id: "fr", englishName: "French", nativeName: "Français", shortCode: "FR", dir: "ltr" },
66
+ { id: "de", englishName: "German", nativeName: "Deutsch", shortCode: "DE", dir: "ltr" },
67
+ { id: "pt-BR", englishName: "Portuguese (Brazil)", nativeName: "Português", shortCode: "PT", dir: "ltr" },
68
+ { id: "ru", englishName: "Russian", nativeName: "Русский", shortCode: "RU", dir: "ltr" },
69
+ { id: "hi", englishName: "Hindi", nativeName: "हिन्दी", shortCode: "HI", dir: "ltr" },
70
+ { id: "ja", englishName: "Japanese", nativeName: "日本語", shortCode: "JA", dir: "ltr" },
71
+ { id: "ko", englishName: "Korean", nativeName: "한국어", shortCode: "KO", dir: "ltr" },
72
+ { id: "zh-CN", englishName: "Chinese (Simplified)", nativeName: "简体中文", shortCode: "ZH", dir: "ltr" },
73
+ { id: "he", englishName: "Hebrew", nativeName: "עברית", shortCode: "HE", dir: "rtl" },
74
+ { id: "ar", englishName: "Arabic", nativeName: "العربية", shortCode: "AR", dir: "rtl" },
77
75
  ] as const
78
76
 
79
77
  export const LOCALE_IDS: ReadonlyArray<LocaleId> = LANGUAGES.map((l) => l.id)
package/src/index.ts CHANGED
@@ -110,6 +110,7 @@ export {
110
110
  VIDEO_MODE_ALIASES,
111
111
  VIDEO_GEN_COLLAPSED_T2V_IDS,
112
112
  resolveVideoProviderForMode,
113
+ resolveVideoModeForInputs,
113
114
  isSeedance2Provider,
114
115
  MINIMAX_H3_PROVIDERS,
115
116
  isMinimaxH3Provider,
@@ -396,7 +397,6 @@ export {
396
397
  TILT_CARRIED_FRACTION,
397
398
  isTiltDirection,
398
399
  defaultCarriedFraction,
399
- buildSurroundFillPrompt,
400
400
  type SurroundDirection,
401
401
  } from "./surround.js"
402
402
 
@@ -762,6 +762,9 @@ export type {
762
762
  WorkflowExportObject,
763
763
  WorkflowExportCreature,
764
764
  WorkflowExportLocation,
765
+ WorkflowMediaRef,
766
+ WorkflowPortability,
767
+ WorkflowImportReport,
765
768
  } from "./workflow-export.js"
766
769
  export { stripExportContent } from "./workflow-export.js"
767
770
 
@@ -860,6 +863,8 @@ export * from "./reduce-strategy-registry.js"
860
863
  export {
861
864
  COMBINE_TRANSITIONS,
862
865
  COMBINE_TRANSITION_IDS,
866
+ PICKER_TO_COMBINE_TRANSITION,
867
+ resolveSlideshowTransition,
863
868
  COMBINE_TRANSITION_GROUP_ORDER,
864
869
  COMBINE_TRANSITION_GROUP_LABELS,
865
870
  getCombineTransition,
@@ -886,6 +891,8 @@ export {
886
891
  FAN_OUT_EACH_TYPES,
887
892
  } from "./producer-types.js"
888
893
 
894
+ export { SUNO_TRACK_SOURCE_TYPES } from "./suno-track-sources.js"
895
+
889
896
  export {
890
897
  VOICE_CHANGER_MODELS,
891
898
  VOICE_CHANGER_MODEL_IDS,
@@ -953,3 +960,6 @@ export * from "./smart-cut-windows.js"
953
960
 
954
961
  export * from "./entity-asset-types.js"
955
962
  export * from "./hint-graph-types.js"
963
+
964
+ // --- Organizations (second tenancy axis): wire contract only — enums, settings request schemas, error codes ---
965
+ export * from "./organizations/index.js"
package/src/llm-models.ts CHANGED
@@ -529,6 +529,9 @@ export type LlmFeature =
529
529
  // feature (not ai-writer, which it used to piggyback on) so the model
530
530
  // default and the tiered credit ids are the strategy's own.
531
531
  | "pick-best-llm"
532
+ // In-app Workflow Copilot turns (backend agent loop; metered, reservation
533
+ // ceiling under `STATIC_CREDIT_COSTS["workflow-copilot"]`).
534
+ | "workflow-copilot"
532
535
 
533
536
  /** Engine-dependent LlmFeature for the motion-graphics node (design §8: every credit-id site must branch on engine). */
534
537
  export function motionGraphicsFeature(engine?: string): LlmFeature {
@@ -553,6 +556,7 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
553
556
  "translate": "gemini-3.6-flash",
554
557
  "image-critic": "claude-sonnet-4.6",
555
558
  "pick-best-llm": "claude-sonnet-4.6",
559
+ "workflow-copilot": "claude-sonnet-5",
556
560
  }
557
561
 
558
562
  /**
@@ -886,6 +886,9 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
886
886
  description:
887
887
  "Grok Imagine Image 2.0 — expressive, high-contrast t2i. Generations chain into grok-2-segment (free named region masks) and grok-2-edit (region-targeted edits).",
888
888
  useCases: ["stylized", "expressive", "general"],
889
+ // Refs auto-route to grok-2-i2i (segment-map → image-edit chain); ONE
890
+ // reference (REF_IMAGE_MAX_LIMITS).
891
+ features: ["reference-image"],
889
892
  aspectRatios: GROK_RATIOS,
890
893
  pricing: [{ identifier: "grok-2", credits: 10 }],
891
894
  },
@@ -901,6 +904,22 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
901
904
  useCases: ["edit", "region-edit"],
902
905
  pricing: [{ identifier: "grok-2-edit", credits: 10 }],
903
906
  },
907
+ // Auto-selected when references are attached to grok-2 (T2I_TO_I2I_VARIANT):
908
+ // the t2i endpoint takes no image, so the FREE segment-map mints a task id
909
+ // from the reference URL and image-edit consumes it. Single reference.
910
+ "grok-2-i2i": {
911
+ id: "grok-2-i2i",
912
+ kind: "image",
913
+ modes: ["i2i"] as const,
914
+ family: "xAI",
915
+ label: "Grok Imagine 2 (reference)",
916
+ series: "Grok",
917
+ description:
918
+ "grok-2 guided by ONE reference image via the segment-map → image-edit chain — preserves the reference's composition while applying the prompt.",
919
+ useCases: ["restyle", "edit", "reference"],
920
+ features: ["reference-image"],
921
+ pricing: [{ identifier: "grok-2-i2i", credits: 10 }],
922
+ },
904
923
  "grok-2-segment": {
905
924
  id: "grok-2-segment",
906
925
  kind: "image",
@@ -5,7 +5,7 @@
5
5
  import { z } from "zod"
6
6
  import { MODEL_CATALOG } from "./model-catalog.js"
7
7
 
8
- /** Base USD cost per 1 Nodaro credit, at cost. Used for cost→credit conversion. */
8
+ /** Base USD value of 1 Nodaro credit. Used for cost→credit conversion. */
9
9
  export const CREDIT_BASE_USD = 0.002
10
10
 
11
11
  /** Max characters for the (assembled) prompt accepted by the image-generation routes
@@ -232,16 +232,25 @@ export function getMaxTtsChars(provider: string | undefined): number {
232
232
  * Suno per-version field caps (from docs.kie.ai/suno-api/generate-music). The old
233
233
  * flat {@link SUNO_TEXT_MAX} (3000) was simultaneously too low for V4.5+/V5
234
234
  * prompts (5000) and too high for `style` (1000) and `title` (80).
235
- * - prompt / lyrics: 500 in non-custom mode (all versions); in custom mode
235
+ * - prompt / lyrics: 3000 in non-custom mode (all versions); in custom mode
236
236
  * 3000 for V4/V3.5 and 5000 for V4.5 / V4.5PLUS / V4.5ALL / V5 / V5.5.
237
237
  * - style: 200 for V4/V3.5, 1000 for V4.5+.
238
238
  * - title: 80 (all versions).
239
239
  */
240
240
  export const SUNO_TITLE_MAX = 80
241
241
 
242
- /** Max Suno `prompt` (= lyrics in custom mode) length for a model version. */
242
+ /**
243
+ * Max Suno `prompt` (= lyrics in custom mode) length for a model version.
244
+ *
245
+ * NON-CUSTOM WAS 500 UNTIL 2026-08-19 — six times under the provider's
246
+ * documented 3000, and the route TRUNCATES to this number instead of
247
+ * rejecting, so everything past it vanished without a trace. Field evidence:
248
+ * a 950-character recast score brief (instruments, vocal, the source's own
249
+ * scat syllables, the arrangement's arc) reached Suno as exactly 500
250
+ * characters, cut mid-word.
251
+ */
243
252
  export function getMaxSunoPromptChars(model: string | undefined, customMode: boolean): number {
244
- if (!customMode) return 500
253
+ if (!customMode) return 3000
245
254
  return model === "V4" || model === "V3_5" ? 3000 : 5000
246
255
  }
247
256
 
@@ -364,6 +373,7 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
364
373
  "gpt-image",
365
374
  "gpt-image-2",
366
375
  "grok",
376
+ "grok-2",
367
377
  "qwen",
368
378
  "seedream",
369
379
  "seedream-5-lite",
@@ -374,6 +384,7 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
374
384
  "nano-banana-edit",
375
385
  "gpt-image-i2i",
376
386
  "gpt-image-2-i2i",
387
+ "grok-2-i2i",
377
388
  "flux-i2i",
378
389
  "flux-pro-i2i",
379
390
  "flux-kontext",
@@ -412,6 +423,9 @@ export const T2I_TO_I2I_VARIANT: Record<string, string> = {
412
423
  "gpt-image": "gpt-image-i2i",
413
424
  "gpt-image-2": "gpt-image-2-i2i",
414
425
  "grok": "grok-i2i",
426
+ // grok-2's t2i takes NO image input; its "i2i" is the segment-map(image_url)
427
+ // → image-edit(task_id) chain in the KIE provider (single reference).
428
+ "grok-2": "grok-2-i2i",
415
429
  "qwen": "qwen-i2i",
416
430
  "seedream": "seedream-edit",
417
431
  "seedream-5-lite": "seedream-5-lite-i2i",
@@ -437,6 +451,8 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
437
451
  "nano-banana-2": 4,
438
452
  "nano-banana-2-lite": 10,
439
453
  "wan-2.7": 9,
454
+ // grok-2 reference chain consumes exactly one image (segment-map input).
455
+ "grok-2-i2i": 1,
440
456
  // Image-to-image (multi-source array)
441
457
  "nano-banana-edit": 8,
442
458
  "gpt-image-i2i": 16,
@@ -592,6 +608,7 @@ export const IMAGE_I2I_PROVIDERS = [
592
608
  "flux-pro-i2i",
593
609
  "gpt-image-i2i",
594
610
  "gpt-image-2-i2i",
611
+ "grok-2-i2i",
595
612
  "ideogram-edit",
596
613
  "ideogram-remix",
597
614
  "ideogram-reframe",
@@ -824,6 +841,35 @@ export function resolveVideoProviderForMode(
824
841
  return provider
825
842
  }
826
843
 
844
+ /**
845
+ * Which execution mode a unified Generate Video run takes from what is wired.
846
+ * Shared by the frontend DAG executor (`execute-node.ts`) and the backend
847
+ * orchestrator (`payload-builder.ts`) so the two cannot disagree.
848
+ *
849
+ * A start frame is image-to-video, full stop. Reference images ALONE are the
850
+ * nuance: most models forward refs on either path, but a split-id model
851
+ * (VIDEO_MODE_ALIASES) can carry them on one twin only — Grok Imagine 1's
852
+ * text-to-video endpoint has no image parameter at all, while its i2v twin
853
+ * takes up to 7. Refs wired without a start frame used to resolve to t2v →
854
+ * `grok` → silently dropped (#861). When refs are present and ONLY the i2v
855
+ * twin can carry them, the run is image-to-video with the refs as its images.
856
+ * Derived from VIDEO_REF_LIMITS_BY_PROVIDER (= the catalog's `reference-image`
857
+ * feature), never from a provider name; single-id models are untouched
858
+ * because both twins are the same id.
859
+ */
860
+ export function resolveVideoModeForInputs(
861
+ provider: string | undefined,
862
+ inputs: { readonly hasStartFrame: boolean; readonly hasImageRefs: boolean },
863
+ ): "image-to-video" | "text-to-video" {
864
+ if (inputs.hasStartFrame) return "image-to-video"
865
+ if (!provider || !inputs.hasImageRefs) return "text-to-video"
866
+ const i2v = resolveVideoProviderForMode(provider, "image-to-video")
867
+ const t2v = resolveVideoProviderForMode(provider, "text-to-video")
868
+ if (i2v === t2v) return "text-to-video"
869
+ const carriesImageRefs = (id: string) => (VIDEO_REF_LIMITS_BY_PROVIDER[id]?.images ?? 0) > 0
870
+ return carriesImageRefs(i2v) && !carriesImageRefs(t2v) ? "image-to-video" : "text-to-video"
871
+ }
872
+
827
873
  /**
828
874
  * t2v twin ids hidden from the unified Generate Video picker — the i2v/base
829
875
  * entry already represents both modes (execution remaps by image presence).
@@ -1526,9 +1572,8 @@ export const SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC = 1.8
1526
1572
  * credit formulas bill per continuation join. Clears the provider floor
1527
1573
  * above with margin while staying short enough to keep the model focused on
1528
1574
  * continuing the boundary motion instead of re-staging the whole clip.
1529
- * Guarded ≥ floor by model-constants tests; the private-plugin twin
1530
- * (nodaro-cloud-plugins chain.ts TAIL_SEC / bridge-math.ts MIN_REF) is
1531
- * guarded by that repo's r2v-ref-floor.test.ts — keep the two in sync.
1575
+ * Guarded ≥ floor by model-constants tests; the plugin repo carries a twin
1576
+ * constant guarded by its own tests — keep the two in sync.
1532
1577
  */
1533
1578
  export const SEEDANCE_2_CONTINUATION_REF_SEC = 2
1534
1579
 
@@ -0,0 +1,2 @@
1
+ export * from "./types.js"
2
+ export * from "./views.js"