@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/src/llm-models.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
export type LlmTier = "economy" | "standard" | "premium"
|
|
17
17
|
export type KieApiFormat = "chat-completions" | "messages" | "responses"
|
|
18
|
+
export type LlmVendor = "anthropic" | "google" | "openai" | "xai"
|
|
18
19
|
|
|
19
20
|
export const LLM_REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] as const
|
|
20
21
|
export type LlmReasoningEffort = (typeof LLM_REASONING_EFFORTS)[number]
|
|
@@ -32,7 +33,7 @@ export interface LlmModelDef {
|
|
|
32
33
|
* For messages: the model id sent in the body (e.g. "claude-haiku-4-5-v1messages").
|
|
33
34
|
* For responses: the model id sent in the body (e.g. "gpt-5-4"). */
|
|
34
35
|
kieSlugOrModel: string
|
|
35
|
-
vendor:
|
|
36
|
+
vendor: LlmVendor
|
|
36
37
|
supportsImages: boolean
|
|
37
38
|
maxOutputTokens: number
|
|
38
39
|
/**
|
|
@@ -159,6 +160,30 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
159
160
|
// direct is the reliability fallback only.
|
|
160
161
|
directGeminiModel: "gemini-3.6-flash",
|
|
161
162
|
},
|
|
163
|
+
{
|
|
164
|
+
id: "gemini-3.7-flash",
|
|
165
|
+
displayName: "Gemini 3.7 Flash",
|
|
166
|
+
desc: "Newest fast Gemini, agentic-tuned",
|
|
167
|
+
tier: "economy",
|
|
168
|
+
kieFormat: "chat-completions",
|
|
169
|
+
// KIE serves it on the OpenAI-compatible dialect under this slug
|
|
170
|
+
// (docs.kie.ai/market/gemini/gemini-3-7-flash-openai.md) — same
|
|
171
|
+
// chat-completions path shape as gemini-3.6-flash.
|
|
172
|
+
kieSlugOrModel: "gemini-3-7-flash-openai",
|
|
173
|
+
vendor: "google",
|
|
174
|
+
structuredOutputMode: "kie-response-format",
|
|
175
|
+
supportsImages: true,
|
|
176
|
+
// Google's own cap is 65,536, but the field feeds BOTH lanes and the KIE
|
|
177
|
+
// flash endpoints cap at 8192 (the measured 3.6 posture) — stay at the
|
|
178
|
+
// KIE-safe intersection, same reasoning as `reasoningEfforts` below.
|
|
179
|
+
maxOutputTokens: 8192,
|
|
180
|
+
// KIE's 3.7 endpoint enumerates reasoning_effort low | high (verified
|
|
181
|
+
// against its OpenAPI spec 2026-08-18), identical to 3.6.
|
|
182
|
+
reasoningEfforts: ["low", "high"],
|
|
183
|
+
// Assumed parity with 3.6 pending a live probe on the direct lane.
|
|
184
|
+
directReasoningEfforts: ["none", "low", "medium", "high"],
|
|
185
|
+
directGeminiModel: "gemini-3.7-flash",
|
|
186
|
+
},
|
|
162
187
|
{
|
|
163
188
|
id: "claude-haiku-4.5",
|
|
164
189
|
displayName: "Claude Haiku 4.5",
|
|
@@ -321,7 +346,35 @@ export const LLM_MODELS: readonly LlmModelDef[] = [
|
|
|
321
346
|
reasoningEfforts: ["none", "low", "medium", "high", "xhigh", "max"],
|
|
322
347
|
supportsTemperature: false,
|
|
323
348
|
},
|
|
324
|
-
|
|
349
|
+
{
|
|
350
|
+
id: "grok-4.6",
|
|
351
|
+
displayName: "Grok 4.6",
|
|
352
|
+
desc: "xAI flagship, strong reasoning",
|
|
353
|
+
tier: "standard",
|
|
354
|
+
kieFormat: "responses",
|
|
355
|
+
// KIE serves Grok on the responses dialect under its own family path —
|
|
356
|
+
// grok/v1/responses, NOT codex/v1/responses (llm-client derives the path
|
|
357
|
+
// from `vendor`). Live-verified end-to-end 2026-08-18: array `input`,
|
|
358
|
+
// `developer` system role, `input_image` URL vision, `text.format`
|
|
359
|
+
// json_schema enforcement, SSE `response.output_text.delta` stream, and
|
|
360
|
+
// `credits_consumed` actual-cost capture. (grok-4.5 was deferred 2026-07-13
|
|
361
|
+
// because none of this was live; 4.6 is its activation.)
|
|
362
|
+
kieSlugOrModel: "grok-4-6",
|
|
363
|
+
vendor: "xai",
|
|
364
|
+
structuredOutputMode: "responses-json-schema",
|
|
365
|
+
supportsImages: true,
|
|
366
|
+
maxOutputTokens: 16384,
|
|
367
|
+
// KIE's documented enum, each level live-verified (echoed back) 2026-08-18.
|
|
368
|
+
// No `none`: the endpoint reasons unconditionally (see thinkingDefaultOn).
|
|
369
|
+
reasoningEfforts: ["low", "medium", "high", "xhigh"],
|
|
370
|
+
// Live-probed 2026-08-18: `temperature` is silently IGNORED (request echo
|
|
371
|
+
// stays at the 0.7 default), so never send it — same treatment as GPT-5.5+.
|
|
372
|
+
supportsTemperature: false,
|
|
373
|
+
// Reasons with NO reasoning param sent (effort defaults to "low" server-side
|
|
374
|
+
// — a trivial probe spent 169 of 170 output tokens on reasoning), so every
|
|
375
|
+
// call needs output headroom, not just xhigh.
|
|
376
|
+
thinkingDefaultOn: true,
|
|
377
|
+
},
|
|
325
378
|
{
|
|
326
379
|
id: "claude-sonnet-5",
|
|
327
380
|
displayName: "Claude Sonnet 5",
|
|
@@ -407,6 +460,55 @@ export const STRUCTURED_VISION_MODELS = LLM_MODELS.filter(
|
|
|
407
460
|
(m) => m.supportsImages && m.structuredOutputMode != null,
|
|
408
461
|
)
|
|
409
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Vendor presentation order + labels for model pickers. Every LlmVendor MUST
|
|
465
|
+
* appear in the order list (guarded by a registry test) so a new vendor can't
|
|
466
|
+
* ship with its models silently sorted to the end of every menu unlabeled.
|
|
467
|
+
* Alphabetical on purpose: stable, and no vendor-preference fights.
|
|
468
|
+
*/
|
|
469
|
+
export const LLM_VENDOR_ORDER: readonly LlmVendor[] = ["anthropic", "google", "openai", "xai"]
|
|
470
|
+
export const LLM_VENDOR_LABELS: Record<LlmVendor, string> = {
|
|
471
|
+
anthropic: "Anthropic",
|
|
472
|
+
google: "Google",
|
|
473
|
+
openai: "OpenAI",
|
|
474
|
+
xai: "xAI",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const TIER_RANK: Record<LlmTier, number> = { economy: 0, standard: 1, premium: 2 }
|
|
478
|
+
|
|
479
|
+
export interface LlmModelGroup {
|
|
480
|
+
vendor: LlmVendor
|
|
481
|
+
/** Display heading for the group (LLM_VENDOR_LABELS[vendor]). */
|
|
482
|
+
label: string
|
|
483
|
+
models: LlmModelDef[]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The ONE ordering every LLM model menu renders: grouped by vendor (in
|
|
488
|
+
* LLM_VENDOR_ORDER), and inside each group sorted economy → standard → premium
|
|
489
|
+
* (registry order breaks ties, which keeps family generations adjacent).
|
|
490
|
+
* A flat registry-order dump was genuinely hard to scan at 17 models — every
|
|
491
|
+
* picker (config panel, quick strips, quick toolbar) derives from this so the
|
|
492
|
+
* menus can't drift apart. Groups with no models (after `filter`) are omitted.
|
|
493
|
+
*/
|
|
494
|
+
export function groupLlmModelsByVendor(models: readonly LlmModelDef[] = LLM_MODELS): LlmModelGroup[] {
|
|
495
|
+
const groups: LlmModelGroup[] = []
|
|
496
|
+
for (const vendor of LLM_VENDOR_ORDER) {
|
|
497
|
+
const members = models
|
|
498
|
+
.filter((m) => m.vendor === vendor)
|
|
499
|
+
.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier])
|
|
500
|
+
if (members.length > 0) groups.push({ vendor, label: LLM_VENDOR_LABELS[vendor], models: members })
|
|
501
|
+
}
|
|
502
|
+
return groups
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** {@link groupLlmModelsByVendor} flattened — for menus that can't render
|
|
506
|
+
* group headers (e.g. the compact node quick strips) but should still read
|
|
507
|
+
* vendor-clustered and tier-ordered. */
|
|
508
|
+
export function orderedLlmModels(models: readonly LlmModelDef[] = LLM_MODELS): LlmModelDef[] {
|
|
509
|
+
return groupLlmModelsByVendor(models).flatMap((g) => g.models)
|
|
510
|
+
}
|
|
511
|
+
|
|
410
512
|
export type LlmFeature =
|
|
411
513
|
| "ai-writer"
|
|
412
514
|
| "llm-chat"
|
|
@@ -427,6 +529,9 @@ export type LlmFeature =
|
|
|
427
529
|
// feature (not ai-writer, which it used to piggyback on) so the model
|
|
428
530
|
// default and the tiered credit ids are the strategy's own.
|
|
429
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"
|
|
430
535
|
|
|
431
536
|
/** Engine-dependent LlmFeature for the motion-graphics node (design §8: every credit-id site must branch on engine). */
|
|
432
537
|
export function motionGraphicsFeature(engine?: string): LlmFeature {
|
|
@@ -451,6 +556,7 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
|
|
|
451
556
|
"translate": "gemini-3.6-flash",
|
|
452
557
|
"image-critic": "claude-sonnet-4.6",
|
|
453
558
|
"pick-best-llm": "claude-sonnet-4.6",
|
|
559
|
+
"workflow-copilot": "claude-sonnet-5",
|
|
454
560
|
}
|
|
455
561
|
|
|
456
562
|
/**
|
|
@@ -465,6 +571,12 @@ export const LLM_FEATURE_DEFAULTS: Record<LlmFeature, string> = {
|
|
|
465
571
|
export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean; audio: boolean }> = {
|
|
466
572
|
"gemini-3-flash": { image: true, video: true, audio: true },
|
|
467
573
|
"gemini-3.6-flash": { image: true, video: true, audio: true },
|
|
574
|
+
// gemini-3.7-flash is IMAGE-ONLY by DECISION, not omission: full video+audio
|
|
575
|
+
// caps would auto-enroll it in VIDEO_ANALYSIS_LLM_MODELS (derived below) and
|
|
576
|
+
// force a video-analysis tier + pricing decision that is deliberately
|
|
577
|
+
// deferred while the smart-family A/B routes this model internally (#747).
|
|
578
|
+
// Flip these two flags ONLY together with that VA-side decision.
|
|
579
|
+
"gemini-3.7-flash": { image: true, video: false, audio: false },
|
|
468
580
|
"gemini-3.1-pro": { image: true, video: true, audio: true },
|
|
469
581
|
"claude-haiku-4.5": { image: true, video: false, audio: false },
|
|
470
582
|
"claude-sonnet-4.6": { image: true, video: false, audio: false },
|
|
@@ -475,6 +587,7 @@ export const LLM_MODALITY_CAPS: Record<string, { image: boolean; video: boolean;
|
|
|
475
587
|
"gpt-5.6-luna": { image: true, video: false, audio: false },
|
|
476
588
|
"gpt-5.6-terra": { image: true, video: false, audio: false },
|
|
477
589
|
"gpt-5.6-sol": { image: true, video: false, audio: false },
|
|
590
|
+
"grok-4.6": { image: true, video: false, audio: false },
|
|
478
591
|
"claude-sonnet-5": { image: true, video: false, audio: false },
|
|
479
592
|
"claude-opus-4.8": { image: true, video: false, audio: false },
|
|
480
593
|
"claude-opus-5": { image: true, video: false, audio: false },
|
package/src/model-catalog.ts
CHANGED
|
@@ -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",
|
package/src/model-constants.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { z } from "zod"
|
|
6
6
|
import { MODEL_CATALOG } from "./model-catalog.js"
|
|
7
7
|
|
|
8
|
-
/** Base USD
|
|
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:
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
1530
|
-
*
|
|
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,152 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Organizations — wire contract for the second tenancy axis
|
|
5
|
+
* (Organization -> Workspace -> Member).
|
|
6
|
+
*
|
|
7
|
+
* Lives in @nodaro/shared because SDK/MCP consumers need these enums, the
|
|
8
|
+
* request schemas the API validates, and the error codes it returns. This
|
|
9
|
+
* package carries the CONTRACT ONLY — no resolution logic, no presets, no
|
|
10
|
+
* vocabulary, no access rule. Those are server-side.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Selects which workspace a request LISTS from and CREATES into. It never
|
|
15
|
+
* authorizes: reading, updating, deleting or running an identified object is
|
|
16
|
+
* decided by that object's own workspace, so a forgotten or forged header can
|
|
17
|
+
* neither widen access nor move a charge.
|
|
18
|
+
*
|
|
19
|
+
* Fastify lower-cases incoming header keys, hence the second constant — read
|
|
20
|
+
* `req.headers[WORKSPACE_HEADER_LOWER]`, send `WORKSPACE_HEADER`.
|
|
21
|
+
*/
|
|
22
|
+
export const WORKSPACE_HEADER = "X-Nodaro-Workspace"
|
|
23
|
+
export const WORKSPACE_HEADER_LOWER = "x-nodaro-workspace"
|
|
24
|
+
|
|
25
|
+
export const ORG_KINDS = ["school", "team"] as const
|
|
26
|
+
export type OrgKind = (typeof ORG_KINDS)[number]
|
|
27
|
+
|
|
28
|
+
export const ORG_ROLES = ["owner", "admin", "member"] as const
|
|
29
|
+
export type OrgRole = (typeof ORG_ROLES)[number]
|
|
30
|
+
|
|
31
|
+
export const WORKSPACE_ROLES = ["admin", "member"] as const
|
|
32
|
+
export type WorkspaceRole = (typeof WORKSPACE_ROLES)[number]
|
|
33
|
+
|
|
34
|
+
export const MEMBER_STATUSES = ["active", "suspended"] as const
|
|
35
|
+
export type MemberStatus = (typeof MEMBER_STATUSES)[number]
|
|
36
|
+
|
|
37
|
+
/** `pending` = created, awaiting platform-admin approval. */
|
|
38
|
+
export const ORG_STATUSES = ["pending", "active", "suspended", "deleted"] as const
|
|
39
|
+
export type OrgStatus = (typeof ORG_STATUSES)[number]
|
|
40
|
+
|
|
41
|
+
/** An explicit per-workflow grant (works for personal workflows too). */
|
|
42
|
+
export const COLLABORATOR_ROLES = ["editor", "viewer"] as const
|
|
43
|
+
export type CollaboratorRole = (typeof COLLABORATOR_ROLES)[number]
|
|
44
|
+
|
|
45
|
+
export const WORKFLOW_VISIBILITIES = ["private", "workspace"] as const
|
|
46
|
+
export type WorkflowVisibility = (typeof WORKFLOW_VISIBILITIES)[number]
|
|
47
|
+
|
|
48
|
+
/** What an identity may do with a workflow, strongest first. */
|
|
49
|
+
export const ACCESS_LEVELS = ["own", "edit", "view", "none"] as const
|
|
50
|
+
export type AccessLevel = (typeof ACCESS_LEVELS)[number]
|
|
51
|
+
|
|
52
|
+
/** The access a setting may grant to a non-creator. */
|
|
53
|
+
export const GRANTED_ACCESS = ["view", "edit"] as const
|
|
54
|
+
export type GrantedAccess = (typeof GRANTED_ACCESS)[number]
|
|
55
|
+
|
|
56
|
+
export const SUBMISSION_STATUSES = ["submitted", "in_review", "returned", "approved"] as const
|
|
57
|
+
export type SubmissionStatus = (typeof SUBMISSION_STATUSES)[number]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error codes the organization endpoints add to the standard envelope
|
|
61
|
+
* (`{ error: { code, message } }`). Clients dispatch on the code, never on
|
|
62
|
+
* the message text.
|
|
63
|
+
*/
|
|
64
|
+
export const ORG_ERROR_CODES = [
|
|
65
|
+
"not_a_member",
|
|
66
|
+
"insufficient_role",
|
|
67
|
+
"org_not_active",
|
|
68
|
+
"member_suspended",
|
|
69
|
+
"workspace_archived",
|
|
70
|
+
"personal_space_disabled",
|
|
71
|
+
"token_workspace_mismatch",
|
|
72
|
+
"run_requires_authenticated_member",
|
|
73
|
+
"budget_exceeded",
|
|
74
|
+
"member_cap_exceeded",
|
|
75
|
+
"model_not_allowed",
|
|
76
|
+
"invitation_expired",
|
|
77
|
+
"invitation_revoked",
|
|
78
|
+
"email_mismatch",
|
|
79
|
+
"join_code_invalid",
|
|
80
|
+
"domain_not_allowed",
|
|
81
|
+
"already_started",
|
|
82
|
+
"collab_unavailable",
|
|
83
|
+
// Organization, workspace and membership endpoints.
|
|
84
|
+
"terms_required",
|
|
85
|
+
"not_org_member",
|
|
86
|
+
"already_a_member",
|
|
87
|
+
"owner_cannot_leave",
|
|
88
|
+
"has_active_workspaces",
|
|
89
|
+
// Invitations and join codes.
|
|
90
|
+
"invitation_not_found",
|
|
91
|
+
"invitation_accepted",
|
|
92
|
+
"bulk_invite_cap_exceeded",
|
|
93
|
+
] as const
|
|
94
|
+
export type OrgErrorCode = (typeof ORG_ERROR_CODES)[number]
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The settings every organization kind has a default for. `organizations.
|
|
98
|
+
* settings` and `workspaces.settings` store PARTIAL overrides of this shape;
|
|
99
|
+
* `resolveEffectiveSettings` (./settings.ts) produces the full one.
|
|
100
|
+
*/
|
|
101
|
+
export const PresetSettingsSchema = z.object({
|
|
102
|
+
/** What org/workspace admins may do with a member's workflow. */
|
|
103
|
+
admin_access: z.enum(GRANTED_ACCESS),
|
|
104
|
+
default_workflow_visibility: z.enum(WORKFLOW_VISIBILITIES),
|
|
105
|
+
/** What a plain member may do with a `visibility = workspace` workflow. */
|
|
106
|
+
member_access_to_shared: z.enum(GRANTED_ACCESS),
|
|
107
|
+
members_can_create_projects: z.boolean(),
|
|
108
|
+
member_caps_enabled: z.boolean(),
|
|
109
|
+
/** Whether members keep a personal (non-workspace) space at all. */
|
|
110
|
+
personal_space_enabled: z.boolean(),
|
|
111
|
+
/** Whether a workspace admin may invite NEW people into the org. */
|
|
112
|
+
workspace_admins_can_invite: z.boolean(),
|
|
113
|
+
/** Whether an editor collaborator may invite further collaborators. */
|
|
114
|
+
collaborators_can_invite: z.boolean(),
|
|
115
|
+
/**
|
|
116
|
+
* When the organization is SUSPENDED, do its content rules still bind its
|
|
117
|
+
* members?
|
|
118
|
+
*
|
|
119
|
+
* Today this governs exactly one rule — `personal_space_enabled` — and that
|
|
120
|
+
* is not an accident: every other key above governs behaviour INSIDE a
|
|
121
|
+
* workspace, and a suspended organization grants no workspace context at
|
|
122
|
+
* all, so those are already moot.
|
|
123
|
+
*
|
|
124
|
+
* The name is general because an organization is deciding a principle here,
|
|
125
|
+
* not one checkbox's fate. Default `false`, which is today's behaviour: a
|
|
126
|
+
* suspended organization stops binding, and its members work independently
|
|
127
|
+
* until it resumes. An organization whose reason for disabling the personal
|
|
128
|
+
* space is contractual — the work made here belongs to the institution —
|
|
129
|
+
* turns this on, because an unpaid invoice does not void a contract.
|
|
130
|
+
*/
|
|
131
|
+
policy_survives_suspension: z.boolean(),
|
|
132
|
+
})
|
|
133
|
+
export type PresetSettings = z.infer<typeof PresetSettingsSchema>
|
|
134
|
+
export type PresetSettingKey = keyof PresetSettings
|
|
135
|
+
export const PRESET_SETTING_KEYS = Object.freeze(
|
|
136
|
+
Object.keys(PresetSettingsSchema.shape) as readonly PresetSettingKey[],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
/** `workspaces.settings` — per-workspace overrides only. */
|
|
140
|
+
export const WorkspaceSettingsSchema = PresetSettingsSchema.partial()
|
|
141
|
+
export type WorkspaceSettings = z.infer<typeof WorkspaceSettingsSchema>
|
|
142
|
+
|
|
143
|
+
const EMAIL_DOMAIN_PATTERN = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/
|
|
144
|
+
|
|
145
|
+
/** `organizations.settings` — preset overrides plus org-only keys. */
|
|
146
|
+
export const OrgSettingsSchema = PresetSettingsSchema.partial().extend({
|
|
147
|
+
/** Lower-case domains that join codes / domain auto-join accept. Empty = any. */
|
|
148
|
+
allowed_email_domains: z.array(z.string().regex(EMAIL_DOMAIN_PATTERN)).optional(),
|
|
149
|
+
/** Per-org relabelling of the kind vocabulary (./vocabulary.ts). */
|
|
150
|
+
vocabulary_overrides: z.record(z.string(), z.string()).optional(),
|
|
151
|
+
})
|
|
152
|
+
export type OrgSettings = z.infer<typeof OrgSettingsSchema>
|