@sogni-ai/sogni-client 5.72.1 → 5.74.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 (52) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +59 -0
  3. package/dist/Account/index.d.ts +5 -0
  4. package/dist/Account/index.js +50 -23
  5. package/dist/Account/index.js.map +1 -1
  6. package/dist/Account/subscription.types.d.ts +2 -0
  7. package/dist/Account/types.d.ts +9 -0
  8. package/dist/ApiClient/WebSocketClient/events.d.ts +6 -0
  9. package/dist/Projects/Job.d.ts +24 -0
  10. package/dist/Projects/Job.js +38 -18
  11. package/dist/Projects/Job.js.map +1 -1
  12. package/dist/Projects/createJobRequestMessage.d.ts +12 -0
  13. package/dist/Projects/createJobRequestMessage.js +80 -0
  14. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  15. package/dist/Projects/index.d.ts +4 -1
  16. package/dist/Projects/index.js +23 -7
  17. package/dist/Projects/index.js.map +1 -1
  18. package/dist/Projects/recovery.js +2 -0
  19. package/dist/Projects/recovery.js.map +1 -1
  20. package/dist/Projects/types/RawProject.d.ts +7 -0
  21. package/dist/Projects/types/events.d.ts +6 -0
  22. package/dist/Projects/types/index.d.ts +52 -0
  23. package/dist/Projects/types/index.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/dist-esm/Account/index.js +50 -23
  27. package/dist-esm/Account/index.js.map +1 -1
  28. package/dist-esm/Projects/Job.js +37 -18
  29. package/dist-esm/Projects/Job.js.map +1 -1
  30. package/dist-esm/Projects/createJobRequestMessage.js +79 -0
  31. package/dist-esm/Projects/createJobRequestMessage.js.map +1 -1
  32. package/dist-esm/Projects/index.js +25 -9
  33. package/dist-esm/Projects/index.js.map +1 -1
  34. package/dist-esm/Projects/recovery.js +2 -0
  35. package/dist-esm/Projects/recovery.js.map +1 -1
  36. package/dist-esm/Projects/types/index.js.map +1 -1
  37. package/dist-esm/version.js +1 -1
  38. package/llms-full.txt +48 -0
  39. package/llms.txt +2 -0
  40. package/package.json +3 -1
  41. package/src/Account/index.ts +51 -4
  42. package/src/Account/subscription.types.ts +2 -0
  43. package/src/Account/types.ts +10 -0
  44. package/src/ApiClient/WebSocketClient/events.ts +6 -0
  45. package/src/Projects/Job.ts +62 -1
  46. package/src/Projects/createJobRequestMessage.ts +90 -0
  47. package/src/Projects/index.ts +24 -4
  48. package/src/Projects/recovery.ts +1 -0
  49. package/src/Projects/types/RawProject.ts +7 -0
  50. package/src/Projects/types/events.ts +6 -0
  51. package/src/Projects/types/index.ts +52 -0
  52. package/src/version.ts +1 -1
package/llms-full.txt CHANGED
@@ -963,6 +963,49 @@ const project = await sogni.projects.create({
963
963
  });
964
964
  ```
965
965
 
966
+ SEEDANCE 2.5 DRAFT -> 1080P FINAL:
967
+
968
+ Draft mode is chosen at submission; a normal job cannot be finalized. A draft
969
+ (`seedanceDraft: true`) takes every normal Seedance 2.5 input at a 480p size and
970
+ costs the same as that 480p job. The completed job reports `job.seedanceDraft`
971
+ and `job.seedanceDraftExpiresAt` (ISO 8601). Finalize it with
972
+ `seedanceDraftJobId: draftJob.id` on the same model and account before it
973
+ expires: the final reuses the draft's prompt, media, duration, aspect ratio,
974
+ seed and audio setting, so send none of them (no references, frames, audio,
975
+ `duration`, `frames`, `ratio`, `seed`, `generateAudio` or `seedanceTaskType`),
976
+ use `numberOfMedia: 1` and an empty `positivePrompt`. `outputFormat` and
977
+ `returnLastFrame` may be set. The output is the draft's aspect ratio at 1080p,
978
+ priced at 1080p for the draft's duration (video-input rate when the draft had
979
+ one); each finalize bills again. The final reports `job.seedanceDraftJobId`.
980
+ `estimateVideoCost` accepts both fields; for a finalize the server prices from
981
+ the draft, so width, height, duration and fps do not change the quote.
982
+
983
+ ```javascript
984
+ const draft = await sogni.projects.create({
985
+ type: 'video',
986
+ network: 'fast',
987
+ modelId: 'seedance-2-5',
988
+ positivePrompt: 'A lighthouse keeper climbs a spiral stair as a storm rolls in',
989
+ numberOfMedia: 1,
990
+ duration: 8,
991
+ width: 864,
992
+ height: 496,
993
+ seedanceDraft: true
994
+ });
995
+ await draft.waitForCompletion();
996
+ const [draftJob] = draft.jobs; // draftJob.seedanceDraft === true
997
+
998
+ const final = await sogni.projects.create({
999
+ type: 'video',
1000
+ network: 'fast',
1001
+ modelId: 'seedance-2-5',
1002
+ positivePrompt: '',
1003
+ numberOfMedia: 1,
1004
+ seedanceDraftJobId: draftJob.id
1005
+ });
1006
+ await final.waitForCompletion(); // final.jobs[0].seedanceDraftJobId === draftJob.id
1007
+ ```
1008
+
966
1009
  SEEDANCE IMAGE-TO-VIDEO:
967
1010
 
968
1011
  ```javascript
@@ -2274,6 +2317,8 @@ VIDEO-SPECIFIC PARAMETERS:
2274
2317
  referenceVideoUrls?: string[]; // Seedance only
2275
2318
  referenceAudioUrls?: string[]; // Seedance only
2276
2319
  seedanceTaskType?: 'reference' | 'edit' | 'extend'; // Seedance 2.5 loose-reference operation
2320
+ seedanceDraft?: boolean; // Seedance 2.5: 480p draft that can be finalized at 1080p
2321
+ seedanceDraftJobId?: string; // Seedance 2.5: finalize that draft (its job id) at 1080p; inherits its inputs
2277
2322
  audioStart?: number; // Seconds into audio
2278
2323
  audioDuration?: number; // Seconds of audio
2279
2324
  videoStart?: number; // Seconds into video
@@ -2828,3 +2873,6 @@ For additional resources:
2828
2873
  - Examples: https://github.com/Sogni-AI/sogni-client/tree/main/examples
2829
2874
  - API Docs: https://sdk-docs.sogni.ai
2830
2875
  - Support: https://www.sogni.ai/support
2876
+
2877
+ OPTIONAL PRIORITY MODE
2878
+ Paid Unlimited members may set priorityMode: true on projects.create() with network: fast, tokenType: spark and billingMode: subscription. Earlier Fast queue placement costs 1.5x daily and monthly usage credits, behind Premium Spark. Plan caps and concurrency are unchanged; rendering speed, quality and GPU assignment are not guaranteed to change. Omit the option for standard behavior. Trials, Relaxed, token-paid generations and third-party vendor models cannot use it. Image, video and audio estimate helpers also accept priorityMode and include the weighted daily share in dailyFairUsePct. This setting applies to media generations, not chat or prompt-writing calls.
package/llms.txt CHANGED
@@ -482,6 +482,8 @@ Use `referenceImageUrls`, `referenceVideoUrls`, and `referenceAudioUrls` for See
482
482
 
483
483
  For Seedance 2.5 loose-reference video operations, set `seedanceTaskType` to `reference`, `edit`, or `extend`. Edit/extend require a reference video. The server applies adaptive source aspect ratio to first/last-frame, edit, and extend requests and provider-selected duration to edit requests. Loose-reference requests require an explicit task type.
484
484
 
485
+ Seedance 2.5 draft mode: send `seedanceDraft: true` with a 480p size for a draft priced like the same 480p job (choose it at submission; a normal job cannot be finalized). The completed job reports `job.seedanceDraft` and `job.seedanceDraftExpiresAt` (ISO 8601). Finalize it at 1080p with `seedanceDraftJobId: draftJob.id` on the same model, `numberOfMedia: 1` and an empty `positivePrompt`: the final reuses the draft's prompt, media, duration, aspect ratio, seed and audio setting, so omit references, frames, audio, `duration`, `frames`, `ratio`, `seed`, `generateAudio` and `seedanceTaskType`. The final reports `job.seedanceDraftJobId`. `estimateVideoCost` accepts both fields; the server prices a finalize from the draft. The two fields are mutually exclusive.
486
+
485
487
  Aliases used by chat tools and examples: `seedance2`, `seedance2-mini`, `seedance2-5`, `seedance2-5-uncensored`, and `seedance2-mini-uncensored`. V2V uses `seedance2` or `seedance2-mini` as the model selector and `seedance-v2v` as the control mode. Seedance Mini is the preferred lower-cost 720p tier. See `examples/workflow_partner_seedance_video.mjs` for focused endpoint coverage across Seedance T2V/I2V/IA2V/V2V and multimodal context prompt expansion.
486
488
 
487
489
  ### Happy Horse 1.1 (external API)
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "5.72.1",
6
+ "version": "5.74.0",
7
7
  "engines": {
8
8
  "node": ">=22"
9
9
  },
@@ -70,6 +70,7 @@
70
70
  "test:model-options": "npm run build && node scripts/check-video-model-options.cjs",
71
71
  "test:video-estimates": "npm run build && node scripts/check-video-estimates.cjs",
72
72
  "test:seedance-task-type": "npm run build && node scripts/check-seedance-task-type.cjs && node scripts/check-seedance-r2v-example.cjs",
73
+ "test:seedance-draft": "npm run build && node scripts/check-seedance-draft.cjs",
73
74
  "test:model-consent-error": "npm run build && node scripts/check-model-consent-error.cjs",
74
75
  "test:wan3-video": "npm run build && node scripts/check-wan3-video.cjs",
75
76
  "test:sam3-image-segment": "npm run build && node scripts/check-sam3-image-segment.cjs",
@@ -81,6 +82,7 @@
81
82
  "test:capability-exports": "npm run build && node scripts/check-capability-exports.cjs",
82
83
  "test:generation-receipt-transport": "npm run build && node scripts/check-generation-receipt-transport.cjs",
83
84
  "test:prompt-expansion": "npm run build && node scripts/check-prompt-expansion-transport.cjs",
85
+ "test:priority-mode": "npm run build && node scripts/check-priority-mode.cjs && node scripts/check-priority-capability.cjs",
84
86
  "test:reference-content-types": "npm run build && node scripts/check-reference-content-types.cjs",
85
87
  "test:minimax-h3-audio-policy": "node scripts/check-minimax-h3-source-audio-policy.cjs",
86
88
  "test:minimax-h3-balanced": "npm run build && node scripts/check-minimax-h3-balanced.cjs",
@@ -102,6 +102,11 @@ class AccountApi extends ApiGroup {
102
102
  */
103
103
  private appliedSubscriptionSocketWrites = 0;
104
104
  private accountSessionVersion?: number;
105
+ /** Priority availability belongs to the current authenticated connection. */
106
+ private socketPriorityAvailability: {
107
+ sessionVersion: number;
108
+ available: boolean;
109
+ } | null = null;
105
110
 
106
111
  constructor(config: ApiConfig) {
107
112
  super(config);
@@ -133,6 +138,7 @@ class AccountApi extends ApiGroup {
133
138
  }
134
139
 
135
140
  private handleServerConnected({ network }: { network: SupernetType }) {
141
+ this.clearSocketPriorityAvailability();
136
142
  this.currentAccount._update({
137
143
  networkStatus: 'connected',
138
144
  network
@@ -140,6 +146,7 @@ class AccountApi extends ApiGroup {
140
146
  }
141
147
 
142
148
  private handleServerConnecting({ network }: { network: SupernetType }) {
149
+ this.clearSocketPriorityAvailability();
143
150
  this.currentAccount._update({
144
151
  networkStatus: 'connecting',
145
152
  network
@@ -147,12 +154,41 @@ class AccountApi extends ApiGroup {
147
154
  }
148
155
 
149
156
  private handleServerDisconnected() {
157
+ this.clearSocketPriorityAvailability();
150
158
  this.currentAccount._update({
151
159
  networkStatus: 'disconnected',
152
160
  network: null
153
161
  });
154
162
  }
155
163
 
164
+ private clearSocketPriorityAvailability() {
165
+ this.socketPriorityAvailability = null;
166
+ const subscription = this.currentAccount.subscription;
167
+ if (!subscription?.capabilities?.priorityMode) return;
168
+ const capabilities = { ...subscription.capabilities };
169
+ delete capabilities.priorityMode;
170
+ this.currentAccount._update({ subscription: { ...subscription, capabilities } });
171
+ }
172
+
173
+ /** Plan refreshes retain the current connection's coarse Priority availability. */
174
+ private withSocketPriorityAvailability(
175
+ subscription: SubscriptionEntitlementSnapshot
176
+ ): SubscriptionEntitlementSnapshot {
177
+ const available = Boolean(
178
+ this.socketPriorityAvailability?.available &&
179
+ this.socketPriorityAvailability.sessionVersion === this.client.auth.sessionVersion &&
180
+ this.client.socket.isConnected &&
181
+ subscription.active &&
182
+ subscription.status !== 'trialing' &&
183
+ (subscription.tier === 'unlimited' || subscription.tier === 'unlimited_pro')
184
+ );
185
+ if (available === (subscription.capabilities?.priorityMode === true)) return subscription;
186
+ const capabilities = { ...subscription.capabilities };
187
+ if (available) capabilities.priorityMode = true;
188
+ else delete capabilities.priorityMode;
189
+ return { ...subscription, capabilities };
190
+ }
191
+
156
192
  private mapSocketSubscriptionEntitlement(
157
193
  data: SocketSubscriptionEntitlementData | undefined
158
194
  ): SubscriptionEntitlementSnapshot | undefined {
@@ -289,12 +325,17 @@ class AccountApi extends ApiGroup {
289
325
  }
290
326
  if (source === 'socket') {
291
327
  this.appliedSubscriptionSocketWrites += 1;
328
+ this.socketPriorityAvailability = {
329
+ sessionVersion: this.client.auth.sessionVersion,
330
+ available: subscription.capabilities?.priorityMode === true
331
+ };
292
332
  }
333
+ const normalizedSubscription = this.withSocketPriorityAvailability(subscription);
293
334
  const current = this.currentAccount.subscription;
294
- if (current && JSON.stringify(current) === JSON.stringify(subscription)) {
335
+ if (current && JSON.stringify(current) === JSON.stringify(normalizedSubscription)) {
295
336
  return true;
296
337
  }
297
- this.currentAccount._update({ subscription });
338
+ this.currentAccount._update({ subscription: normalizedSubscription });
298
339
  return true;
299
340
  }
300
341
 
@@ -355,6 +396,7 @@ class AccountApi extends ApiGroup {
355
396
  this.accountSessionVersion = this.client.auth.sessionVersion;
356
397
  this.lastAppliedSubscriptionVersion = null;
357
398
  this.appliedSubscriptionSocketWrites = 0;
399
+ this.socketPriorityAvailability = null;
358
400
  this.currentAccount._clear();
359
401
  }
360
402
 
@@ -364,6 +406,7 @@ class AccountApi extends ApiGroup {
364
406
  // the next login starts from a clean slate.
365
407
  this.lastAppliedSubscriptionVersion = null;
366
408
  this.appliedSubscriptionSocketWrites = 0;
409
+ this.socketPriorityAvailability = null;
367
410
  this.currentAccount._clear();
368
411
  } else {
369
412
  this.clearChangedAccountSession();
@@ -699,6 +742,7 @@ class AccountApi extends ApiGroup {
699
742
  if (params.provider) {
700
743
  query.provider = params.provider;
701
744
  }
745
+ if (params.includePriorityAccounting === true) query.includePriorityAccounting = 'true';
702
746
  const res = await this.client.rest.get<ApiResponse<TxHistoryData>>(
703
747
  '/v1/transactions/list',
704
748
  query
@@ -723,7 +767,10 @@ class AccountApi extends ApiGroup {
723
767
  paymentModel: tx.paymentModel,
724
768
  subscriptionTier: tx.subscriptionTier,
725
769
  subscriptionTrialing: tx.subscriptionTrialing,
726
- subscriptionThrottled: tx.subscriptionThrottled
770
+ subscriptionThrottled: tx.subscriptionThrottled,
771
+ ...(params.includePriorityAccounting === true && tx.priorityUsage
772
+ ? { priorityUsage: tx.priorityUsage }
773
+ : {})
727
774
  })
728
775
  ),
729
776
  next: {
@@ -965,7 +1012,7 @@ class AccountApi extends ApiGroup {
965
1012
  // currentAccount.subscription.
966
1013
  return this.currentAccount.subscription;
967
1014
  }
968
- return subscription;
1015
+ return this.currentAccount.subscription ?? subscription;
969
1016
  }
970
1017
 
971
1018
  /**
@@ -46,6 +46,8 @@ export type SubscriptionTerm = 'monthly' | 'annual';
46
46
  /** Actionable monthly fair-use state returned only while Fast limits are active. */
47
47
  export interface SubscriptionFairUseState {
48
48
  limited: true;
49
+ /** Temporary accounting confirmation, rather than confirmed exhausted capacity. */
50
+ accountingStatus?: 'pending';
49
51
  /** Subscription-funded Spark used in the current monthly fair-use window. */
50
52
  usageSpark: number;
51
53
  /** Retail-equivalent USD value of the subscription-funded usage. */
@@ -97,6 +97,14 @@ export interface TxHistoryParams {
97
97
  limit: number;
98
98
  provider?: string;
99
99
  offset?: number;
100
+ /** Include saved Priority labels on plan usage entries. */
101
+ includePriorityAccounting?: boolean;
102
+ }
103
+
104
+ export interface PriorityUsageSummary {
105
+ outputCount: number;
106
+ rate: 1.5;
107
+ coverage: 'complete' | 'partial';
100
108
  }
101
109
 
102
110
  export interface TxHistoryData {
@@ -105,6 +113,7 @@ export interface TxHistoryData {
105
113
  }
106
114
 
107
115
  export interface TxRaw {
116
+ priorityUsage?: PriorityUsageSummary;
108
117
  _id: string;
109
118
  id: string;
110
119
  SID: number;
@@ -130,6 +139,7 @@ export interface TxRaw {
130
139
  }
131
140
 
132
141
  export interface TxHistoryEntry {
142
+ priorityUsage?: PriorityUsageSummary;
133
143
  id: string;
134
144
  address: string;
135
145
  createTime: Date;
@@ -292,6 +292,12 @@ export type JobResultData = {
292
292
  lastFrameUrl?: string;
293
293
  lastFrameKey?: string;
294
294
  outputFormat?: string;
295
+ /** Seedance 2.5: this job is a draft that can be finalized at 1080p. */
296
+ seedanceDraft?: boolean;
297
+ /** Seedance 2.5 draft: ISO 8601 time after which it can no longer be finalized. */
298
+ seedanceDraftExpiresAt?: string;
299
+ /** Seedance 2.5 final: the Sogni job id of the draft it finalized. */
300
+ seedanceDraftJobId?: string;
295
301
  /**
296
302
  * Files the worker uploaded for this result, as the worker reported them.
297
303
  * The SDK reads `contentType` to pick the download endpoint for a result
@@ -117,6 +117,41 @@ function directResultUrlFromRawJob(rawJob: RawJob): string | null {
117
117
  );
118
118
  }
119
119
 
120
+ type SeedanceDraftResultFields = Pick<
121
+ JobData,
122
+ 'seedanceDraft' | 'seedanceDraftExpiresAt' | 'seedanceDraftJobId'
123
+ >;
124
+
125
+ function pickSeedanceDraftFields(source: unknown): SeedanceDraftResultFields {
126
+ if (!source || typeof source !== 'object') return {};
127
+ const { seedanceDraft, seedanceDraftExpiresAt, seedanceDraftJobId } = source as Record<
128
+ string,
129
+ unknown
130
+ >;
131
+ return {
132
+ ...(seedanceDraft === true ? { seedanceDraft } : {}),
133
+ ...(typeof seedanceDraftExpiresAt === 'string' && seedanceDraftExpiresAt
134
+ ? { seedanceDraftExpiresAt }
135
+ : {}),
136
+ ...(typeof seedanceDraftJobId === 'string' && seedanceDraftJobId ? { seedanceDraftJobId } : {})
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Seedance 2.5 draft result fields from a socket result or a REST job record,
142
+ * read from the record itself or its allowlisted `result` receipt (the record
143
+ * wins). Only well-formed fields are returned, so a record without them never
144
+ * clears values a job already holds.
145
+ * @internal
146
+ */
147
+ export function seedanceDraftResultFields(record: unknown): SeedanceDraftResultFields {
148
+ if (!record || typeof record !== 'object') return {};
149
+ return {
150
+ ...pickSeedanceDraftFields((record as { result?: unknown }).result),
151
+ ...pickSeedanceDraftFields(record)
152
+ };
153
+ }
154
+
120
155
  function etaProgressPercent(
121
156
  startedAt: Date | undefined,
122
157
  eta: Date | undefined
@@ -156,6 +191,12 @@ export interface JobData {
156
191
  lastFrameUrl?: string;
157
192
  lastFrameKey?: string;
158
193
  outputFormat?: string;
194
+ /** Seedance 2.5: this job is a draft that can be finalized at 1080p. */
195
+ seedanceDraft?: boolean;
196
+ /** Seedance 2.5 draft: ISO 8601 time after which it can no longer be finalized. */
197
+ seedanceDraftExpiresAt?: string;
198
+ /** Seedance 2.5 final: the Sogni job id of the draft it finalized. */
199
+ seedanceDraftJobId?: string;
159
200
  provenance?: JobProvenance;
160
201
  error?: ErrorData;
161
202
  positivePrompt?: string;
@@ -220,6 +261,7 @@ class Job extends DataEntity<JobData, JobEventMap> {
220
261
  lastFrameUrl: rawJob.lastFrameUrl ?? (rawJob.result?.lastFrameUrl as string | undefined),
221
262
  lastFrameKey: rawJob.lastFrameKey ?? (rawJob.result?.lastFrameKey as string | undefined),
222
263
  outputFormat: rawJob.outputFormat ?? (rawJob.result?.outputFormat as string | undefined),
264
+ ...seedanceDraftResultFields(rawJob),
223
265
  provenance: rawJob.result
224
266
  },
225
267
  options
@@ -317,6 +359,24 @@ class Job extends DataEntity<JobData, JobEventMap> {
317
359
  return this.data.lastFrameUrl;
318
360
  }
319
361
 
362
+ /**
363
+ * Seedance 2.5: `true` on a completed draft, which can be finalized at 1080p by
364
+ * passing this job's `id` as `seedanceDraftJobId`. Reported to the owner only.
365
+ */
366
+ get seedanceDraft() {
367
+ return this.data.seedanceDraft;
368
+ }
369
+
370
+ /** Seedance 2.5 draft: ISO 8601 time after which it can no longer be finalized. */
371
+ get seedanceDraftExpiresAt() {
372
+ return this.data.seedanceDraftExpiresAt;
373
+ }
374
+
375
+ /** Seedance 2.5 final: the Sogni job id of the draft it finalized. */
376
+ get seedanceDraftJobId() {
377
+ return this.data.seedanceDraftJobId;
378
+ }
379
+
320
380
  /** Refresh the signed URL for an exported final frame. */
321
381
  async getLastFrameUrl(): Promise<string> {
322
382
  const url = await this._api.mediaDownloadUrl({
@@ -552,7 +612,8 @@ class Job extends DataEntity<JobData, JobEventMap> {
552
612
  ...(data.result ? { provenance: data.result } : {}),
553
613
  lastFrameUrl: data.lastFrameUrl ?? (data.result?.lastFrameUrl as string | undefined),
554
614
  lastFrameKey: data.lastFrameKey ?? (data.result?.lastFrameKey as string | undefined),
555
- outputFormat: data.outputFormat ?? (data.result?.outputFormat as string | undefined)
615
+ outputFormat: data.outputFormat ?? (data.result?.outputFormat as string | undefined),
616
+ ...seedanceDraftResultFields(data)
556
617
  };
557
618
  if (JOB_STATUS_MAP[data.status]) {
558
619
  delta.status = JOB_STATUS_MAP[data.status];
@@ -166,6 +166,8 @@ function validateVideoWorkflowAssets(params: VideoProjectParams): void {
166
166
  if (exportError) {
167
167
  throw new ApiError(400, { status: 'error', errorCode: 0, message: exportError });
168
168
  }
169
+ validateSeedanceDraftFields(params);
170
+ validateSeedanceDraftFinalize(params);
169
171
  validateVideoContextImages(params);
170
172
  validateVideoKeyframes(params);
171
173
  validateVideoReferenceArrays(params);
@@ -250,6 +252,80 @@ function validateVideoWorkflowAssets(params: VideoProjectParams): void {
250
252
  }
251
253
  }
252
254
 
255
+ /** Prompt a finalize request carries; the final always uses its draft's prompt. */
256
+ const SEEDANCE_DRAFT_FINALIZE_PROMPT = 'Finalize the Seedance 2.5 draft at 1080p.';
257
+
258
+ function seedanceDraftError(message: string): never {
259
+ throw new ApiError(400, { status: 'error', errorCode: 0, message });
260
+ }
261
+
262
+ /**
263
+ * Seedance 2.5 draft mode fields, shared by projects and video estimates:
264
+ * `seedanceDraft` creates a 480p draft and `seedanceDraftJobId` finalizes one
265
+ * at 1080p. The server checks ownership, expiry, model and size; this only
266
+ * catches requests no server would accept.
267
+ */
268
+ export function validateSeedanceDraftFields(params: {
269
+ modelId?: string;
270
+ model?: string;
271
+ seedanceDraft?: unknown;
272
+ seedanceDraftJobId?: unknown;
273
+ }): void {
274
+ const { seedanceDraft, seedanceDraftJobId } = params;
275
+ if (seedanceDraft !== undefined && typeof seedanceDraft !== 'boolean') {
276
+ seedanceDraftError('seedanceDraft must be a boolean.');
277
+ }
278
+ if (
279
+ seedanceDraftJobId !== undefined &&
280
+ (typeof seedanceDraftJobId !== 'string' || !seedanceDraftJobId.trim())
281
+ ) {
282
+ seedanceDraftError('seedanceDraftJobId must be the Sogni job id of a Seedance 2.5 draft.');
283
+ }
284
+ if (seedanceDraft !== true && seedanceDraftJobId === undefined) return;
285
+ if (!isSeedance25Model(String(params.modelId ?? params.model ?? ''))) {
286
+ seedanceDraftError('Seedance draft mode is supported only by Seedance 2.5.');
287
+ }
288
+ if (seedanceDraft === true && seedanceDraftJobId !== undefined) {
289
+ seedanceDraftError(
290
+ 'seedanceDraft and seedanceDraftJobId are mutually exclusive: create a draft, or finalize one.'
291
+ );
292
+ }
293
+ }
294
+
295
+ /**
296
+ * A finalize renders an existing draft at 1080p and inherits every
297
+ * generation-defining input from it; the vendor refuses any of them sent again,
298
+ * even with identical values.
299
+ */
300
+ function validateSeedanceDraftFinalize(params: VideoProjectParams): void {
301
+ if (params.seedanceDraftJobId === undefined) return;
302
+ const inherited: Array<[keyof VideoProjectParams, boolean]> = [
303
+ ['referenceImage', Boolean(params.referenceImage)],
304
+ ['referenceImageEnd', Boolean(params.referenceImageEnd)],
305
+ ['referenceImageUrls', asReferenceUrlArray(params.referenceImageUrls).length > 0],
306
+ ['referenceVideo', Boolean(params.referenceVideo)],
307
+ ['referenceVideoUrls', asReferenceUrlArray(params.referenceVideoUrls).length > 0],
308
+ ['referenceAudio', Boolean(params.referenceAudio)],
309
+ ['referenceAudioUrls', asReferenceUrlArray(params.referenceAudioUrls).length > 0],
310
+ ['referenceAudioIdentity', Boolean(params.referenceAudioIdentity)],
311
+ ['duration', params.duration !== undefined],
312
+ ['frames', params.frames !== undefined],
313
+ ['ratio', params.ratio !== undefined],
314
+ ['seed', params.seed !== undefined],
315
+ ['generateAudio', params.generateAudio !== undefined],
316
+ ['seedanceTaskType', params.seedanceTaskType !== undefined]
317
+ ];
318
+ const sent = inherited.filter(([, present]) => present).map(([name]) => name);
319
+ if (sent.length) {
320
+ seedanceDraftError(
321
+ `A Seedance 2.5 finalize reuses its draft's inputs and settings; remove ${sent.join(', ')}.`
322
+ );
323
+ }
324
+ if ((params.numberOfMedia ?? 1) !== 1) {
325
+ seedanceDraftError('A Seedance 2.5 finalize renders its draft once; numberOfMedia must be 1.');
326
+ }
327
+ }
328
+
253
329
  function validateSeedanceTaskType(params: VideoProjectParams): void {
254
330
  const taskType = params.seedanceTaskType;
255
331
  const isSeedance25 = isSeedance25Model(params.modelId);
@@ -1754,6 +1830,15 @@ function applyVideoParams(
1754
1830
  if (params.seedanceTaskType !== undefined) {
1755
1831
  keyFrame.seedanceTaskType = params.seedanceTaskType;
1756
1832
  }
1833
+ if (params.seedanceDraft === true) {
1834
+ keyFrame.seedanceDraft = true;
1835
+ }
1836
+ if (params.seedanceDraftJobId !== undefined) {
1837
+ keyFrame.seedanceDraftJobId = params.seedanceDraftJobId;
1838
+ // The final inherits the draft's prompt. The request still carries a
1839
+ // non-empty prompt, which the server ignores for a finalize.
1840
+ keyFrame.positivePrompt = SEEDANCE_DRAFT_FINALIZE_PROMPT;
1841
+ }
1757
1842
  if (params.audioIdentityStrength !== undefined) {
1758
1843
  keyFrame.identityGuidanceScale = params.audioIdentityStrength;
1759
1844
  }
@@ -2059,6 +2144,11 @@ function createJobRequestMessage(id: string, params: ProjectParams, options: Mod
2059
2144
  if (params.network) {
2060
2145
  jobRequest.network = params.network;
2061
2146
  }
2147
+ if (params.priorityMode !== undefined) {
2148
+ if (typeof params.priorityMode !== 'boolean')
2149
+ throw new Error('Priority mode must be true or false.');
2150
+ jobRequest.priorityMode = params.priorityMode;
2151
+ }
2062
2152
  if (isImageParams(params) && params.embedPromptMetadata !== undefined) {
2063
2153
  if (typeof params.embedPromptMetadata !== 'boolean') {
2064
2154
  throw new Error('embedPromptMetadata must be a boolean');
@@ -41,7 +41,8 @@ import {
41
41
  } from './recovery.js';
42
42
  import createJobRequestMessage, {
43
43
  type JobRequestRaw,
44
- rejectRetiredOutputScale
44
+ rejectRetiredOutputScale,
45
+ validateSeedanceDraftFields
45
46
  } from './createJobRequestMessage.js';
46
47
  import { ApiError, ApiResponse } from '../ApiClient/index.js';
47
48
  import { EstimationResponse } from './types/EstimationResponse.js';
@@ -66,7 +67,7 @@ import type { WaitingReason } from './types/WaitingReason.js';
66
67
  import ErrorData from '../types/ErrorData.js';
67
68
  import { SupernetType } from '../ApiClient/WebSocketClient/types.js';
68
69
  import Cache from '../lib/Cache.js';
69
- import Job, { enhancementDefaults } from './Job.js';
70
+ import Job, { enhancementDefaults, seedanceDraftResultFields } from './Job.js';
70
71
  import {
71
72
  calculateVideoFrames,
72
73
  getEnhacementStrength,
@@ -1035,6 +1036,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1035
1036
  let performedStepCount = data.performedStepCount;
1036
1037
  let seed = data.lastSeed !== undefined ? Number(data.lastSeed) : undefined;
1037
1038
  const provenance = jobProvenanceFromResult(data);
1039
+ const seedanceDraftFields = seedanceDraftResultFields(data);
1038
1040
  if (project) {
1039
1041
  const job = project.job(data.imgID);
1040
1042
  if (job) {
@@ -1053,6 +1055,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1053
1055
  lastFrameUrl: data.lastFrameUrl,
1054
1056
  lastFrameKey: data.lastFrameKey,
1055
1057
  outputFormat: data.outputFormat,
1058
+ // Set with the status, so a `completed` listener can already read them.
1059
+ ...seedanceDraftFields,
1056
1060
  // Unchanged meaning: the server withheld the media. The label for
1057
1061
  // media that WAS delivered is `nsfwDetected`, deliberately kept out of
1058
1062
  // this flag so upgrading the SDK changes no existing app's behaviour.
@@ -1078,6 +1082,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1078
1082
  resultUrl: downloadUrl,
1079
1083
  lastFrameUrl: data.lastFrameUrl,
1080
1084
  outputFormat: data.outputFormat,
1085
+ ...seedanceDraftFields,
1081
1086
  isNSFW: Boolean(data.triggeredNSFWFilter),
1082
1087
  nsfwDetected: data.nsfwDetected === true,
1083
1088
  nsfwSources: Array.isArray(data.nsfwSources) ? [...data.nsfwSources] : [],
@@ -1467,7 +1472,9 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
1467
1472
  delta.seed = event.seed;
1468
1473
  }
1469
1474
  job._update({
1470
- ...delta
1475
+ ...delta,
1476
+ // A job first seen in its own completion still learns it is a draft.
1477
+ ...seedanceDraftResultFields(event)
1471
1478
  });
1472
1479
  break;
1473
1480
  }
@@ -2121,6 +2128,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
2121
2128
  : (job.result as Record<string, unknown> | undefined)?.outputFormat) as
2122
2129
  | string
2123
2130
  | undefined,
2131
+ ...seedanceDraftResultFields(job),
2124
2132
  ...(typeof job.resultUrl === 'string' && job.resultUrl
2125
2133
  ? { resultUrl: job.resultUrl }
2126
2134
  : {}),
@@ -3336,7 +3344,8 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3336
3344
  contextImages,
3337
3345
  gptImageQuality,
3338
3346
  outputFormat,
3339
- billingMode
3347
+ billingMode,
3348
+ priorityMode
3340
3349
  }: EstimateRequest): Promise<CostEstimation> {
3341
3350
  let apiVersion = 2;
3342
3351
  const modelOptions = await this.getModelOptions(model);
@@ -3372,6 +3381,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3372
3381
  if (gptImageQuality) queryParams.set('gptImageQuality', gptImageQuality);
3373
3382
  if (outputFormat) queryParams.set('outputFormat', outputFormat);
3374
3383
  if (billingMode) queryParams.set('billingMode', billingMode);
3384
+ if (priorityMode !== undefined) queryParams.set('priorityMode', String(priorityMode));
3375
3385
  const query = queryParams.toString();
3376
3386
  const r = await this.client.socket.get<EstimationResponse>(
3377
3387
  `/api/v${apiVersion}/job/estimate/${pathParams.join('/')}${query ? `?${query}` : ''}`
@@ -3429,6 +3439,9 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3429
3439
  * - referenceVideoDurationSeconds: Combined duration of MiniMax H3 r2v video input.
3430
3440
  * - keyframeCount / keyframes: MiniMax H3 intermediate keyframes (0-8). The first two are
3431
3441
  * included; each extra keyframe adds 0.75 s of output time on FastH3 and 0.3 s on other tiers.
3442
+ * - seedanceDraft: Seedance 2.5 480p draft, priced like the same 480p job.
3443
+ * - seedanceDraftJobId: Seedance 2.5 finalize of that draft at 1080p, priced by the server from
3444
+ * the draft (duration, aspect ratio, input video); width, height, duration and fps do not change it.
3432
3445
  * @return {Promise<Object>} Returns an object containing the estimated costs for the video in different units:
3433
3446
  * - token: Cost in tokens.
3434
3447
  * - usd: Cost in USD.
@@ -3437,6 +3450,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3437
3450
  */
3438
3451
  async estimateVideoCost(params: VideoEstimateRequest) {
3439
3452
  rejectRetiredOutputScale(params);
3453
+ validateSeedanceDraftFields(params);
3440
3454
  const frames = params.frames
3441
3455
  ? params.frames
3442
3456
  : calculateVideoFrames(params.model, params.duration, params.fps);
@@ -3500,6 +3514,11 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3500
3514
  const network = params.network ?? this._currentNetworkType;
3501
3515
  if (network) query.set('network', network);
3502
3516
  if (params.billingMode) query.set('billingMode', params.billingMode);
3517
+ if (params.priorityMode !== undefined) query.set('priorityMode', String(params.priorityMode));
3518
+ if (params.seedanceDraft === true) query.set('seedanceDraft', '1');
3519
+ if (params.seedanceDraftJobId !== undefined) {
3520
+ query.set('seedanceDraftJobId', params.seedanceDraftJobId);
3521
+ }
3503
3522
  const queryString = query.toString();
3504
3523
  const r = await this.client.socket.get<EstimationResponse>(
3505
3524
  `/api/v1/job-video/estimate/${path}${queryString ? `?${queryString}` : ''}`
@@ -3540,6 +3559,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
3540
3559
  const network = params.network ?? this._currentNetworkType;
3541
3560
  if (network) query.set('network', network);
3542
3561
  if (params.billingMode) query.set('billingMode', params.billingMode);
3562
+ if (params.priorityMode !== undefined) query.set('priorityMode', String(params.priorityMode));
3543
3563
  const queryString = query.toString();
3544
3564
  const r = await this.client.socket.get<EstimationResponse>(
3545
3565
  `/api/v1/job-audio/estimate/${path}${queryString ? `?${queryString}` : ''}`
@@ -121,6 +121,7 @@ export function projectParamsFromRecoveredProject(project: RecoveredProject): Pr
121
121
  if (tokenType) base.tokenType = tokenType;
122
122
  const billingMode = project.billingMode || request.billingMode;
123
123
  if (billingMode) base.billingMode = billingMode;
124
+ if (project.priorityMode !== undefined) base.priorityMode = project.priorityMode;
124
125
  if (request.disableSafety === true) base.disableNSFWFilter = true;
125
126
  if (typeof request.outputFormat === 'string' && request.outputFormat) {
126
127
  base.outputFormat = request.outputFormat;
@@ -5,6 +5,7 @@ import type { WaitingReason, JobWaitingReason } from './WaitingReason.js';
5
5
  export interface RawProject {
6
6
  /** Missing on older projects or clients that do not report prompt preparation. */
7
7
  promptExpanded?: boolean;
8
+ priorityMode?: boolean;
8
9
  waitingReason?: WaitingReason | null;
9
10
  jobWaitingReasons?: JobWaitingReason[];
10
11
  id: string;
@@ -96,6 +97,12 @@ export interface RawJob {
96
97
  lastFrameUrl?: string;
97
98
  lastFrameKey?: string;
98
99
  outputFormat?: string;
100
+ /** Seedance 2.5: this job is a draft that can be finalized at 1080p. */
101
+ seedanceDraft?: boolean;
102
+ /** Seedance 2.5 draft: ISO 8601 time after which it can no longer be finalized. */
103
+ seedanceDraftExpiresAt?: string;
104
+ /** Seedance 2.5 final: the Sogni job id of the draft it finalized. */
105
+ seedanceDraftJobId?: string;
99
106
  result?: JobProvenance & Record<string, unknown>;
100
107
  }
101
108
 
@@ -108,6 +108,12 @@ export interface JobCompleted extends JobEventBase {
108
108
  resultUrl: string | null;
109
109
  lastFrameUrl?: string;
110
110
  outputFormat?: string;
111
+ /** Seedance 2.5: this job is a draft that can be finalized at 1080p. */
112
+ seedanceDraft?: boolean;
113
+ /** Seedance 2.5 draft: ISO 8601 time after which it can no longer be finalized. */
114
+ seedanceDraftExpiresAt?: string;
115
+ /** Seedance 2.5 final: the Sogni job id of the draft it finalized. */
116
+ seedanceDraftJobId?: string;
111
117
  /**
112
118
  * A safety signal fired. Either the media was withheld (filter on, no
113
119
  * `resultUrl`) or it was delivered and merely labelled - see `nsfwDetected`.