@nodaro/shared 2.7.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/dist/index.cjs +281 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -95
- package/dist/index.d.ts +503 -95
- package/dist/index.js +257 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/llm-models.test.ts +137 -10
- package/src/__tests__/organizations-types.test.ts +61 -0
- package/src/__tests__/producer-types.test.ts +11 -0
- package/src/__tests__/prompt-length-limits.test.ts +7 -3
- package/src/__tests__/seedance2-continuation-ref.test.ts +2 -3
- package/src/__tests__/video-analysis-catalog-sync.test.ts +1 -1
- package/src/__tests__/video-analysis-pricing.test.ts +2 -2
- package/src/__tests__/video-mode-for-inputs.test.ts +74 -0
- package/src/combine-transitions.ts +38 -0
- package/src/credit-estimators/video-utils.ts +1 -1
- package/src/featured-entities.ts +1 -1
- package/src/i18n/types.ts +12 -14
- package/src/index.ts +17 -1
- package/src/llm-models.ts +115 -2
- package/src/model-catalog.ts +19 -0
- package/src/model-constants.ts +52 -7
- package/src/organizations/index.ts +2 -0
- package/src/organizations/types.ts +152 -0
- package/src/organizations/views.ts +220 -0
- package/src/producer-types.ts +6 -0
- package/src/smart-cut-windows.ts +8 -17
- package/src/suno-track-sources.ts +23 -0
- package/src/surround.ts +10 -90
- package/src/video-analysis-pricing.ts +25 -36
- package/src/workflow-export.ts +41 -0
package/package.json
CHANGED
|
@@ -6,12 +6,18 @@ import {
|
|
|
6
6
|
LLM_REASONING_EFFORTS,
|
|
7
7
|
getLlmModel,
|
|
8
8
|
getLlmTier,
|
|
9
|
+
getLlmModalityCaps,
|
|
10
|
+
VIDEO_ANALYSIS_LLM_MODELS,
|
|
9
11
|
buildLlmCreditIdentifier,
|
|
10
12
|
resolveLlmCreditId,
|
|
11
13
|
motionGraphicsFeature,
|
|
12
14
|
effectiveReasoningEffort,
|
|
13
15
|
supportsAdvancedMode,
|
|
14
16
|
availableReasoningEfforts,
|
|
17
|
+
LLM_VENDOR_ORDER,
|
|
18
|
+
LLM_VENDOR_LABELS,
|
|
19
|
+
groupLlmModelsByVendor,
|
|
20
|
+
orderedLlmModels,
|
|
15
21
|
} from "../llm-models.js"
|
|
16
22
|
import type { LlmModelDef, LlmTier, LlmFeature } from "../llm-models.js"
|
|
17
23
|
import { PIPELINE_PINNABLE_SCRIPT_LLMS } from "../pipeline-types.js"
|
|
@@ -28,6 +34,7 @@ import { PIPELINE_PINNABLE_SCRIPT_LLMS } from "../pipeline-types.js"
|
|
|
28
34
|
const EXPECTED_MODEL_IDS = [
|
|
29
35
|
"gemini-3-flash",
|
|
30
36
|
"gemini-3.6-flash",
|
|
37
|
+
"gemini-3.7-flash",
|
|
31
38
|
"claude-haiku-4.5",
|
|
32
39
|
"claude-sonnet-4.6",
|
|
33
40
|
"gpt-5.2",
|
|
@@ -38,6 +45,7 @@ const EXPECTED_MODEL_IDS = [
|
|
|
38
45
|
"gpt-5.6-luna",
|
|
39
46
|
"gpt-5.6-terra",
|
|
40
47
|
"gpt-5.6-sol",
|
|
48
|
+
"grok-4.6",
|
|
41
49
|
"claude-sonnet-5",
|
|
42
50
|
"claude-opus-4.8",
|
|
43
51
|
"claude-opus-5",
|
|
@@ -86,13 +94,13 @@ describe("LLM_MODELS data integrity", () => {
|
|
|
86
94
|
expect(new Set(ids).size).toBe(ids.length)
|
|
87
95
|
})
|
|
88
96
|
|
|
89
|
-
it("has
|
|
97
|
+
it("has 5 economy, 5 standard, 8 premium models", () => {
|
|
90
98
|
const tierCounts: Record<LlmTier, number> = { economy: 0, standard: 0, premium: 0 }
|
|
91
99
|
for (const model of LLM_MODELS) {
|
|
92
100
|
tierCounts[model.tier]++
|
|
93
101
|
}
|
|
94
|
-
expect(tierCounts.economy).toBe(
|
|
95
|
-
expect(tierCounts.standard).toBe(
|
|
102
|
+
expect(tierCounts.economy).toBe(5)
|
|
103
|
+
expect(tierCounts.standard).toBe(5)
|
|
96
104
|
expect(tierCounts.premium).toBe(8)
|
|
97
105
|
})
|
|
98
106
|
|
|
@@ -103,11 +111,12 @@ describe("LLM_MODELS data integrity", () => {
|
|
|
103
111
|
expect(formats).toContain("responses")
|
|
104
112
|
})
|
|
105
113
|
|
|
106
|
-
it("all
|
|
114
|
+
it("all four vendors are represented", () => {
|
|
107
115
|
const vendors = new Set(LLM_MODELS.map((m) => m.vendor))
|
|
108
116
|
expect(vendors).toContain("anthropic")
|
|
109
117
|
expect(vendors).toContain("google")
|
|
110
118
|
expect(vendors).toContain("openai")
|
|
119
|
+
expect(vendors).toContain("xai")
|
|
111
120
|
})
|
|
112
121
|
|
|
113
122
|
it("all models support images", () => {
|
|
@@ -383,10 +392,11 @@ describe("LLM_FEATURE_DEFAULTS", () => {
|
|
|
383
392
|
"translate",
|
|
384
393
|
"image-critic",
|
|
385
394
|
"pick-best-llm",
|
|
395
|
+
"workflow-copilot",
|
|
386
396
|
]
|
|
387
397
|
|
|
388
|
-
it("has entries for all
|
|
389
|
-
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(
|
|
398
|
+
it("has entries for all 17 features", () => {
|
|
399
|
+
expect(Object.keys(LLM_FEATURE_DEFAULTS)).toHaveLength(17)
|
|
390
400
|
for (const feature of ALL_FEATURES) {
|
|
391
401
|
expect(LLM_FEATURE_DEFAULTS).toHaveProperty(feature)
|
|
392
402
|
}
|
|
@@ -472,6 +482,7 @@ describe("STRUCTURED_VISION_MODELS", () => {
|
|
|
472
482
|
"claude-sonnet-4.6",
|
|
473
483
|
"gemini-3-flash",
|
|
474
484
|
"gemini-3.6-flash",
|
|
485
|
+
"gemini-3.7-flash",
|
|
475
486
|
"gemini-3.1-pro",
|
|
476
487
|
"claude-sonnet-5",
|
|
477
488
|
"claude-opus-4.8",
|
|
@@ -484,15 +495,18 @@ describe("STRUCTURED_VISION_MODELS", () => {
|
|
|
484
495
|
"gpt-5.6-luna",
|
|
485
496
|
"gpt-5.6-terra",
|
|
486
497
|
"gpt-5.6-sol",
|
|
498
|
+
// responses-format Grok — vision + text.format live-verified 2026-08-18.
|
|
499
|
+
"grok-4.6",
|
|
487
500
|
].sort(),
|
|
488
501
|
)
|
|
489
502
|
})
|
|
490
503
|
|
|
491
|
-
it("includes Anthropic (forced-tool), Gemini (response_format), and OpenAI (responses text.format) vendors", () => {
|
|
504
|
+
it("includes Anthropic (forced-tool), Gemini (response_format), and OpenAI/xAI (responses text.format) vendors", () => {
|
|
492
505
|
const vendors = new Set(STRUCTURED_VISION_MODELS.map((m) => m.vendor))
|
|
493
506
|
expect(vendors).toContain("anthropic")
|
|
494
507
|
expect(vendors).toContain("google")
|
|
495
508
|
expect(vendors).toContain("openai")
|
|
509
|
+
expect(vendors).toContain("xai")
|
|
496
510
|
})
|
|
497
511
|
|
|
498
512
|
it("excludes chat-completions GPT models — no native structured mode there (parse+retry only)", () => {
|
|
@@ -509,9 +523,9 @@ describe("STRUCTURED_VISION_MODELS", () => {
|
|
|
509
523
|
})
|
|
510
524
|
|
|
511
525
|
// ---------------------------------------------------------------------------
|
|
512
|
-
// Reasoning effort registry (GPT-5.6 / Claude Sonnet 5 / Claude Opus 4.8
|
|
513
|
-
// grok-4.5
|
|
514
|
-
//
|
|
526
|
+
// Reasoning effort registry (GPT-5.6 / Claude Sonnet 5 / Claude Opus 4.8 /
|
|
527
|
+
// Grok 4.6 — grok-4.5 was deferred on 2026-07-13 because its chat endpoint
|
|
528
|
+
// wasn't live; grok-4.6 is its activation, live-verified 2026-08-18.)
|
|
515
529
|
// ---------------------------------------------------------------------------
|
|
516
530
|
describe("reasoning effort registry", () => {
|
|
517
531
|
it("every reasoningEfforts list is a subset of the superset, in ascending order", () => {
|
|
@@ -526,6 +540,7 @@ describe("reasoning effort registry", () => {
|
|
|
526
540
|
expect(getLlmTier("gpt-5.6-luna")).toBe("economy")
|
|
527
541
|
expect(getLlmTier("gpt-5.6-terra")).toBe("standard")
|
|
528
542
|
expect(getLlmTier("gpt-5.6-sol")).toBe("premium")
|
|
543
|
+
expect(getLlmTier("grok-4.6")).toBe("standard")
|
|
529
544
|
expect(getLlmTier("claude-sonnet-5")).toBe("standard")
|
|
530
545
|
expect(getLlmTier("claude-opus-4.8")).toBe("premium")
|
|
531
546
|
expect(getLlmTier("gpt-5.5")).toBe("premium")
|
|
@@ -557,6 +572,62 @@ describe("reasoning effort registry", () => {
|
|
|
557
572
|
expect(getLlmModel("claude-fable-5")?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh", "max"])
|
|
558
573
|
expect(getLlmModel("claude-fable-5")?.directFallbackModel).toBe("claude-fable-5")
|
|
559
574
|
})
|
|
575
|
+
|
|
576
|
+
it("grok-4.6: responses format on the grok family path, KIE's documented effort enum, no sampling params, reasons by default", () => {
|
|
577
|
+
const m = getLlmModel("grok-4.6")
|
|
578
|
+
expect(m?.vendor).toBe("xai")
|
|
579
|
+
expect(m?.kieFormat).toBe("responses")
|
|
580
|
+
expect(m?.kieSlugOrModel).toBe("grok-4-6")
|
|
581
|
+
expect(m?.structuredOutputMode).toBe("responses-json-schema")
|
|
582
|
+
// KIE's enum is low..xhigh with NO `none` — the endpoint reasons
|
|
583
|
+
// unconditionally (thinkingDefaultOn), so `none` would be a lie.
|
|
584
|
+
expect(m?.reasoningEfforts).toEqual(["low", "medium", "high", "xhigh"])
|
|
585
|
+
// Live-probed 2026-08-18: temperature is silently ignored — never send it.
|
|
586
|
+
expect(m?.supportsTemperature).toBe(false)
|
|
587
|
+
// Load-bearing: consumers floor max_tokens off this flag (a trivial probe
|
|
588
|
+
// spent 169/170 output tokens on reasoning with no reasoning param sent).
|
|
589
|
+
expect(m?.thinkingDefaultOn).toBe(true)
|
|
590
|
+
// KIE-only: no direct lane on either vendor SDK.
|
|
591
|
+
expect(m?.directFallbackModel).toBeUndefined()
|
|
592
|
+
expect(m?.directGeminiModel).toBeUndefined()
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it("grok-4.6 resolves from its dash-form wire slug", () => {
|
|
596
|
+
expect(getLlmModel("grok-4-6")?.id).toBe("grok-4.6")
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// gemini-3.7-flash exposure (both lanes live-verified 2026-08-18: KIE
|
|
602
|
+
// chat-completions with reasoning_effort + enforced response_format, and the
|
|
603
|
+
// direct Google id resolving on generativelanguage)
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
describe("gemini-3.7-flash exposure", () => {
|
|
606
|
+
it("KIE-first chat-completions with a direct lane — 3.6-flash's proven shape", () => {
|
|
607
|
+
const m = getLlmModel("gemini-3.7-flash")
|
|
608
|
+
expect(m?.tier).toBe("economy")
|
|
609
|
+
expect(m?.vendor).toBe("google")
|
|
610
|
+
expect(m?.kieFormat).toBe("chat-completions")
|
|
611
|
+
expect(m?.kieSlugOrModel).toBe("gemini-3-7-flash-openai")
|
|
612
|
+
// KIE-first on purpose (no preferDirect): the cheap lane serves Generate
|
|
613
|
+
// Text; the direct lane is Advanced mode + the reliability fallback.
|
|
614
|
+
expect(m?.preferDirect).toBeUndefined()
|
|
615
|
+
expect(m?.directGeminiModel).toBe("gemini-3.7-flash")
|
|
616
|
+
expect(m?.reasoningEfforts).toEqual(["low", "high"])
|
|
617
|
+
expect(m?.structuredOutputMode).toBe("kie-response-format")
|
|
618
|
+
expect(supportsAdvancedMode("gemini-3.7-flash")).toBe(true)
|
|
619
|
+
expect(availableReasoningEfforts("gemini-3.7-flash", true)).toEqual(["none", "low", "medium", "high"])
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("stays OUT of video-analysis — image-only modality caps by decision, not omission", () => {
|
|
623
|
+
// Full video+audio caps would auto-enroll it in VIDEO_ANALYSIS_LLM_MODELS
|
|
624
|
+
// and force a VA tier + pricing decision that is deliberately deferred
|
|
625
|
+
// while the smart-family A/B routes this model internally (2026-08-18).
|
|
626
|
+
// If this test goes red, someone flipped the caps — that flip is only
|
|
627
|
+
// valid TOGETHER with the VA-side tier/pricing decision.
|
|
628
|
+
expect(getLlmModalityCaps("gemini-3.7-flash")).toEqual({ image: true, video: false, audio: false })
|
|
629
|
+
expect(VIDEO_ANALYSIS_LLM_MODELS).not.toContain("gemini-3.7-flash")
|
|
630
|
+
})
|
|
560
631
|
})
|
|
561
632
|
|
|
562
633
|
describe("effectiveReasoningEffort", () => {
|
|
@@ -584,6 +655,7 @@ describe("buildLlmCreditIdentifier effort bump (xhigh/max only)", () => {
|
|
|
584
655
|
})
|
|
585
656
|
it("standard + xhigh → premium", () => {
|
|
586
657
|
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-terra", "xhigh")).toBe("llm-chat:premium")
|
|
658
|
+
expect(buildLlmCreditIdentifier("llm-chat", "grok-4.6", "xhigh")).toBe("llm-chat:premium")
|
|
587
659
|
})
|
|
588
660
|
it("premium + max stays premium", () => {
|
|
589
661
|
expect(buildLlmCreditIdentifier("llm-chat", "gpt-5.6-sol", "max")).toBe("llm-chat:premium")
|
|
@@ -767,3 +839,58 @@ describe("lane-aware reasoning efforts", () => {
|
|
|
767
839
|
expect(bad).toEqual([])
|
|
768
840
|
})
|
|
769
841
|
})
|
|
842
|
+
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Vendor grouping — the ONE ordering every LLM model menu renders
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
describe("groupLlmModelsByVendor / orderedLlmModels", () => {
|
|
847
|
+
it("every vendor in the union has an order slot AND a label (totality)", () => {
|
|
848
|
+
// A new vendor added to LlmVendor without a menu decision would silently
|
|
849
|
+
// sort its models to the end of every picker unlabeled — fail here instead.
|
|
850
|
+
const vendorsInUse = new Set(LLM_MODELS.map((m) => m.vendor))
|
|
851
|
+
for (const v of vendorsInUse) {
|
|
852
|
+
expect(LLM_VENDOR_ORDER, `vendor "${v}" missing from LLM_VENDOR_ORDER`).toContain(v)
|
|
853
|
+
expect(LLM_VENDOR_LABELS[v], `vendor "${v}" missing from LLM_VENDOR_LABELS`).toBeTruthy()
|
|
854
|
+
}
|
|
855
|
+
for (const v of LLM_VENDOR_ORDER) {
|
|
856
|
+
expect(LLM_VENDOR_LABELS[v]).toBeTruthy()
|
|
857
|
+
}
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it("covers every model exactly once, in vendor order", () => {
|
|
861
|
+
const groups = groupLlmModelsByVendor()
|
|
862
|
+
const flattened = groups.flatMap((g) => g.models.map((m) => m.id))
|
|
863
|
+
expect(flattened.sort()).toEqual(LLM_MODELS.map((m) => m.id).sort())
|
|
864
|
+
expect(new Set(flattened).size).toBe(flattened.length)
|
|
865
|
+
const groupVendors = groups.map((g) => g.vendor)
|
|
866
|
+
expect(groupVendors).toEqual(LLM_VENDOR_ORDER.filter((v) => groupVendors.includes(v)))
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
it("inside each group models are tier-ordered economy → standard → premium", () => {
|
|
870
|
+
const rank = { economy: 0, standard: 1, premium: 2 } as const
|
|
871
|
+
for (const g of groupLlmModelsByVendor()) {
|
|
872
|
+
const ranks = g.models.map((m) => rank[m.tier])
|
|
873
|
+
expect(ranks, g.vendor).toEqual([...ranks].sort((a, b) => a - b))
|
|
874
|
+
}
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
it("groups carry their display label and omit empty groups after a filter", () => {
|
|
878
|
+
const groups = groupLlmModelsByVendor(LLM_MODELS.filter((m) => m.vendor === "xai"))
|
|
879
|
+
expect(groups).toHaveLength(1)
|
|
880
|
+
expect(groups[0].label).toBe("xAI")
|
|
881
|
+
expect(groups[0].models.map((m) => m.id)).toEqual(["grok-4.6"])
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
it("does not mutate the registry (registry order is load-bearing for LLM_MODEL_IDS)", () => {
|
|
885
|
+
const before = LLM_MODELS.map((m) => m.id)
|
|
886
|
+
groupLlmModelsByVendor()
|
|
887
|
+
orderedLlmModels()
|
|
888
|
+
expect(LLM_MODELS.map((m) => m.id)).toEqual(before)
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it("orderedLlmModels is the flattened grouping", () => {
|
|
892
|
+
expect(orderedLlmModels().map((m) => m.id)).toEqual(
|
|
893
|
+
groupLlmModelsByVendor().flatMap((g) => g.models.map((m) => m.id)),
|
|
894
|
+
)
|
|
895
|
+
})
|
|
896
|
+
})
|
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
15
|
-
* twin constants
|
|
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
|
|
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
|
-
// (
|
|
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
|
|
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
|
|
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
|
package/src/featured-entities.ts
CHANGED
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"
|
|
66
|
-
{ id: "es", englishName: "Spanish", nativeName: "Español", shortCode: "ES", dir: "ltr"
|
|
67
|
-
{ id: "fr", englishName: "French", nativeName: "Français", shortCode: "FR", dir: "ltr"
|
|
68
|
-
{ id: "de", englishName: "German", nativeName: "Deutsch", shortCode: "DE", dir: "ltr"
|
|
69
|
-
{ id: "pt-BR", englishName: "Portuguese (Brazil)", nativeName: "Português", shortCode: "PT", dir: "ltr"
|
|
70
|
-
{ id: "ru", englishName: "Russian", nativeName: "Русский", shortCode: "RU", dir: "ltr"
|
|
71
|
-
{ id: "hi", englishName: "Hindi", nativeName: "हिन्दी", shortCode: "HI", dir: "ltr"
|
|
72
|
-
{ id: "ja", englishName: "Japanese", nativeName: "日本語", shortCode: "JA", dir: "ltr"
|
|
73
|
-
{ id: "ko", englishName: "Korean", nativeName: "한국어", shortCode: "KO", dir: "ltr"
|
|
74
|
-
{ id: "zh-CN", englishName: "Chinese (Simplified)", nativeName: "简体中文", shortCode: "ZH", dir: "ltr"
|
|
75
|
-
{ id: "he", englishName: "Hebrew", nativeName: "עברית", shortCode: "HE", dir: "rtl"
|
|
76
|
-
{ id: "ar", englishName: "Arabic", nativeName: "العربية", shortCode: "AR", dir: "rtl"
|
|
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,
|
|
@@ -281,6 +282,12 @@ export {
|
|
|
281
282
|
LLM_MODELS,
|
|
282
283
|
LLM_MODEL_IDS,
|
|
283
284
|
STRUCTURED_VISION_MODELS,
|
|
285
|
+
LLM_VENDOR_ORDER,
|
|
286
|
+
LLM_VENDOR_LABELS,
|
|
287
|
+
groupLlmModelsByVendor,
|
|
288
|
+
orderedLlmModels,
|
|
289
|
+
type LlmVendor,
|
|
290
|
+
type LlmModelGroup,
|
|
284
291
|
VIDEO_ANALYSIS_LLM_MODELS,
|
|
285
292
|
VIDEO_ANALYSIS_TIERS,
|
|
286
293
|
VIDEO_ANALYSIS_LEGACY_MODELS,
|
|
@@ -390,7 +397,6 @@ export {
|
|
|
390
397
|
TILT_CARRIED_FRACTION,
|
|
391
398
|
isTiltDirection,
|
|
392
399
|
defaultCarriedFraction,
|
|
393
|
-
buildSurroundFillPrompt,
|
|
394
400
|
type SurroundDirection,
|
|
395
401
|
} from "./surround.js"
|
|
396
402
|
|
|
@@ -756,6 +762,9 @@ export type {
|
|
|
756
762
|
WorkflowExportObject,
|
|
757
763
|
WorkflowExportCreature,
|
|
758
764
|
WorkflowExportLocation,
|
|
765
|
+
WorkflowMediaRef,
|
|
766
|
+
WorkflowPortability,
|
|
767
|
+
WorkflowImportReport,
|
|
759
768
|
} from "./workflow-export.js"
|
|
760
769
|
export { stripExportContent } from "./workflow-export.js"
|
|
761
770
|
|
|
@@ -854,6 +863,8 @@ export * from "./reduce-strategy-registry.js"
|
|
|
854
863
|
export {
|
|
855
864
|
COMBINE_TRANSITIONS,
|
|
856
865
|
COMBINE_TRANSITION_IDS,
|
|
866
|
+
PICKER_TO_COMBINE_TRANSITION,
|
|
867
|
+
resolveSlideshowTransition,
|
|
857
868
|
COMBINE_TRANSITION_GROUP_ORDER,
|
|
858
869
|
COMBINE_TRANSITION_GROUP_LABELS,
|
|
859
870
|
getCombineTransition,
|
|
@@ -880,6 +891,8 @@ export {
|
|
|
880
891
|
FAN_OUT_EACH_TYPES,
|
|
881
892
|
} from "./producer-types.js"
|
|
882
893
|
|
|
894
|
+
export { SUNO_TRACK_SOURCE_TYPES } from "./suno-track-sources.js"
|
|
895
|
+
|
|
883
896
|
export {
|
|
884
897
|
VOICE_CHANGER_MODELS,
|
|
885
898
|
VOICE_CHANGER_MODEL_IDS,
|
|
@@ -947,3 +960,6 @@ export * from "./smart-cut-windows.js"
|
|
|
947
960
|
|
|
948
961
|
export * from "./entity-asset-types.js"
|
|
949
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"
|