@sogni-ai/sogni-client 4.2.0-alpha.2 → 4.2.0-alpha.21

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 (109) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/CLAUDE.md +25 -3
  3. package/README.md +411 -136
  4. package/dist/Account/index.d.ts +4 -2
  5. package/dist/Account/index.js +27 -23
  6. package/dist/Account/index.js.map +1 -1
  7. package/dist/Account/types.d.ts +7 -0
  8. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +3 -1
  9. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +26 -2
  10. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -1
  11. package/dist/ApiClient/WebSocketClient/eventSubscriptions.d.ts +33 -0
  12. package/dist/ApiClient/WebSocketClient/eventSubscriptions.js +39 -0
  13. package/dist/ApiClient/WebSocketClient/eventSubscriptions.js.map +1 -0
  14. package/dist/ApiClient/WebSocketClient/events.d.ts +24 -7
  15. package/dist/ApiClient/WebSocketClient/index.d.ts +5 -1
  16. package/dist/ApiClient/WebSocketClient/index.js +24 -1
  17. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  18. package/dist/ApiClient/WebSocketClient/messages.d.ts +2 -0
  19. package/dist/ApiClient/WebSocketClient/types.d.ts +2 -0
  20. package/dist/ApiClient/index.d.ts +6 -1
  21. package/dist/ApiClient/index.js +7 -3
  22. package/dist/ApiClient/index.js.map +1 -1
  23. package/dist/Chat/ChatTools.d.ts +5 -49
  24. package/dist/Chat/ChatTools.js +311 -88
  25. package/dist/Chat/ChatTools.js.map +1 -1
  26. package/dist/Chat/index.d.ts +11 -2
  27. package/dist/Chat/index.js +78 -4
  28. package/dist/Chat/index.js.map +1 -1
  29. package/dist/Chat/modelRouting.d.ts +100 -0
  30. package/dist/Chat/modelRouting.js +441 -0
  31. package/dist/Chat/modelRouting.js.map +1 -0
  32. package/dist/Chat/sogniHostedTools.generated.json +529 -0
  33. package/dist/Chat/tools.d.ts +9 -55
  34. package/dist/Chat/tools.js +72 -228
  35. package/dist/Chat/tools.js.map +1 -1
  36. package/dist/Chat/types.d.ts +91 -2
  37. package/dist/CreativeWorkflows/index.d.ts +23 -0
  38. package/dist/CreativeWorkflows/index.js +274 -0
  39. package/dist/CreativeWorkflows/index.js.map +1 -0
  40. package/dist/CreativeWorkflows/types.d.ts +106 -0
  41. package/dist/CreativeWorkflows/types.js +3 -0
  42. package/dist/CreativeWorkflows/types.js.map +1 -0
  43. package/dist/Projects/Job.d.ts +6 -0
  44. package/dist/Projects/Job.js +60 -5
  45. package/dist/Projects/Job.js.map +1 -1
  46. package/dist/Projects/Project.js +15 -3
  47. package/dist/Projects/Project.js.map +1 -1
  48. package/dist/Projects/createJobRequestMessage.js +140 -6
  49. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  50. package/dist/Projects/index.d.ts +10 -1
  51. package/dist/Projects/index.js +197 -58
  52. package/dist/Projects/index.js.map +1 -1
  53. package/dist/Projects/types/ModelOptions.d.ts +3 -3
  54. package/dist/Projects/types/ModelOptions.js +12 -5
  55. package/dist/Projects/types/ModelOptions.js.map +1 -1
  56. package/dist/Projects/types/ModelTiersRaw.d.ts +7 -7
  57. package/dist/Projects/types/RawProject.d.ts +2 -0
  58. package/dist/Projects/types/events.d.ts +5 -4
  59. package/dist/Projects/types/index.d.ts +77 -7
  60. package/dist/Projects/types/index.js.map +1 -1
  61. package/dist/Projects/utils/index.d.ts +8 -1
  62. package/dist/Projects/utils/index.js +22 -8
  63. package/dist/Projects/utils/index.js.map +1 -1
  64. package/dist/index.d.ts +28 -3
  65. package/dist/index.js +19 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/lib/RestClient.d.ts +4 -1
  68. package/dist/lib/RestClient.js +17 -9
  69. package/dist/lib/RestClient.js.map +1 -1
  70. package/dist/lib/mediaValidation.d.ts +16 -0
  71. package/dist/lib/mediaValidation.js +280 -0
  72. package/dist/lib/mediaValidation.js.map +1 -0
  73. package/dist/lib/validation.d.ts +6 -1
  74. package/dist/lib/validation.js +28 -2
  75. package/dist/lib/validation.js.map +1 -1
  76. package/llms-full.txt +372 -133
  77. package/llms.txt +197 -86
  78. package/package.json +13 -4
  79. package/src/Account/index.ts +22 -2
  80. package/src/Account/types.ts +7 -0
  81. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +47 -3
  82. package/src/ApiClient/WebSocketClient/eventSubscriptions.ts +92 -0
  83. package/src/ApiClient/WebSocketClient/events.ts +25 -7
  84. package/src/ApiClient/WebSocketClient/index.ts +33 -1
  85. package/src/ApiClient/WebSocketClient/messages.ts +2 -0
  86. package/src/ApiClient/WebSocketClient/types.ts +2 -0
  87. package/src/ApiClient/index.ts +32 -2
  88. package/src/Chat/ChatTools.ts +395 -95
  89. package/src/Chat/index.ts +149 -5
  90. package/src/Chat/modelRouting.ts +602 -0
  91. package/src/Chat/sogniHostedTools.generated.json +529 -0
  92. package/src/Chat/tools.ts +98 -245
  93. package/src/Chat/types.ts +100 -2
  94. package/src/CreativeWorkflows/index.ts +290 -0
  95. package/src/CreativeWorkflows/types.ts +134 -0
  96. package/src/Projects/Job.ts +76 -5
  97. package/src/Projects/Project.ts +13 -3
  98. package/src/Projects/createJobRequestMessage.ts +152 -13
  99. package/src/Projects/index.ts +230 -52
  100. package/src/Projects/types/ModelOptions.ts +15 -8
  101. package/src/Projects/types/ModelTiersRaw.ts +7 -7
  102. package/src/Projects/types/RawProject.ts +2 -0
  103. package/src/Projects/types/events.ts +5 -4
  104. package/src/Projects/types/index.ts +86 -6
  105. package/src/Projects/utils/index.ts +24 -8
  106. package/src/index.ts +93 -0
  107. package/src/lib/RestClient.ts +15 -5
  108. package/src/lib/mediaValidation.ts +367 -0
  109. package/src/lib/validation.ts +38 -2
@@ -0,0 +1,602 @@
1
+ import { getVideoWorkflowType } from '../Projects/utils';
2
+
3
+ export { getVideoWorkflowType };
4
+
5
+ /**
6
+ * Public SDK-local copy of the pure model routing helpers from
7
+ * @sogni/creative-agent/backbone/reference. Keep this file self-contained so
8
+ * published SDK consumers do not need the private creative-agent package.
9
+ */
10
+
11
+ export type BackboneMediaType = 'image' | 'video' | 'audio';
12
+
13
+ export type VideoWorkflow =
14
+ | 't2v'
15
+ | 'i2v'
16
+ | 's2v'
17
+ | 'ia2v'
18
+ | 'a2v'
19
+ | 'v2v'
20
+ | 'animate-move'
21
+ | 'animate-replace';
22
+
23
+ export type VideoControlMode =
24
+ | 'animate-move'
25
+ | 'animate-replace'
26
+ | 'seedance-v2v'
27
+ | 'canny'
28
+ | 'pose'
29
+ | 'depth'
30
+ | 'detailer';
31
+
32
+ export interface BackboneAvailableModel {
33
+ id: string;
34
+ media?: string;
35
+ workerCount?: number;
36
+ }
37
+
38
+ export interface SelectBackboneModelInput {
39
+ mediaType: BackboneMediaType;
40
+ requestedModel?: string;
41
+ workflows?: VideoWorkflow[];
42
+ filter?: (modelId: string) => boolean;
43
+ preferredModelIds?: string[];
44
+ }
45
+
46
+ export interface SelectedBackboneModel {
47
+ modelId: string;
48
+ model: BackboneAvailableModel;
49
+ selectedBy: 'requestedModel' | 'preferredModel' | 'workerCount';
50
+ }
51
+
52
+ export interface HostedToolSchemaProperty {
53
+ type?: string | string[];
54
+ enum?: unknown[];
55
+ items?: HostedToolSchemaProperty;
56
+ }
57
+
58
+ export interface HostedToolSchema {
59
+ required?: string[];
60
+ properties?: Record<string, HostedToolSchemaProperty>;
61
+ }
62
+
63
+ export interface HostedToolDefinition {
64
+ function: {
65
+ name: string;
66
+ parameters?: HostedToolSchema;
67
+ };
68
+ }
69
+
70
+ export interface ValidateHostedToolArgumentsOptions {
71
+ skipEnumProperties?: string[];
72
+ }
73
+
74
+ export const PREFERRED_MODEL_IDS = {
75
+ image: {
76
+ gptImage2: 'gpt-image-2',
77
+ flux1Schnell: 'flux1-schnell-fp8',
78
+ flux2: 'flux2_dev_fp8',
79
+ chromaFlash: 'chroma-v.46-flash_fp8',
80
+ zTurbo: 'z_image_turbo_bf16'
81
+ },
82
+ video: {
83
+ t2v: 'ltx23-22b-fp8_t2v_distilled',
84
+ i2v: 'ltx23-22b-fp8_i2v_distilled',
85
+ a2v: 'ltx23-22b-fp8_a2v_distilled',
86
+ ia2v: 'ltx23-22b-fp8_ia2v_distilled',
87
+ s2v: 'wan_v2.2-14b-fp8_s2v_lightx2v',
88
+ v2v: 'ltx23-22b-fp8_v2v_distilled',
89
+ seedanceT2v: 'seedance-2-0',
90
+ seedanceI2v: 'seedance-2-0',
91
+ seedanceIa2v: 'seedance-2-0',
92
+ seedanceFastT2v: 'seedance-2-0-fast',
93
+ seedanceFastI2v: 'seedance-2-0-fast',
94
+ seedanceV2v: 'seedance-2-0',
95
+ animateMove: 'wan_v2.2-14b-fp8_animate-move_lightx2v',
96
+ animateReplace: 'wan_v2.2-14b-fp8_animate-replace_lightx2v'
97
+ },
98
+ audio: {
99
+ aceStepTurbo: 'ace_step_1.5_turbo',
100
+ aceStepSft: 'ace_step_1.5_sft'
101
+ }
102
+ } as const;
103
+
104
+ const GPT_IMAGE_MODEL_ALIASES = [
105
+ 'chatgpt',
106
+ 'chatgpt-image',
107
+ 'chat-gpt',
108
+ 'chat-gpt-image',
109
+ 'openai',
110
+ 'openai-image',
111
+ 'open-ai',
112
+ 'open-ai-image',
113
+ 'gpt',
114
+ 'gpt-image',
115
+ 'gpt2',
116
+ 'gpt-2',
117
+ 'gpt2-image',
118
+ 'gpt-2-image',
119
+ 'gptimage2',
120
+ 'gpt-image2',
121
+ 'gpt-image-2'
122
+ ];
123
+
124
+ function normalizeSelectorKey(value: string): string {
125
+ return value
126
+ .trim()
127
+ .toLowerCase()
128
+ .replace(/[_\s]+/g, '-');
129
+ }
130
+
131
+ const IMAGE_MODEL_SELECTORS: Record<string, string> = {
132
+ ...Object.fromEntries(
133
+ GPT_IMAGE_MODEL_ALIASES.map((alias) => [alias, PREFERRED_MODEL_IDS.image.gptImage2])
134
+ ),
135
+ 'z-turbo': 'z_image_turbo_bf16',
136
+ 'z-image': 'z_image_bf16',
137
+ 'chroma-v46-flash': 'chroma-v.46-flash_fp8',
138
+ 'chroma-detail': 'chroma-v48-detail-svd_fp8',
139
+ 'flux1-krea': 'flux1-krea-dev_fp8_scaled',
140
+ flux2: PREFERRED_MODEL_IDS.image.flux2,
141
+ 'pony-v7': 'coreml-cyberrealisticPony_v7',
142
+ 'qwen-2512': 'qwen_image_2512_fp8',
143
+ 'qwen-2512-lightning': 'qwen_image_2512_fp8_lightning',
144
+ 'albedo-xl': 'coreml-albedobaseXL_v31Large',
145
+ 'animagine-xl': 'coreml-animagineXL40_v4Opt',
146
+ 'anima-pencil-xl': 'coreml-animaPencilXL_v500',
147
+ 'art-universe-xl': 'coreml-artUniverse_sdxlV60',
148
+ 'hyphoria-real': 'coreml-hyphoriaRealIllu_v05',
149
+ 'analog-madness-xl': 'coreml-analogMadnessSDXL_xl2',
150
+ 'cyberrealistic-xl': 'coreml-cyberrealisticXL_v60',
151
+ 'real-dream-xl': 'coreml-realDream_sdxlPony11',
152
+ 'faetastic-xl': 'coreml-sdxlFaetastic_v24',
153
+ 'zavychroma-xl': 'coreml-zavychromaxl_v80',
154
+ 'pony-faetality': 'coreml-ponyFaetality_v11',
155
+ 'dreamshaper-xl': 'coreml-DreamShaper-XL1-Alpha2'
156
+ };
157
+
158
+ const EDIT_IMAGE_MODEL_SELECTORS: Record<string, string> = {
159
+ ...Object.fromEntries(
160
+ GPT_IMAGE_MODEL_ALIASES.map((alias) => [alias, PREFERRED_MODEL_IDS.image.gptImage2])
161
+ ),
162
+ 'qwen-lightning': 'qwen_image_edit_2511_fp8_lightning',
163
+ qwen: 'qwen_image_edit_2511_fp8',
164
+ flux2: PREFERRED_MODEL_IDS.image.flux2
165
+ };
166
+
167
+ const TEXT_VIDEO_MODEL_SELECTORS: Record<string, string> = {
168
+ ltx23: PREFERRED_MODEL_IDS.video.t2v,
169
+ wan22: 'wan_v2.2-14b-fp8_t2v_lightx2v',
170
+ seedance2: PREFERRED_MODEL_IDS.video.seedanceT2v,
171
+ 'seedance2-fast': PREFERRED_MODEL_IDS.video.seedanceFastT2v
172
+ };
173
+
174
+ const IMAGE_VIDEO_MODEL_SELECTORS: Record<string, string> = {
175
+ ltx23: PREFERRED_MODEL_IDS.video.i2v,
176
+ wan22: 'wan_v2.2-14b-fp8_i2v_lightx2v',
177
+ seedance2: PREFERRED_MODEL_IDS.video.seedanceI2v,
178
+ 'seedance2-fast': PREFERRED_MODEL_IDS.video.seedanceFastI2v
179
+ };
180
+
181
+ const VIDEO_TO_VIDEO_MODEL_SELECTORS: Record<string, string> = {
182
+ ltx23: PREFERRED_MODEL_IDS.video.v2v,
183
+ 'ltx23-v2v': PREFERRED_MODEL_IDS.video.v2v,
184
+ seedance2: PREFERRED_MODEL_IDS.video.seedanceV2v
185
+ };
186
+
187
+ const SOUND_TO_VIDEO_MODEL_SELECTORS: Record<string, string> = {
188
+ 'wan-s2v': PREFERRED_MODEL_IDS.video.s2v,
189
+ seedance2: PREFERRED_MODEL_IDS.video.seedanceIa2v,
190
+ 'ltx23-ia2v': PREFERRED_MODEL_IDS.video.ia2v,
191
+ 'ltx23-a2v': PREFERRED_MODEL_IDS.video.a2v
192
+ };
193
+
194
+ const MUSIC_MODEL_SELECTORS: Record<string, string> = {
195
+ turbo: PREFERRED_MODEL_IDS.audio.aceStepTurbo,
196
+ sft: PREFERRED_MODEL_IDS.audio.aceStepSft
197
+ };
198
+
199
+ export function clampVariationCount(value: unknown, fallback = 1): number {
200
+ const count = typeof value === 'number' && Number.isFinite(value) ? value : fallback;
201
+ return Math.max(1, Math.min(16, Math.round(count)));
202
+ }
203
+
204
+ export function isNonEmptyString(value: unknown): value is string {
205
+ return typeof value === 'string' && value.trim().length > 0;
206
+ }
207
+
208
+ export function asStringArray(value: unknown): string[] {
209
+ if (!Array.isArray(value)) {
210
+ return [];
211
+ }
212
+ return value.filter(isNonEmptyString);
213
+ }
214
+
215
+ export function asFiniteNumber(value: unknown): number | undefined {
216
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
217
+ }
218
+
219
+ export function asBooleanValue(value: unknown): boolean | undefined {
220
+ return typeof value === 'boolean' ? value : undefined;
221
+ }
222
+
223
+ export function normalizeTimeSignature(value: unknown): string | undefined {
224
+ if (typeof value === 'string' && value.length > 0) {
225
+ return value;
226
+ }
227
+ if (typeof value === 'number' && Number.isFinite(value)) {
228
+ return String(Math.round(value));
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ export function normalizeVideoControlMode(value: unknown): VideoControlMode {
234
+ switch (value) {
235
+ case 'animate-replace':
236
+ case 'seedance-v2v':
237
+ case 'canny':
238
+ case 'pose':
239
+ case 'depth':
240
+ case 'detailer':
241
+ return value;
242
+ default:
243
+ return 'animate-move';
244
+ }
245
+ }
246
+
247
+ export function getHostedVariationCount(
248
+ args: Record<string, unknown>,
249
+ fallback: unknown = 1
250
+ ): number {
251
+ if (args.number_of_variations !== undefined) {
252
+ return clampVariationCount(args.number_of_variations);
253
+ }
254
+ return clampVariationCount(fallback, 1);
255
+ }
256
+
257
+ export function resolveHostedToolModelSelector(
258
+ toolName: string,
259
+ args: Record<string, unknown>
260
+ ): string | undefined {
261
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
262
+ return undefined;
263
+ }
264
+
265
+ const requestedModel = isNonEmptyString(args.model) ? args.model : undefined;
266
+ if (!requestedModel) {
267
+ return undefined;
268
+ }
269
+
270
+ let selectors: Record<string, string> | null = null;
271
+ switch (toolName) {
272
+ case 'sogni_generate_image':
273
+ selectors = IMAGE_MODEL_SELECTORS;
274
+ break;
275
+ case 'sogni_edit_image':
276
+ selectors = EDIT_IMAGE_MODEL_SELECTORS;
277
+ break;
278
+ case 'sogni_generate_video':
279
+ selectors =
280
+ isNonEmptyString(args.reference_image_url) || isNonEmptyString(args.reference_image_end_url)
281
+ ? IMAGE_VIDEO_MODEL_SELECTORS
282
+ : TEXT_VIDEO_MODEL_SELECTORS;
283
+ break;
284
+ case 'sogni_sound_to_video':
285
+ selectors = SOUND_TO_VIDEO_MODEL_SELECTORS;
286
+ break;
287
+ case 'sogni_video_to_video':
288
+ selectors = VIDEO_TO_VIDEO_MODEL_SELECTORS;
289
+ break;
290
+ case 'sogni_generate_music':
291
+ selectors = MUSIC_MODEL_SELECTORS;
292
+ break;
293
+ default:
294
+ return requestedModel;
295
+ }
296
+
297
+ return (
298
+ selectors[requestedModel] ?? selectors[normalizeSelectorKey(requestedModel)] ?? requestedModel
299
+ );
300
+ }
301
+
302
+ function matchesType(value: unknown, type: string): boolean {
303
+ switch (type) {
304
+ case 'array':
305
+ return Array.isArray(value);
306
+ case 'integer':
307
+ return typeof value === 'number' && Number.isInteger(value);
308
+ case 'number':
309
+ return typeof value === 'number' && Number.isFinite(value);
310
+ case 'boolean':
311
+ return typeof value === 'boolean';
312
+ case 'object':
313
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
314
+ case 'string':
315
+ return typeof value === 'string';
316
+ case 'null':
317
+ return value === null;
318
+ default:
319
+ return true;
320
+ }
321
+ }
322
+
323
+ function typeLabel(type: string | string[] | undefined): string {
324
+ if (Array.isArray(type)) return type.join(' or ');
325
+ return type ?? 'valid value';
326
+ }
327
+
328
+ function typeList(type: string | string[] | undefined): string[] {
329
+ if (!type) return [];
330
+ return Array.isArray(type) ? type : [type];
331
+ }
332
+
333
+ function formatEnum(values: unknown[]): string {
334
+ return values.map((value) => JSON.stringify(value)).join(', ');
335
+ }
336
+
337
+ export function validateHostedToolArguments(
338
+ tools: HostedToolDefinition[],
339
+ toolName: string,
340
+ args: Record<string, unknown>,
341
+ options: ValidateHostedToolArgumentsOptions = {}
342
+ ): { ok: boolean; errors: string[] } {
343
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
344
+ return {
345
+ ok: false,
346
+ errors: ['Tool arguments must be a JSON object']
347
+ };
348
+ }
349
+
350
+ const tool = tools.find((candidate) => candidate.function.name === toolName);
351
+ if (!tool) {
352
+ return {
353
+ ok: false,
354
+ errors: [`Unknown hosted Sogni tool "${toolName}"`]
355
+ };
356
+ }
357
+
358
+ const schema = tool.function.parameters;
359
+ if (!schema) {
360
+ return { ok: true, errors: [] };
361
+ }
362
+
363
+ const errors: string[] = [];
364
+ const properties = schema.properties ?? {};
365
+ const skipEnumProperties = new Set(options.skipEnumProperties ?? ['model']);
366
+
367
+ for (const required of schema.required ?? []) {
368
+ if (args[required] === undefined || args[required] === null) {
369
+ errors.push(`Missing required argument "${required}"`);
370
+ }
371
+ }
372
+
373
+ for (const [name, value] of Object.entries(args)) {
374
+ if (value === undefined || value === null) continue;
375
+
376
+ const property = properties[name];
377
+ if (!property) continue;
378
+
379
+ const allowedTypes = typeList(property.type);
380
+ if (allowedTypes.length > 0 && !allowedTypes.some((type) => matchesType(value, type))) {
381
+ errors.push(`Argument "${name}" must be ${typeLabel(property.type)}`);
382
+ continue;
383
+ }
384
+
385
+ if (
386
+ property.enum &&
387
+ !skipEnumProperties.has(name) &&
388
+ !property.enum.some((candidate) => candidate === value)
389
+ ) {
390
+ errors.push(`Argument "${name}" must be one of ${formatEnum(property.enum)}`);
391
+ }
392
+
393
+ if (Array.isArray(value) && property.items?.type) {
394
+ const itemTypes = typeList(property.items.type);
395
+ value.forEach((item, index) => {
396
+ if (!itemTypes.some((type) => matchesType(item, type))) {
397
+ errors.push(`Argument "${name}[${index}]" must be ${typeLabel(property.items?.type)}`);
398
+ }
399
+ });
400
+ }
401
+ }
402
+
403
+ return {
404
+ ok: errors.length === 0,
405
+ errors
406
+ };
407
+ }
408
+
409
+ export function assertHostedToolArguments(
410
+ tools: HostedToolDefinition[],
411
+ toolName: string,
412
+ args: Record<string, unknown>,
413
+ options?: ValidateHostedToolArgumentsOptions
414
+ ): void {
415
+ const result = validateHostedToolArguments(tools, toolName, args, options);
416
+ if (!result.ok) {
417
+ throw new Error(`Invalid ${toolName} arguments: ${result.errors.join('; ')}`);
418
+ }
419
+ }
420
+
421
+ function isObject(value: unknown): value is Record<string, unknown> {
422
+ return typeof value === 'object' && value !== null;
423
+ }
424
+
425
+ function nonEmptyString(value: unknown): string | null {
426
+ return typeof value === 'string' && value.trim().length > 0 ? value : null;
427
+ }
428
+
429
+ function safeJsonStringify(value: unknown): string | null {
430
+ const seen = new WeakSet<object>();
431
+ try {
432
+ return JSON.stringify(value, (_key, nestedValue: unknown) => {
433
+ if (typeof nestedValue === 'function') {
434
+ return `[Function ${(nestedValue as { name?: string }).name || 'anonymous'}]`;
435
+ }
436
+ if (isObject(nestedValue)) {
437
+ if (seen.has(nestedValue)) {
438
+ return '[Circular]';
439
+ }
440
+ seen.add(nestedValue);
441
+ }
442
+ return nestedValue;
443
+ });
444
+ } catch {
445
+ return null;
446
+ }
447
+ }
448
+
449
+ export function serializeUnknownError(error: unknown, fallback = 'Unknown error'): string {
450
+ if (error instanceof Error) {
451
+ return nonEmptyString(error.message) ?? error.name ?? fallback;
452
+ }
453
+
454
+ const directString = nonEmptyString(error);
455
+ if (directString) {
456
+ return directString;
457
+ }
458
+
459
+ if (!isObject(error)) {
460
+ return String(error ?? fallback);
461
+ }
462
+
463
+ for (const key of ['message', 'errorMessage', 'reason', 'description']) {
464
+ const message = nonEmptyString(error[key]);
465
+ if (message) return message;
466
+ }
467
+
468
+ const nestedError = error.error;
469
+ if (nestedError !== undefined && nestedError !== error) {
470
+ const nestedMessage = serializeUnknownError(nestedError, '');
471
+ if (nestedMessage) return nestedMessage;
472
+ }
473
+
474
+ const nestedCause = error.cause;
475
+ if (nestedCause !== undefined && nestedCause !== error) {
476
+ const nestedMessage = serializeUnknownError(nestedCause, '');
477
+ if (nestedMessage) return nestedMessage;
478
+ }
479
+
480
+ return safeJsonStringify(error) ?? fallback;
481
+ }
482
+
483
+ export function isEditImageModel(modelId: string): boolean {
484
+ return (
485
+ modelId === PREFERRED_MODEL_IDS.image.gptImage2 ||
486
+ modelId.startsWith('qwen_image_edit_') ||
487
+ modelId.startsWith('flux2_') ||
488
+ modelId.includes('kontext')
489
+ );
490
+ }
491
+
492
+ const SEEDANCE_CANONICAL_WORKFLOWS: VideoWorkflow[] = ['t2v', 'i2v', 'ia2v', 'v2v'];
493
+
494
+ function getCompatibleVideoWorkflows(modelId: string): VideoWorkflow[] {
495
+ if (modelId === 'seedance-2-0' || modelId === 'seedance-2-0-fast') {
496
+ return SEEDANCE_CANONICAL_WORKFLOWS;
497
+ }
498
+ const workflow = getVideoWorkflowType(modelId);
499
+ return workflow ? [workflow] : [];
500
+ }
501
+
502
+ export function filterVideoModelsByWorkflow(
503
+ availableModels: Array<{ id: string; media?: string }>,
504
+ workflows: VideoWorkflow[]
505
+ ): string[] {
506
+ return availableModels
507
+ .filter((model) => model.media === 'video')
508
+ .filter((model) => {
509
+ const compatibleWorkflows = getCompatibleVideoWorkflows(model.id);
510
+ return compatibleWorkflows.some((workflow) => workflows.includes(workflow));
511
+ })
512
+ .map((model) => model.id);
513
+ }
514
+
515
+ export function getVideoDefaults(modelId: string): { width: number; height: number; fps: number } {
516
+ const workflow = getVideoWorkflowType(modelId);
517
+ const isLtx2 = modelId.startsWith('ltx2-') || modelId.startsWith('ltx23-');
518
+ const isSeedance = modelId.startsWith('seedance-2-0');
519
+
520
+ if (workflow === 's2v' || workflow === 'animate-move' || workflow === 'animate-replace') {
521
+ return { width: 832, height: 480, fps: 16 };
522
+ }
523
+ if (modelId.includes('seedance-2-0-fast')) {
524
+ return { width: 1280, height: 720, fps: 24 };
525
+ }
526
+ if (isSeedance) {
527
+ return { width: 1920, height: 1088, fps: 24 };
528
+ }
529
+ if (isLtx2) {
530
+ return { width: 1920, height: 1088, fps: 24 };
531
+ }
532
+ return { width: 848, height: 480, fps: 16 };
533
+ }
534
+
535
+ function workerCount(model: BackboneAvailableModel): number {
536
+ return typeof model.workerCount === 'number' && Number.isFinite(model.workerCount)
537
+ ? model.workerCount
538
+ : 0;
539
+ }
540
+
541
+ export function selectBackboneModel(
542
+ models: BackboneAvailableModel[],
543
+ options: SelectBackboneModelInput
544
+ ): SelectedBackboneModel {
545
+ const byMedia = models.filter((model) => model.media === options.mediaType);
546
+ if (byMedia.length === 0) {
547
+ throw new Error(`No ${options.mediaType} models currently available on the network`);
548
+ }
549
+
550
+ const compatible = byMedia.filter((model) => {
551
+ if (options.filter && !options.filter(model.id)) {
552
+ return false;
553
+ }
554
+ if (options.workflows) {
555
+ const compatibleWorkflows = getCompatibleVideoWorkflows(model.id);
556
+ return compatibleWorkflows.some((workflow) => options.workflows?.includes(workflow));
557
+ }
558
+ return true;
559
+ });
560
+
561
+ if (options.requestedModel) {
562
+ const requested = compatible.find((model) => model.id === options.requestedModel);
563
+ if (requested) {
564
+ return {
565
+ modelId: requested.id,
566
+ model: requested,
567
+ selectedBy: 'requestedModel'
568
+ };
569
+ }
570
+ }
571
+
572
+ if (compatible.length === 0) {
573
+ if (options.workflows) {
574
+ throw new Error(
575
+ `No compatible ${options.mediaType} models available for workflows: ${options.workflows.join(', ')}`
576
+ );
577
+ }
578
+ throw new Error(`No compatible ${options.mediaType} models currently available on the network`);
579
+ }
580
+
581
+ if (options.preferredModelIds) {
582
+ for (const preferredId of options.preferredModelIds) {
583
+ const preferred = compatible
584
+ .filter((model) => model.id === preferredId)
585
+ .sort((a, b) => workerCount(b) - workerCount(a))[0];
586
+ if (preferred) {
587
+ return {
588
+ modelId: preferred.id,
589
+ model: preferred,
590
+ selectedBy: 'preferredModel'
591
+ };
592
+ }
593
+ }
594
+ }
595
+
596
+ const selected = [...compatible].sort((a, b) => workerCount(b) - workerCount(a))[0];
597
+ return {
598
+ modelId: selected.id,
599
+ model: selected,
600
+ selectedBy: 'workerCount'
601
+ };
602
+ }