@nodaro/shared 2.18.0 → 2.20.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.
Files changed (83) hide show
  1. package/dist/index.cjs +350 -23
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +955 -564
  4. package/dist/index.d.ts +955 -564
  5. package/dist/index.js +334 -24
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/credit-identifiers.test.ts +180 -0
  9. package/src/__tests__/gvp-supported-providers.test.ts +22 -1
  10. package/src/__tests__/image-pricing-catalog-coverage.test.ts +139 -0
  11. package/src/__tests__/normalize-node-params.test.ts +36 -0
  12. package/src/__tests__/organizations-types.test.ts +29 -1
  13. package/src/__tests__/prompt-length-limits.test.ts +40 -0
  14. package/src/__tests__/suno-credit-type.test.ts +54 -0
  15. package/src/__tests__/video-analysis-brief.test.ts +61 -0
  16. package/src/__tests__/video-audio-capability.test.ts +43 -0
  17. package/src/__tests__/video-mode-for-inputs.test.ts +1 -1
  18. package/src/__tests__/wan-3-catalog.test.ts +243 -0
  19. package/src/credit-identifiers.ts +191 -21
  20. package/src/i18n/held-prop.ar.ts +2 -0
  21. package/src/i18n/held-prop.de.ts +3 -0
  22. package/src/i18n/held-prop.es.ts +3 -0
  23. package/src/i18n/held-prop.fr.ts +2 -0
  24. package/src/i18n/held-prop.he.ts +3 -0
  25. package/src/i18n/held-prop.hi.ts +2 -0
  26. package/src/i18n/held-prop.ja.ts +3 -0
  27. package/src/i18n/held-prop.ko.ts +3 -0
  28. package/src/i18n/held-prop.pt-BR.ts +3 -0
  29. package/src/i18n/held-prop.ru.ts +2 -0
  30. package/src/i18n/held-prop.zh-CN.ts +3 -0
  31. package/src/i18n/person.ar.ts +3 -0
  32. package/src/i18n/person.de.ts +4 -0
  33. package/src/i18n/person.es.ts +4 -0
  34. package/src/i18n/person.fr.ts +3 -0
  35. package/src/i18n/person.he.ts +4 -0
  36. package/src/i18n/person.hi.ts +3 -0
  37. package/src/i18n/person.ja.ts +4 -0
  38. package/src/i18n/person.ko.ts +4 -0
  39. package/src/i18n/person.pt-BR.ts +4 -0
  40. package/src/i18n/person.ru.ts +3 -0
  41. package/src/i18n/person.zh-CN.ts +4 -0
  42. package/src/i18n/setting.ar.ts +2 -0
  43. package/src/i18n/setting.de.ts +3 -0
  44. package/src/i18n/setting.es.ts +3 -0
  45. package/src/i18n/setting.fr.ts +2 -0
  46. package/src/i18n/setting.he.ts +3 -0
  47. package/src/i18n/setting.hi.ts +2 -0
  48. package/src/i18n/setting.ja.ts +3 -0
  49. package/src/i18n/setting.ko.ts +3 -0
  50. package/src/i18n/setting.pt-BR.ts +3 -0
  51. package/src/i18n/setting.ru.ts +2 -0
  52. package/src/i18n/setting.zh-CN.ts +3 -0
  53. package/src/i18n/style.ar.ts +2 -0
  54. package/src/i18n/style.de.ts +3 -0
  55. package/src/i18n/style.es.ts +3 -0
  56. package/src/i18n/style.fr.ts +2 -0
  57. package/src/i18n/style.he.ts +3 -0
  58. package/src/i18n/style.hi.ts +2 -0
  59. package/src/i18n/style.ja.ts +3 -0
  60. package/src/i18n/style.ko.ts +3 -0
  61. package/src/i18n/style.pt-BR.ts +3 -0
  62. package/src/i18n/style.ru.ts +2 -0
  63. package/src/i18n/style.zh-CN.ts +3 -0
  64. package/src/i18n/styling.ar.ts +4 -0
  65. package/src/i18n/styling.de.ts +5 -0
  66. package/src/i18n/styling.es.ts +5 -0
  67. package/src/i18n/styling.fr.ts +4 -0
  68. package/src/i18n/styling.he.ts +5 -0
  69. package/src/i18n/styling.hi.ts +4 -0
  70. package/src/i18n/styling.ja.ts +5 -0
  71. package/src/i18n/styling.ko.ts +5 -0
  72. package/src/i18n/styling.pt-BR.ts +5 -0
  73. package/src/i18n/styling.ru.ts +4 -0
  74. package/src/i18n/styling.zh-CN.ts +5 -0
  75. package/src/index.ts +17 -0
  76. package/src/model-catalog.ts +101 -0
  77. package/src/model-constants.ts +251 -16
  78. package/src/node-default-mappings.ts +5 -0
  79. package/src/normalize-node-params.ts +8 -0
  80. package/src/organizations/types.ts +12 -0
  81. package/src/organizations/views.ts +114 -0
  82. package/src/video-analysis.ts +45 -0
  83. package/src/video-ui-defaults.ts +66 -0
@@ -56,6 +56,16 @@ export type GrantedAccess = (typeof GRANTED_ACCESS)[number]
56
56
  export const SUBMISSION_STATUSES = ["submitted", "in_review", "returned", "approved"] as const
57
57
  export type SubmissionStatus = (typeof SUBMISSION_STATUSES)[number]
58
58
 
59
+ /**
60
+ * How a usage report is bucketed. `workspace` is org-scope only; `none`
61
+ * = flat rows. A runtime list, like `ORG_ROLES`, so the plugin route's Zod is
62
+ * `z.enum(USAGE_GROUP_BYS)` and the migration guard asserts the SQL
63
+ * `NOT IN (...)` list equals this minus `none` — one vocabulary, never three
64
+ * hand-copies that drift.
65
+ */
66
+ export const USAGE_GROUP_BYS = ["workspace", "member", "model", "day", "none"] as const
67
+ export type UsageGroupBy = (typeof USAGE_GROUP_BYS)[number]
68
+
59
69
  /**
60
70
  * Error codes the organization endpoints add to the standard envelope
61
71
  * (`{ error: { code, message } }`). Clients dispatch on the code, never on
@@ -80,6 +90,8 @@ export const ORG_ERROR_CODES = [
80
90
  "domain_not_allowed",
81
91
  "already_started",
82
92
  "collab_unavailable",
93
+ // A CSV usage export whose write-ahead audit row could not be written (503).
94
+ "audit_unavailable",
83
95
  // Organization, workspace and membership endpoints.
84
96
  "terms_required",
85
97
  "not_org_member",
@@ -4,6 +4,7 @@ import type {
4
4
  OrgRole,
5
5
  OrgSettings,
6
6
  OrgStatus,
7
+ UsageGroupBy,
7
8
  WorkspaceRole,
8
9
  WorkspaceSettings,
9
10
  } from "./types.js"
@@ -218,3 +219,116 @@ export interface OrgPage<T> {
218
219
  data: T[]
219
220
  nextCursor: string | null
220
221
  }
222
+
223
+ /**
224
+ * Usage reporting. What `GET /v1/orgs/:id/usage` and
225
+ * `GET /v1/workspaces/:id/usage` return. No cost/USD field appears anywhere —
226
+ * a report shows CREDITS a class or team spent, never the platform's own rates
227
+ * (pricing-leak class, guarded by organizations-types.test.ts and the
228
+ * migration guard).
229
+ */
230
+
231
+ /** One bucket of a usage report. Exactly one of workspace/member/model/day is set. */
232
+ export interface UsageReportRow {
233
+ key: string
234
+ workspace: { id: string; name: string | null; slug: string | null; archived: boolean } | null
235
+ member: { userId: string; displayName: string | null; email: string | null } | null
236
+ model: string | null
237
+ /** `YYYY-MM-DD` in the report's `tz`. */
238
+ day: string | null
239
+ runCount: number
240
+ appRunCount: number
241
+ /** Settled where known, the held reservation otherwise. = settledCredits + inFlightCredits. */
242
+ credits: number
243
+ settledCredits: number
244
+ inFlightCredits: number
245
+ inFlightRuns: number
246
+ }
247
+
248
+ /**
249
+ * A platform-absorbed line for one workspace, split by ORIGIN (never attributed
250
+ * to a member). Two ledgers share the `org_usage_variance` source: a
251
+ * `metered_overrun` (a metered run's overrun beyond the budget — it HAS a
252
+ * settled usage_logs counterpart) and an `app_markup` shortfall (an
253
+ * approved-app markup the budget could not cover — it has NO usage_logs row).
254
+ * `other` is a future/unrecognised origin.
255
+ */
256
+ export interface UsageVarianceRow {
257
+ workspace: { id: string; name: string | null; slug: string | null } | null
258
+ kind: "metered_overrun" | "app_markup" | "other"
259
+ credits: number
260
+ rowCount: number
261
+ }
262
+
263
+ /**
264
+ * Totals over the WHOLE window — every usage_logs row in [from, to] after the
265
+ * scope / userId / workspaceId narrowing — never over the returned `rows`.
266
+ * Unaffected by `truncated`; equals a `groupBy=day` report's column-wise sum.
267
+ */
268
+ export interface UsageReportTotals {
269
+ runCount: number
270
+ credits: number
271
+ settledCredits: number
272
+ inFlightCredits: number
273
+ /** Metered-overrun variance in the window — a run's overrun the platform absorbed. */
274
+ platformAbsorbedCredits: number
275
+ /** Approved-app markup shortfall the platform absorbed. It has NO usage_logs run, so it is not in the figures above. */
276
+ appMarkupAbsorbedCredits: number
277
+ /**
278
+ * settledCredits − platformAbsorbedCredits: the METERED settlement that
279
+ * reached the workspace budget(s). App markup charged to a budget (migration
280
+ * 352) is not a usage_logs row and is not included here; when a markup
281
+ * shortfall is absorbed this figure under-reports and may go negative — it is
282
+ * NOT `workspace_budgets.spent_credits`.
283
+ */
284
+ chargedToBudget: number
285
+ }
286
+
287
+ export interface UsageReport {
288
+ scope: "org" | "workspace"
289
+ scopeId: string
290
+ from: string
291
+ to: string
292
+ tz: string
293
+ groupBy: Exclude<UsageGroupBy, "none">
294
+ /** Present when a member's self-view or an admin's `?userId=` narrowed the report. */
295
+ userId: string | null
296
+ /** Present when an org report was narrowed to one workspace. */
297
+ workspaceId: string | null
298
+ rows: UsageReportRow[]
299
+ variance: UsageVarianceRow[]
300
+ totals: UsageReportTotals
301
+ /**
302
+ * True when more than 5000 buckets existed and the tail of `rows` was dropped
303
+ * — narrow the window. Only `rows` is incomplete; `totals` and `variance`
304
+ * cover the whole window regardless.
305
+ */
306
+ truncated: boolean
307
+ }
308
+
309
+ /** One usage_logs row as an organization sees it. No cost fields, ever. */
310
+ export interface UsageLogEntry {
311
+ id: string
312
+ createdAt: string
313
+ workspace: { id: string; name: string | null; slug: string | null } | null
314
+ member: { userId: string; displayName: string | null; email: string | null } | null
315
+ jobId: string | null
316
+ model: string
317
+ status: "reserved" | "committed"
318
+ creditsReserved: number
319
+ creditsSettled: number | null
320
+ credits: number
321
+ isAppRun: boolean
322
+ }
323
+
324
+ /** Query parameters shared by both usage routes (dates inclusive, IANA tz). */
325
+ export interface UsageQuery {
326
+ from?: string
327
+ to?: string
328
+ tz?: string
329
+ groupBy?: UsageGroupBy
330
+ workspaceId?: string
331
+ userId?: string
332
+ cursor?: string
333
+ limit?: number
334
+ }
@@ -633,3 +633,48 @@ export function inferMusicVideo(analysis: {
633
633
  }),
634
634
  )
635
635
  }
636
+
637
+ // ---------------------------------------------------------------------------
638
+ // The analysis as a BRIEF — the compact projection an LLM is handed
639
+ // ---------------------------------------------------------------------------
640
+
641
+ /** Top-level keys the analyzer derives AFTER the model's pass (merge
642
+ * diagnostics, folded cast looks). A reader drafting FROM the analysis — a
643
+ * production plan, a script — needs none of them. */
644
+ const DERIVED_ANALYSIS_TOP_KEYS: ReadonlySet<string> = new Set(["warnings", "variationFolds"])
645
+
646
+ /** Per-scene keys the validator computes from `visual` and the slot list. */
647
+ const DERIVED_ANALYSIS_SCENE_KEYS: ReadonlySet<string> = new Set(["visualResolved", "slotRefs", "oversized"])
648
+
649
+ const asRecord = (v: unknown): Record<string, unknown> | null =>
650
+ typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null
651
+
652
+ /**
653
+ * The analysis with its server-derived fields removed — the form an LLM is
654
+ * handed when the analysis IS the brief (Nodaro Studio's Director and its
655
+ * job-id loader). Drops `warnings` and `variationFolds` at the top and
656
+ * `visualResolved`, `slotRefs`, `oversized` on every scene; keeps
657
+ * `refImageUrl` (a downstream cast image) and everything else. Never mutates;
658
+ * a non-object input comes back as-is.
659
+ *
660
+ * ONE strip list, shared by the worker that composes the brief server-side
661
+ * (`llm-structured` jobs with a `videoUrl`) and the client that loads a
662
+ * finished analysis into a textarea, so the two can never drift.
663
+ */
664
+ export function stripDerivedAnalysisFields(json: unknown): unknown {
665
+ const doc = asRecord(json)
666
+ if (!doc) return json
667
+ const out: Record<string, unknown> = {}
668
+ for (const [key, value] of Object.entries(doc)) {
669
+ if (DERIVED_ANALYSIS_TOP_KEYS.has(key)) continue
670
+ out[key] =
671
+ key === "scenes" && Array.isArray(value)
672
+ ? value.map((scene) => {
673
+ const s = asRecord(scene)
674
+ if (!s) return scene
675
+ return Object.fromEntries(Object.entries(s).filter(([k]) => !DERIVED_ANALYSIS_SCENE_KEYS.has(k)))
676
+ })
677
+ : value
678
+ }
679
+ return out
680
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * UI-default fills for the unified video nodes — the ONE source of truth shared
3
+ * by the backend DAG payload builder, the config-panel fail-safe snaps and the
4
+ * node's hover/run strip.
5
+ *
6
+ * Several config panels RENDER a default without persisting it to node data, so
7
+ * an untouched node submits `aspectRatio` / `resolution` undefined and the
8
+ * enqueued job row (and the /v1/jobs echo) then disagrees with what actually
9
+ * renders. Worse, the panels' generic snap writes `resolutions[0]` for a stale
10
+ * value, which is the CHEAPEST tier under the repo's ascending-resolution
11
+ * convention — not necessarily the tier the model renders and bills.
12
+ *
13
+ * These helpers were local to `backend/src/services/workflow-engine/payload-builder.ts`
14
+ * until the Wan 3.0 launch gave the platform its first provider whose declared
15
+ * billing default differs from `resolutions[0]`; three surfaces then had to
16
+ * agree, so they live here.
17
+ *
18
+ * The resolution fill is per-family ON PURPOSE. Wan 3.0 reads its DECLARED
19
+ * billing default (PRICING_DEFAULT_RESOLUTION = 720p) because its catalog list
20
+ * is ascending, so an index-0 fill would write 480p — a tier both `runWan3`
21
+ * (which renders 720P) and the credit identifier (which bills the 720p row)
22
+ * disagree with. The Seedance family keeps its historical first-catalog-tier
23
+ * fill, deliberately: switching it to PRICING_DEFAULT_RESOLUTION would reprice
24
+ * live seedance-2-5 runs, whose declared default (720p) differs from the 480p
25
+ * this has always filled. That 480p-fill vs 720p-billing-default divergence on
26
+ * seedance-2-5 is a KNOWN pre-existing gap, out of scope here — do not "align"
27
+ * the branches without repricing it deliberately.
28
+ */
29
+
30
+ import {
31
+ PRICING_DEFAULT_RESOLUTION,
32
+ isSeedance2Provider,
33
+ isMinimaxH3Provider,
34
+ isWan3Provider,
35
+ isGeminiOmniProvider,
36
+ } from "./model-constants.js"
37
+ import { MODEL_CATALOG } from "./model-catalog.js"
38
+
39
+ /** `adaptive` is the aspect default for Seedance 2, MiniMax H3 and Wan 3.0. */
40
+ export function uiAspectRatioFill(provider: string): string | undefined {
41
+ return isSeedance2Provider(provider) || isMinimaxH3Provider(provider) || isWan3Provider(provider)
42
+ ? "adaptive"
43
+ : undefined
44
+ }
45
+
46
+ /** See the file docstring — per-family on purpose; never a generic
47
+ * PRICING_DEFAULT_RESOLUTION read (that would reprice seedance-2-5). */
48
+ export function uiResolutionFill(provider: string): string | undefined {
49
+ if (isWan3Provider(provider)) return PRICING_DEFAULT_RESOLUTION[provider]
50
+ if (isSeedance2Provider(provider)) return MODEL_CATALOG[provider]?.resolutions?.[0]
51
+ return undefined
52
+ }
53
+
54
+ /**
55
+ * Duration the node RENDERS and BILLS when `data.duration` is unset, for the
56
+ * families whose credit identifier declares a duration fallback that is NOT
57
+ * `durations[0]`. Wan 3.0's bare identifier is the 5s tier while its ladder
58
+ * starts at 2s; the Gemini Omni family's is the 8s tier while its ladder starts
59
+ * at 4s. Everyone else falls back to the first listed duration, so this returns
60
+ * undefined and the caller keeps `durations[0]`.
61
+ */
62
+ export function uiDurationFill(provider: string): number | undefined {
63
+ if (isWan3Provider(provider)) return 5
64
+ if (isGeminiOmniProvider(provider)) return 8
65
+ return undefined
66
+ }