@sogni-ai/sogni-client 5.52.1 → 5.53.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [5.53.1](https://github.com/Sogni-AI/sogni-client/compare/v5.53.0...v5.53.1) (2026-09-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **projects:** resend requests lost to a socket drop instead of failing them ([0d9fbee](https://github.com/Sogni-AI/sogni-client/commit/0d9fbee8b48aa3a73e18420d52691ac109806c4f))
7
+
8
+ # [5.53.0](https://github.com/Sogni-AI/sogni-client/compare/v5.52.1...v5.53.0) (2026-09-19)
9
+
10
+
11
+ ### Features
12
+
13
+ * **projects:** return the daily fair-use share on cost estimates ([62594b1](https://github.com/Sogni-AI/sogni-client/commit/62594b1c67021015a69711081a2e668dee9514e7))
14
+
1
15
  ## [5.52.1](https://github.com/Sogni-AI/sogni-client/compare/v5.52.0...v5.52.1) (2026-09-18)
2
16
 
3
17
 
package/README.md CHANGED
@@ -1008,7 +1008,7 @@ When creating video projects, you can specify:
1008
1008
  - `seedanceTaskType` - Seedance 2.5 loose-reference operation: `reference`, `edit`, or `extend`. Edit and extend require a reference video.
1009
1009
  - `hasVideoInput` - Estimate-only flag for `estimateVideoCost`; set this when estimating a canonical Seedance video-input job without passing `referenceVideo`/`referenceVideoUrls`
1010
1010
  - `referenceImageCount` - Optional estimate-only count of image references the video job will submit; models whose pricing does not use it ignore it
1011
- - `referenceVideoCount` / `referenceVideoDurationSeconds` - Estimate-only MiniMax H3 r2v input metadata; reference-video seconds use the full resolution-tier input rate ($0.05/s at 480p or $0.08/s at 544/768p), even with Turbo output
1011
+ - `referenceVideoCount` / `referenceVideoDurationSeconds` - Estimate-only MiniMax H3 r2v input metadata; reference-video seconds use the full resolution-tier input rate ($0.05/s at 480p or $0.08/s at 544/768p), even with Turbo output. On the two-stage Reference ids, reference video with 2K output is $0.13/s
1012
1012
  - MiniMax H3 two-stage quotes (FastH3 and Ref2VA alike): call `estimateVideoCost` with the `_2stage` model id and the canvas the job renders (`672`×`384` for 720p, `960`×`544` for 1080p, `1344`×`768` for 2K). Two-stage output is a model id, not a request option; passing the retired `outputScale` throws before any request
1013
1013
 
1014
1014
  Seedance 2.0 can combine image, video, and audio reference assets in one external API request. Reference limits are up to 9 image assets, 3 video assets, 3 audio assets, and 12 asset files total. Text+audio without at least one image or video reference is not supported by Seedance. URL-array references must be HTTPS URLs that the vendor can fetch; local multi-reference files should be uploaded first, as shown in `examples/workflow_partner_seedance_video.mjs`. In prompts and creative briefs, refer to attachments by Seedance-style tags: `@Image1`, `@Video1`, and `@Audio1`, counted independently by modality in attachment order. Assign each useful reference a role, such as product identity, motion timing, camera path, edit rhythm, background music, or speech reference. Prefer positive preservation language like "maintain the same product silhouette and logo placement from @Image1"; exact readable text, logos, lip-sync, voice cloning, and real-human-reference behavior still need review. Seedance dispatch omits negative prompts; Wan 2.2 and LTX 2.3 video models can still use `negativePrompt`. Seedance jobs are Spark-only and should not use SOGNI token fallback.
@@ -87,6 +87,14 @@ declare class ProjectsApi extends ApiGroup<ProjectApiEvents> {
87
87
  private _awaitingResubmit;
88
88
  /** When each resubmitted project was last sent, for the recently-created grace. */
89
89
  private _resubmittedAt;
90
+ /** Bumped on every transport loss; tells a frame sent before a drop from one sent after. */
91
+ private _transportGeneration;
92
+ /**
93
+ * The transport generation each request was written on, for requests whose
94
+ * send completed. A request written on a connection that has since dropped
95
+ * and that the server never saw died with that connection.
96
+ */
97
+ private _sentOnGeneration;
90
98
  /**
91
99
  * Renders the server announced it moved to another worker (`jobRetry`), still
92
100
  * waiting for that worker's first frame. Only consulted when a frame arrives
@@ -278,6 +286,16 @@ declare class ProjectsApi extends ApiGroup<ProjectApiEvents> {
278
286
  attempts?: number;
279
287
  delayMs?: number;
280
288
  }): Promise<Record<string, ProjectResolution>>;
289
+ /**
290
+ * Send a request again, once, when it was written on a connection that then
291
+ * dropped and no server frame, snapshot or lookup has ever mentioned it. A
292
+ * frame written into a dead socket can never arrive later, so this cannot run
293
+ * the generation twice. Requests whose delivery is merely uncertain (a
294
+ * missing cross-tab ACK) never qualify: the primary tab may still send them.
295
+ */
296
+ private _resendUndelivered;
297
+ /** Resubmitted within the recently-created grace: the server may not list it yet. */
298
+ private _recentlyResubmitted;
281
299
  /**
282
300
  * Second opinion for a project neither the terminal REST record nor the live
283
301
  * socket list knows. Returns `undefined` when the lookup cannot vouch for it.
@@ -408,7 +426,7 @@ declare class ProjectsApi extends ApiGroup<ProjectApiEvents> {
408
426
  /**
409
427
  * Estimate image project cost
410
428
  */
411
- estimateCost({ network, tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages, gptImageQuality, outputFormat }: EstimateRequest): Promise<CostEstimation>;
429
+ estimateCost({ network, tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages, gptImageQuality, outputFormat, billingMode }: EstimateRequest): Promise<CostEstimation>;
412
430
  /**
413
431
  * Estimate image enhancement cost
414
432
  * @param strength
@@ -443,6 +461,7 @@ declare class ProjectsApi extends ApiGroup<ProjectApiEvents> {
443
461
  sogni: string;
444
462
  estimatedRenderSeconds: number | undefined;
445
463
  estimatedTotalSeconds: number | undefined;
464
+ dailyFairUsePct: number | undefined;
446
465
  }>;
447
466
  /**
448
467
  * Estimate the cost of an audio generation job.
@@ -375,6 +375,14 @@ class ProjectsApi extends ApiGroup_js_1.default {
375
375
  this._awaitingResubmit = new Set();
376
376
  /** When each resubmitted project was last sent, for the recently-created grace. */
377
377
  this._resubmittedAt = new Map();
378
+ /** Bumped on every transport loss; tells a frame sent before a drop from one sent after. */
379
+ this._transportGeneration = 0;
380
+ /**
381
+ * The transport generation each request was written on, for requests whose
382
+ * send completed. A request written on a connection that has since dropped
383
+ * and that the server never saw died with that connection.
384
+ */
385
+ this._sentOnGeneration = new Map();
378
386
  /**
379
387
  * Renders the server announced it moved to another worker (`jobRetry`), still
380
388
  * waiting for that worker's first frame. Only consulted when a frame arrives
@@ -784,6 +792,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
784
792
  if (project.finished) {
785
793
  this._unadmittedRequests.delete(project.id);
786
794
  this._resubmittedAt.delete(project.id);
795
+ this._sentOnGeneration.delete(project.id);
787
796
  // Sync project data with the server and remove it from the list after some time
788
797
  project._syncToServer().catch((e) => {
789
798
  // 404 errors are expected when project is still initializing
@@ -937,6 +946,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
937
946
  }
938
947
  handleTransportLost() {
939
948
  this.transportDisconnected = true;
949
+ this._transportGeneration++;
940
950
  this._clearAuthenticatedTimer();
941
951
  // A transport gap is not a project failure. The server keeps rendering and
942
952
  // hands the project back on reconnect (same app-id), so keep the tracked
@@ -1126,7 +1136,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
1126
1136
  */
1127
1137
  resolveMissing(projectIds_1) {
1128
1138
  return __awaiter(this, arguments, void 0, function* (projectIds, options = {}) {
1129
- var _a, _b, _c;
1139
+ var _a, _b;
1130
1140
  const attempts = Math.max(1, (_a = options.attempts) !== null && _a !== void 0 ? _a : this._recoveryTuning.missingProjectAttempts);
1131
1141
  const delayMs = (_b = options.delayMs) !== null && _b !== void 0 ? _b : this._recoveryTuning.missingProjectRetryMs;
1132
1142
  const result = {};
@@ -1165,14 +1175,72 @@ class ProjectsApi extends ApiGroup_js_1.default {
1165
1175
  const unlisted = pending.filter((id) => !(live === null || live === void 0 ? void 0 : live.includes(id)));
1166
1176
  const checks = new Map(yield Promise.all(unlisted.map((id) => __awaiter(this, void 0, void 0, function* () { return [id, yield this._lookupUnlistedProject(id)]; }))));
1167
1177
  for (const id of pending) {
1168
- result[id] = (live === null || live === void 0 ? void 0 : live.includes(id))
1169
- ? { state: 'active' }
1170
- : ((_c = checks.get(id)) !== null && _c !== void 0 ? _c : { state: 'lost' });
1178
+ if (live === null || live === void 0 ? void 0 : live.includes(id)) {
1179
+ result[id] = { state: 'active' };
1180
+ continue;
1181
+ }
1182
+ const check = checks.get(id);
1183
+ if (check) {
1184
+ result[id] = check;
1185
+ continue;
1186
+ }
1187
+ // Nothing on the server knows it. A request that died with a dropped
1188
+ // connection is sent again; one just (re)sent is still being admitted.
1189
+ result[id] =
1190
+ (yield this._resendUndelivered(id)) || this._recentlyResubmitted(id)
1191
+ ? { state: 'active' }
1192
+ : { state: 'lost' };
1171
1193
  }
1172
1194
  }
1173
1195
  return result;
1174
1196
  });
1175
1197
  }
1198
+ /**
1199
+ * Send a request again, once, when it was written on a connection that then
1200
+ * dropped and no server frame, snapshot or lookup has ever mentioned it. A
1201
+ * frame written into a dead socket can never arrive later, so this cannot run
1202
+ * the generation twice. Requests whose delivery is merely uncertain (a
1203
+ * missing cross-tab ACK) never qualify: the primary tab may still send them.
1204
+ */
1205
+ _resendUndelivered(projectId) {
1206
+ return __awaiter(this, void 0, void 0, function* () {
1207
+ const request = this._unadmittedRequests.get(projectId);
1208
+ const sentOn = this._sentOnGeneration.get(projectId);
1209
+ const project = this.projects.find((p) => p.id === projectId);
1210
+ if (!request ||
1211
+ sentOn === undefined ||
1212
+ sentOn >= this._transportGeneration ||
1213
+ this.transportDisconnected ||
1214
+ this._resubmittedAt.has(projectId) ||
1215
+ this._awaitingResubmit.has(projectId) ||
1216
+ (project && project.finished)) {
1217
+ return false;
1218
+ }
1219
+ // Claimed before the await so a concurrent lookup cannot send it twice.
1220
+ this._resubmittedAt.set(projectId, Date.now());
1221
+ this._sentOnGeneration.delete(projectId);
1222
+ this.client.logger.info(`Project ${projectId} was sent on a connection that dropped before the server received it; resubmitting`);
1223
+ try {
1224
+ yield this.client.socket.send('jobRequest', request);
1225
+ }
1226
+ catch (error) {
1227
+ if (!(error instanceof requestDelivery_js_1.MessageDeliveryUncertainError)) {
1228
+ this.client.logger.warn(`Resubmitting project ${projectId} failed`, error);
1229
+ return false;
1230
+ }
1231
+ }
1232
+ this._resubmittedAt.set(projectId, Date.now());
1233
+ this._sentOnGeneration.set(projectId, this._transportGeneration);
1234
+ project === null || project === void 0 ? void 0 : project._keepAlive();
1235
+ this._scheduleRecheck(this._recoveryTuning.recentlyCreatedGraceMs);
1236
+ return true;
1237
+ });
1238
+ }
1239
+ /** Resubmitted within the recently-created grace: the server may not list it yet. */
1240
+ _recentlyResubmitted(projectId) {
1241
+ const at = this._resubmittedAt.get(projectId);
1242
+ return at !== undefined && Date.now() - at < this._recoveryTuning.recentlyCreatedGraceMs;
1243
+ }
1176
1244
  /**
1177
1245
  * Second opinion for a project neither the terminal REST record nor the live
1178
1246
  * socket list knows. Returns `undefined` when the lookup cannot vouch for it.
@@ -1551,6 +1619,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
1551
1619
  // Recorded before sending: a refusal can arrive as soon as the frame lands.
1552
1620
  this._unadmittedRequests.set(project.id, request);
1553
1621
  yield this.client.socket.send('jobRequest', request);
1622
+ this._sentOnGeneration.set(project.id, this._transportGeneration);
1554
1623
  return this._trackSubmitted(project);
1555
1624
  }
1556
1625
  catch (error) {
@@ -1581,6 +1650,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
1581
1650
  return project;
1582
1651
  }
1583
1652
  this._unadmittedRequests.delete(project.id);
1653
+ this._sentOnGeneration.delete(project.id);
1584
1654
  project._dispose();
1585
1655
  return tracked;
1586
1656
  }
@@ -2153,8 +2223,8 @@ class ProjectsApi extends ApiGroup_js_1.default {
2153
2223
  * Estimate image project cost
2154
2224
  */
2155
2225
  estimateCost(_a) {
2156
- return __awaiter(this, arguments, void 0, function* ({ network = 'fast', tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages, gptImageQuality, outputFormat }) {
2157
- var _b, _c;
2226
+ return __awaiter(this, arguments, void 0, function* ({ network = 'fast', tokenType, model, imageCount, stepCount, previewCount, cnEnabled, startingImageStrength, width, height, sizePreset, guidance, sampler, contextImages, gptImageQuality, outputFormat, billingMode }) {
2227
+ var _b, _c, _d;
2158
2228
  let apiVersion = 2;
2159
2229
  const modelOptions = yield this.getModelOptions(model);
2160
2230
  const pathParams = [
@@ -2192,6 +2262,8 @@ class ProjectsApi extends ApiGroup_js_1.default {
2192
2262
  queryParams.set('gptImageQuality', gptImageQuality);
2193
2263
  if (outputFormat)
2194
2264
  queryParams.set('outputFormat', outputFormat);
2265
+ if (billingMode)
2266
+ queryParams.set('billingMode', billingMode);
2195
2267
  const query = queryParams.toString();
2196
2268
  const r = yield this.client.socket.get(`/api/v${apiVersion}/job/estimate/${pathParams.join('/')}${query ? `?${query}` : ''}`);
2197
2269
  return {
@@ -2200,7 +2272,8 @@ class ProjectsApi extends ApiGroup_js_1.default {
2200
2272
  spark: r.quote.project.costInSpark,
2201
2273
  sogni: r.quote.project.costInSogni,
2202
2274
  estimatedRenderSeconds: (_b = r.benchmark) === null || _b === void 0 ? void 0 : _b.estimatedRenderTimeSec,
2203
- estimatedTotalSeconds: (_c = r.benchmark) === null || _c === void 0 ? void 0 : _c.estimatedTotalTimeSec
2275
+ estimatedTotalSeconds: (_c = r.benchmark) === null || _c === void 0 ? void 0 : _c.estimatedTotalTimeSec,
2276
+ dailyFairUsePct: (_d = r.dailyFairUse) === null || _d === void 0 ? void 0 : _d.pct
2204
2277
  };
2205
2278
  });
2206
2279
  }
@@ -2246,7 +2319,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
2246
2319
  */
2247
2320
  estimateVideoCost(params) {
2248
2321
  return __awaiter(this, void 0, void 0, function* () {
2249
- var _a, _b, _c;
2322
+ var _a, _b, _c, _d, _e;
2250
2323
  (0, createJobRequestMessage_js_1.rejectRetiredOutputScale)(params);
2251
2324
  const frames = params.frames
2252
2325
  ? params.frames
@@ -2292,6 +2365,12 @@ class ProjectsApi extends ApiGroup_js_1.default {
2292
2365
  params.referenceVideoDurationSeconds >= 0) {
2293
2366
  query.set('referenceVideoDurationSeconds', String(params.referenceVideoDurationSeconds));
2294
2367
  }
2368
+ // Unpinned, the job renders on the connection's network, so quote that one.
2369
+ const network = (_b = params.network) !== null && _b !== void 0 ? _b : this._currentNetworkType;
2370
+ if (network)
2371
+ query.set('network', network);
2372
+ if (params.billingMode)
2373
+ query.set('billingMode', params.billingMode);
2295
2374
  const queryString = query.toString();
2296
2375
  const r = yield this.client.socket.get(`/api/v1/job-video/estimate/${path}${queryString ? `?${queryString}` : ''}`);
2297
2376
  return {
@@ -2299,8 +2378,9 @@ class ProjectsApi extends ApiGroup_js_1.default {
2299
2378
  usd: r.quote.project.costInUSD,
2300
2379
  spark: r.quote.project.costInSpark,
2301
2380
  sogni: r.quote.project.costInSogni,
2302
- estimatedRenderSeconds: (_b = r.benchmark) === null || _b === void 0 ? void 0 : _b.estimatedRenderTimeSec,
2303
- estimatedTotalSeconds: (_c = r.benchmark) === null || _c === void 0 ? void 0 : _c.estimatedTotalTimeSec
2381
+ estimatedRenderSeconds: (_c = r.benchmark) === null || _c === void 0 ? void 0 : _c.estimatedRenderTimeSec,
2382
+ estimatedTotalSeconds: (_d = r.benchmark) === null || _d === void 0 ? void 0 : _d.estimatedTotalTimeSec,
2383
+ dailyFairUsePct: (_e = r.dailyFairUse) === null || _e === void 0 ? void 0 : _e.pct
2304
2384
  };
2305
2385
  });
2306
2386
  }
@@ -2317,7 +2397,7 @@ class ProjectsApi extends ApiGroup_js_1.default {
2317
2397
  */
2318
2398
  estimateAudioCost(params) {
2319
2399
  return __awaiter(this, void 0, void 0, function* () {
2320
- var _a, _b;
2400
+ var _a, _b, _c, _d;
2321
2401
  const pathParams = [
2322
2402
  params.tokenType,
2323
2403
  params.model,
@@ -2326,14 +2406,23 @@ class ProjectsApi extends ApiGroup_js_1.default {
2326
2406
  params.numberOfMedia
2327
2407
  ];
2328
2408
  const path = pathParams.map((p) => encodeURIComponent(p)).join('/');
2329
- const r = yield this.client.socket.get(`/api/v1/job-audio/estimate/${path}`);
2409
+ const query = new URLSearchParams();
2410
+ // Unpinned, the job renders on the connection's network, so quote that one.
2411
+ const network = (_a = params.network) !== null && _a !== void 0 ? _a : this._currentNetworkType;
2412
+ if (network)
2413
+ query.set('network', network);
2414
+ if (params.billingMode)
2415
+ query.set('billingMode', params.billingMode);
2416
+ const queryString = query.toString();
2417
+ const r = yield this.client.socket.get(`/api/v1/job-audio/estimate/${path}${queryString ? `?${queryString}` : ''}`);
2330
2418
  return {
2331
2419
  token: r.quote.project.costInToken,
2332
2420
  usd: r.quote.project.costInUSD,
2333
2421
  spark: r.quote.project.costInSpark,
2334
2422
  sogni: r.quote.project.costInSogni,
2335
- estimatedRenderSeconds: (_a = r.benchmark) === null || _a === void 0 ? void 0 : _a.estimatedRenderTimeSec,
2336
- estimatedTotalSeconds: (_b = r.benchmark) === null || _b === void 0 ? void 0 : _b.estimatedTotalTimeSec
2423
+ estimatedRenderSeconds: (_b = r.benchmark) === null || _b === void 0 ? void 0 : _b.estimatedRenderTimeSec,
2424
+ estimatedTotalSeconds: (_c = r.benchmark) === null || _c === void 0 ? void 0 : _c.estimatedTotalTimeSec,
2425
+ dailyFairUsePct: (_d = r.dailyFairUse) === null || _d === void 0 ? void 0 : _d.pct
2337
2426
  };
2338
2427
  });
2339
2428
  }