@picsart/ai-sdk 5.24.1 → 5.25.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 (4) hide show
  1. package/README.md +54 -1
  2. package/index.d.ts +58 -1
  3. package/index.js +167 -35
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -188,9 +188,61 @@ for await (const update of ai.subscribe(handle)) {
188
188
  const status = await ai.status(handle)
189
189
  ```
190
190
 
191
+ ## Error Handling
192
+
193
+ Every failure thrown by `generate()`, `generateText()`, `submit()`, and `result()`
194
+ is an `ApiError` with the same four fields, so you can branch on the error
195
+ instead of pattern-matching its message:
196
+
197
+ ```typescript
198
+ import { createClient, Models, ApiError } from '@picsart/ai-sdk'
199
+
200
+ try {
201
+ const result = await ai.generate(Models.Flux2Pro, { prompt: 'a cat on mars' })
202
+ } catch (err) {
203
+ if (err instanceof ApiError) {
204
+ err.status // 402 — HTTP status, or its synthesized equivalent
205
+ err.code // 'payment_required' — platform `reason`, else an SDK code
206
+ err.reason // same value as `code`, named after the platform's own field
207
+ err.message // 'Submit failed (402): Not enough credits'
208
+
209
+ if (err.status === 402) return topUpCredits()
210
+ if (err.status === 429 || err.status >= 500) return retry()
211
+ if (err.code === 'validation_error') return showFormError(err.message)
212
+ }
213
+ throw err
214
+ }
215
+ ```
216
+
217
+ `code` carries the platform's `reason` verbatim whenever the API supplies one
218
+ (`content_moderation`, `unauthorized`, …). When it doesn't, the SDK fills in a
219
+ conventional slug for the status — `payment_required` for 402, `rate_limited`
220
+ for 429, and so on.
221
+
222
+ Failures that never reach the network get the status they semantically deserve,
223
+ so one retry predicate covers every case:
224
+
225
+ | Failure | `status` | `code` |
226
+ |---------|----------|--------|
227
+ | Unknown model id | 400 | `unknown_model` |
228
+ | `generate()` on a text model (or the reverse) | 400 | `wrong_model_mode` |
229
+ | Parameter validation | 400 | `validation_error` |
230
+ | Async lifecycle on an execute-only transport | 400 | `unsupported_transport` |
231
+ | HTTP error from the API | the response's status | platform `reason`, else the status slug |
232
+ | Poll deadline exceeded | 408 | `timeout` |
233
+ | Aborted via `options.signal`, or a canceled job | 499 | `aborted` / `canceled` |
234
+ | Job finished `FAILED` | the task's `statusCode`, else 502 | platform `reason`, else `generation_failed` |
235
+ | Response the SDK can't parse | 502 | `invalid_response` |
236
+
237
+ Aborts raised by `fetch` itself are deliberately **not** wrapped, so
238
+ `err.name === 'AbortError'` keeps working on the `DOMException`.
239
+
240
+ `message` is human-readable and may change between versions — branch on `status`
241
+ and `code`, not on the message text.
242
+
191
243
  ## Public API
192
244
 
193
- The SDK exports 7 symbols:
245
+ The SDK exports 8 symbols:
194
246
 
195
247
  | Export | Type | Description |
196
248
  |--------|------|-------------|
@@ -201,6 +253,7 @@ The SDK exports 7 symbols:
201
253
  | `AuthenticatedFetch` | type | `(url, init?) => Promise<Response>` — for the custom-`fetch` path |
202
254
  | `SdkTransport` | type | Advanced: custom transport interface |
203
255
  | `WorkflowJobHandle` | type | Job handle for submit/status/cancel |
256
+ | `ApiError` | class | Unified error: `{ status, code, reason, message }` — see [Error Handling](#error-handling) |
204
257
 
205
258
  ## Package Structure
206
259
 
package/index.d.ts CHANGED
@@ -49,6 +49,10 @@ interface WorkflowStatusResult<TResult = unknown> {
49
49
  status: WorkflowStatus;
50
50
  result?: TResult;
51
51
  error?: string;
52
+ /** Platform error `reason` on a failed task, when the response carried one. */
53
+ reason?: string;
54
+ /** Numeric status on the error payload, when the response carried one. */
55
+ statusCode?: number;
52
56
  progress?: WorkflowProgress;
53
57
  /** Credit usage reported by the platform, when present on the response. */
54
58
  usage?: CreditUsage;
@@ -2774,6 +2778,59 @@ declare function getVoiceById(id: string): VoiceOption | undefined;
2774
2778
  /** @deprecated Load the model's catalog instead (`ai.catalogs.voices(modelId)`) — loaded voices are searched automatically. */
2775
2779
  declare function getVoiceById(id: string, extra: VoiceOption[] | undefined): VoiceOption | undefined;
2776
2780
 
2781
+ /**
2782
+ * Failure codes the SDK synthesizes when the platform supplies no `reason`.
2783
+ * An API-supplied `reason` passes through unchanged, so the open `string`
2784
+ * member keeps arbitrary platform reasons assignable while preserving
2785
+ * autocomplete on the known set.
2786
+ */
2787
+ type ApiErrorCode = 'unknown_model' | 'wrong_model_mode' | 'validation_error' | 'unsupported_transport' | 'timeout' | 'aborted' | 'canceled' | 'generation_failed' | 'invalid_response' | 'bad_request' | 'unauthorized' | 'payment_required' | 'forbidden' | 'not_found' | 'rate_limited' | 'server_error' | (string & {});
2788
+ /** Everything but the message needed to build a {@link ApiError}. */
2789
+ interface ApiErrorInit {
2790
+ /**
2791
+ * HTTP status of the failing response. When no HTTP exchange took place the
2792
+ * SDK synthesizes the semantically matching code: 400 for input the SDK
2793
+ * itself rejects, 408 for a poll deadline, 499 for an abort or cancel, 502
2794
+ * for a response it cannot make sense of.
2795
+ */
2796
+ status: number;
2797
+ /** Platform `reason` when present, otherwise an SDK-synthesized code. */
2798
+ code: ApiErrorCode;
2799
+ }
2800
+ /**
2801
+ * The single error type thrown by the SDK's generation surface —
2802
+ * `generate()`, `generateText()`, `submit()`, and `result()`.
2803
+ *
2804
+ * Unrelated to the `Api*` types (`ApiResponse`, `ApiRunOptions`, …), which
2805
+ * describe the low-level `ai.apis` surface. `ai.apis.run()` throws the
2806
+ * workflows client's own errors, not this.
2807
+ *
2808
+ * ```ts
2809
+ * try {
2810
+ * await ai.generate(Models.Flux2Pro, { prompt: 'a cat' });
2811
+ * } catch (err) {
2812
+ * if (err instanceof ApiError) {
2813
+ * if (err.status === 402) return topUpCredits();
2814
+ * if (err.status === 429 || err.status >= 500) return retry();
2815
+ * if (err.code === 'validation_error') return showFormError(err.message);
2816
+ * }
2817
+ * throw err;
2818
+ * }
2819
+ * ```
2820
+ *
2821
+ * Aborts raised by `fetch` itself are never wrapped — a caller checking
2822
+ * `err.name === 'AbortError'` on a `DOMException` keeps working.
2823
+ */
2824
+ declare class ApiError extends Error {
2825
+ /** HTTP status, or the synthesized equivalent for non-HTTP failures. */
2826
+ readonly status: number;
2827
+ /** Platform `reason`, or an SDK-synthesized code. Always equal to {@link reason}. */
2828
+ readonly code: ApiErrorCode;
2829
+ /** Alias of {@link code}, named after the platform's own error field. */
2830
+ readonly reason: ApiErrorCode;
2831
+ constructor(message: string, init: ApiErrorInit);
2832
+ }
2833
+
2777
2834
  /**
2778
2835
  * Pricing internals — owns the ModelPricingClient, the per-model cache, and
2779
2836
  * the credit-range lookup. The Model accessor delegates to the helpers below
@@ -2902,4 +2959,4 @@ declare const findModel: (ref: string) => ModelDefinition | undefined;
2902
2959
  */
2903
2960
  declare const KLING_DUAL_IMAGE_EFFECTS: ReadonlySet<string>;
2904
2961
 
2905
- export { ALL_MODELS, type AiClient, type ApiResponse, type ApiRunOptions, type ApiSchemas, type ApisClient, type AppIdentity, type AppType, type AuthenticatedFetch, type AvatarOption, type BooleanDescriptor, type BooleanEntry, type CatalogDescriptor, type CatalogEntry, type CatalogItem, type CatalogKind, type CatalogPage, type CatalogPageOptions, type CatalogPreview, type CatalogQuery, type CatalogResult, type CatalogSource, type CatalogsClient, type CatalogsOptions, type ClientConfig, type CreditRange, type CreditRangeContext, type CreditTier, type CreditUsage, DEFAULT_VISIBLE_RELEASES, type DeepLinkResult, type DriveAttributes, type DriveClient, type DriveConfig, type DriveFile, type DriveFileDetails, type DriveFolder, type DriveMediaItem, type DriveSaveResult, type EntryMeta, type EnumDescriptor, type EnumEntry, type EnumOption, type FileDescriptor, type FileEntry, type FlatParamEntry, type GenerateOptions, type GenerateResult, type GenerateResultItem, type GenerateTextResult, type GenerationContext, type GenerationFile, type GenerationMode, KLING_DUAL_IMAGE_EFFECTS, type ListOptions, type MediaModelId, type MediaTypeFilter, Model, type ModelDefinition, type ModelDescriptor, type ModelFilter$1 as ModelFilter, type ModelInput, type ModelInputById, type ModelMeta, type ModelParams, type ModelParamsAccessor, Models, type ObjectDescriptor, type ObjectEntry, type ParamDescriptor, type ParamEntry, type ParamOption, type PayloadDriveFolderOptions, type PayloadDriveOptions, type PricingOptions, type ProviderInfo, type RangeDescriptor, type RangeEntry, type ReleaseTag, type SaveParams, type SdkPayload, type SdkTransport, type TextDescriptor, type TextEntry, type TextModelId, type TextModelInputById, type ToolUsage, type TypedModelId, type UserReaction, type ValidationResult$1 as ValidationResult, type VoiceOption, type WorkflowJobHandle, buildFilename, buildGenerationAttributes, catalog, createClient, decodeDeepLinkPayload, encodeDeepLinkPayload, findModel, getModel, getModelsByMode, getVoiceById, inferResourceType, isVisibleForReleases, parseGeneration, releaseOf, toAvatarOption, toVoiceOption };
2962
+ export { ALL_MODELS, type AiClient, ApiError, type ApiErrorCode, type ApiErrorInit, type ApiResponse, type ApiRunOptions, type ApiSchemas, type ApisClient, type AppIdentity, type AppType, type AuthenticatedFetch, type AvatarOption, type BooleanDescriptor, type BooleanEntry, type CatalogDescriptor, type CatalogEntry, type CatalogItem, type CatalogKind, type CatalogPage, type CatalogPageOptions, type CatalogPreview, type CatalogQuery, type CatalogResult, type CatalogSource, type CatalogsClient, type CatalogsOptions, type ClientConfig, type CreditRange, type CreditRangeContext, type CreditTier, type CreditUsage, DEFAULT_VISIBLE_RELEASES, type DeepLinkResult, type DriveAttributes, type DriveClient, type DriveConfig, type DriveFile, type DriveFileDetails, type DriveFolder, type DriveMediaItem, type DriveSaveResult, type EntryMeta, type EnumDescriptor, type EnumEntry, type EnumOption, type FileDescriptor, type FileEntry, type FlatParamEntry, type GenerateOptions, type GenerateResult, type GenerateResultItem, type GenerateTextResult, type GenerationContext, type GenerationFile, type GenerationMode, KLING_DUAL_IMAGE_EFFECTS, type ListOptions, type MediaModelId, type MediaTypeFilter, Model, type ModelDefinition, type ModelDescriptor, type ModelFilter$1 as ModelFilter, type ModelInput, type ModelInputById, type ModelMeta, type ModelParams, type ModelParamsAccessor, Models, type ObjectDescriptor, type ObjectEntry, type ParamDescriptor, type ParamEntry, type ParamOption, type PayloadDriveFolderOptions, type PayloadDriveOptions, type PricingOptions, type ProviderInfo, type RangeDescriptor, type RangeEntry, type ReleaseTag, type SaveParams, type SdkPayload, type SdkTransport, type TextDescriptor, type TextEntry, type TextModelId, type TextModelInputById, type ToolUsage, type TypedModelId, type UserReaction, type ValidationResult$1 as ValidationResult, type VoiceOption, type WorkflowJobHandle, buildFilename, buildGenerationAttributes, catalog, createClient, decodeDeepLinkPayload, encodeDeepLinkPayload, findModel, getModel, getModelsByMode, getVoiceById, inferResourceType, isVisibleForReleases, parseGeneration, releaseOf, toAvatarOption, toVoiceOption };
package/index.js CHANGED
@@ -441,6 +441,62 @@ var require_build = __commonJS({
441
441
  }
442
442
  });
443
443
 
444
+ // src/core/errors.ts
445
+ var ApiError = class extends Error {
446
+ /** HTTP status, or the synthesized equivalent for non-HTTP failures. */
447
+ status;
448
+ /** Platform `reason`, or an SDK-synthesized code. Always equal to {@link reason}. */
449
+ code;
450
+ /** Alias of {@link code}, named after the platform's own error field. */
451
+ reason;
452
+ constructor(message, init) {
453
+ super(message);
454
+ this.name = "ApiError";
455
+ this.status = init.status;
456
+ this.code = init.code;
457
+ this.reason = init.code;
458
+ }
459
+ };
460
+ var CODE_BY_STATUS = {
461
+ 400: "bad_request",
462
+ 401: "unauthorized",
463
+ 402: "payment_required",
464
+ 403: "forbidden",
465
+ 404: "not_found",
466
+ 408: "timeout",
467
+ 409: "conflict",
468
+ 413: "payload_too_large",
469
+ 422: "unprocessable_entity",
470
+ 429: "rate_limited",
471
+ 500: "server_error",
472
+ 502: "bad_gateway",
473
+ 503: "service_unavailable",
474
+ 504: "gateway_timeout"
475
+ };
476
+ function codeForStatus(status) {
477
+ return CODE_BY_STATUS[status] ?? (status >= 500 ? "server_error" : `http_${status}`);
478
+ }
479
+ async function readErrorBody(res) {
480
+ let text = "";
481
+ try {
482
+ text = await res.text();
483
+ } catch {
484
+ return { text: "" };
485
+ }
486
+ try {
487
+ const parsed = JSON.parse(text);
488
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
489
+ return { text, json: parsed };
490
+ }
491
+ } catch {
492
+ }
493
+ return { text };
494
+ }
495
+ function reasonFrom(json, status) {
496
+ const raw = json?.reason ?? json?.code;
497
+ return typeof raw === "string" && raw.length > 0 ? raw : codeForStatus(status);
498
+ }
499
+
444
500
  // src/core/workflow.ts
445
501
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
446
502
  var DEFAULT_MAX_ATTEMPTS = 300;
@@ -476,7 +532,9 @@ function parseWorkflowStatus(handle, raw) {
476
532
  const result = pickFirst(raw, [["response", "result"], ["result"]]);
477
533
  const usageRaw = pickFirst(raw, [["response", "usage"], ["usage"]]);
478
534
  const usage = usageRaw && typeof usageRaw === "object" && (typeof usageRaw.credits === "number" || Array.isArray(usageRaw.details)) ? usageRaw : void 0;
479
- const errorRaw = pickFirst(raw, [["response", "error"], ["error"], ["message"], ["reason"]]);
535
+ const errorRaw = pickFirst(raw, [["response", "error"], ["response", "message"], ["error"], ["message"], ["reason"]]);
536
+ const reasonRaw = pickFirst(raw, [["response", "reason"], ["reason"]]);
537
+ const statusCodeRaw = pickFirst(raw, [["response", "statusCode"], ["statusCode"]]);
480
538
  const progressRaw = pickFirst(raw, [["response", "progress"], ["progress"]]);
481
539
  const progress = progressRaw && typeof progressRaw === "object" ? {
482
540
  percent: typeof progressRaw.percent === "number" ? progressRaw.percent : void 0,
@@ -487,6 +545,8 @@ function parseWorkflowStatus(handle, raw) {
487
545
  status,
488
546
  result,
489
547
  error: typeof errorRaw === "string" ? errorRaw : void 0,
548
+ reason: typeof reasonRaw === "string" ? reasonRaw : void 0,
549
+ statusCode: typeof statusCodeRaw === "number" ? statusCodeRaw : void 0,
490
550
  progress,
491
551
  usage,
492
552
  raw
@@ -502,13 +562,19 @@ function createWorkflowClient(transport, options = {}) {
502
562
  const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
503
563
  const submit = async (request) => {
504
564
  if (!transport.submit) {
505
- throw new Error("Transport does not support submit (execute-only transport)");
565
+ throw new ApiError("Transport does not support submit (execute-only transport)", {
566
+ status: 400,
567
+ code: "unsupported_transport"
568
+ });
506
569
  }
507
570
  return transport.submit(request);
508
571
  };
509
572
  const status = async (handle, signal) => {
510
573
  if (!transport.status) {
511
- throw new Error("Transport does not support status (execute-only transport)");
574
+ throw new ApiError("Transport does not support status (execute-only transport)", {
575
+ status: 400,
576
+ code: "unsupported_transport"
577
+ });
512
578
  }
513
579
  const raw = await transport.status(handle, signal);
514
580
  return parseStatus(handle, raw);
@@ -518,13 +584,16 @@ function createWorkflowClient(transport, options = {}) {
518
584
  const maxAttempts = pollOptions.maxAttempts ?? defaultMaxAttempts;
519
585
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
520
586
  if (pollOptions.signal?.aborted) {
521
- throw new Error("Operation aborted");
587
+ throw new ApiError("Operation aborted", { status: 499, code: "aborted" });
522
588
  }
523
589
  const next = await status(handle, pollOptions.signal);
524
590
  if (isTerminal(next.status)) return next;
525
591
  await sleep2(intervalMs);
526
592
  }
527
- throw new Error(`Timed out waiting for workflow ${handle.workflow}:${handle.id}`);
593
+ throw new ApiError(
594
+ `Timed out waiting for workflow ${handle.workflow}:${handle.id}`,
595
+ { status: 408, code: "timeout" }
596
+ );
528
597
  };
529
598
  const run = async (request, runOptions = {}) => {
530
599
  const runMode = runOptions.mode;
@@ -543,14 +612,17 @@ function createWorkflowClient(transport, options = {}) {
543
612
  const maxAttempts = subscribeOptions.maxAttempts ?? defaultMaxAttempts;
544
613
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
545
614
  if (subscribeOptions.signal?.aborted) {
546
- throw new Error("Operation aborted");
615
+ throw new ApiError("Operation aborted", { status: 499, code: "aborted" });
547
616
  }
548
617
  const next = await status(handle, subscribeOptions.signal);
549
618
  yield next;
550
619
  if (isTerminal(next.status)) return next;
551
620
  await sleep2(intervalMs);
552
621
  }
553
- throw new Error(`Timed out waiting for workflow ${handle.workflow}:${handle.id}`);
622
+ throw new ApiError(
623
+ `Timed out waiting for workflow ${handle.workflow}:${handle.id}`,
624
+ { status: 408, code: "timeout" }
625
+ );
554
626
  };
555
627
  return { submit, status, result, run, subscribe };
556
628
  }
@@ -8676,14 +8748,22 @@ var getModelsByMode = (mode, includeDisabled = false) => ALL_MODELS.filter((m) =
8676
8748
  // src/core/contracts.ts
8677
8749
  function requireObject(value, message) {
8678
8750
  if (!value || typeof value !== "object" || Array.isArray(value)) {
8679
- throw new Error(message);
8751
+ throw new ApiError(message, { status: 400, code: "validation_error" });
8680
8752
  }
8681
8753
  }
8682
8754
  function buildInputSchema(model) {
8683
8755
  return {
8684
8756
  parse(input) {
8685
8757
  requireObject(input, `Invalid input for model "${model.id}"`);
8686
- validateAll(model.paramConfig, input);
8758
+ try {
8759
+ validateAll(model.paramConfig, input);
8760
+ } catch (err) {
8761
+ if (err instanceof ApiError) throw err;
8762
+ throw new ApiError(err instanceof Error ? err.message : String(err), {
8763
+ status: 400,
8764
+ code: "validation_error"
8765
+ });
8766
+ }
8687
8767
  return input;
8688
8768
  }
8689
8769
  };
@@ -8692,7 +8772,10 @@ function buildOutputSchema(model) {
8692
8772
  return {
8693
8773
  parse(output) {
8694
8774
  if (output == null) {
8695
- throw new Error(`Model "${model.id}" returned empty output`);
8775
+ throw new ApiError(`Model "${model.id}" returned empty output`, {
8776
+ status: 502,
8777
+ code: "invalid_response"
8778
+ });
8696
8779
  }
8697
8780
  return output;
8698
8781
  }
@@ -8735,7 +8818,12 @@ function throwIfErrorResult(result, modelName) {
8735
8818
  if (isError) {
8736
8819
  const code = typeof status === "number" ? ` (${status})` : "";
8737
8820
  const detail = message ? String(message) : "unknown error";
8738
- throw new Error(`${modelName} failed${code}: ${detail}`);
8821
+ const httpStatus = typeof status === "number" ? status : 502;
8822
+ const reason = obj.reason;
8823
+ throw new ApiError(`${modelName} failed${code}: ${detail}`, {
8824
+ status: httpStatus,
8825
+ code: typeof reason === "string" && reason.length > 0 ? reason : codeForStatus(httpStatus)
8826
+ });
8739
8827
  }
8740
8828
  }
8741
8829
  function extractSyncResult(raw) {
@@ -8937,7 +9025,9 @@ var findModel = (ref) => {
8937
9025
  // src/core/resolve.ts
8938
9026
  function resolveModel(id) {
8939
9027
  const found = findModel(id);
8940
- if (!found) throw new Error(`Unknown model: "${id}"`);
9028
+ if (!found) {
9029
+ throw new ApiError(`Unknown model: "${id}"`, { status: 400, code: "unknown_model" });
9030
+ }
8941
9031
  return found;
8942
9032
  }
8943
9033
 
@@ -8977,18 +9067,34 @@ function buildTransport(config) {
8977
9067
  { params: request.payload },
8978
9068
  request.signal
8979
9069
  );
8980
- const data = await res.json();
9070
+ const { text, json } = await readErrorBody(res);
8981
9071
  if (!res.ok) {
8982
- throw new Error(`Submit failed (${res.status}): ${data.message ?? JSON.stringify(data)}`);
9072
+ const detail = json ? json.message ?? JSON.stringify(json) : text;
9073
+ throw new ApiError(`Submit failed (${res.status}): ${detail}`, {
9074
+ status: res.status,
9075
+ code: reasonFrom(json, res.status)
9076
+ });
9077
+ }
9078
+ const response = json?.response;
9079
+ const id = response?.id ?? json?.id;
9080
+ if (!id) {
9081
+ throw new ApiError(`No task id in response: ${json ? JSON.stringify(json) : text}`, {
9082
+ status: 502,
9083
+ code: "invalid_response"
9084
+ });
8983
9085
  }
8984
- const response = data.response;
8985
- const id = response?.id ?? data.id;
8986
- if (!id) throw new Error(`No task id in response: ${JSON.stringify(data)}`);
8987
9086
  return { workflow: request.workflow, id: String(id) };
8988
9087
  },
8989
9088
  async status(handle, signal) {
8990
9089
  const res = await f(`${apiUrl}/workflows/${handle.workflow}/${handle.id}/result`, { signal });
8991
- if (!res.ok) throw new Error(`Status check failed (${res.status}): ${await res.text()}`);
9090
+ if (!res.ok) {
9091
+ const { text, json } = await readErrorBody(res);
9092
+ const detail = json ? json.message ?? text : text;
9093
+ throw new ApiError(`Status check failed (${res.status}): ${detail}`, {
9094
+ status: res.status,
9095
+ code: reasonFrom(json, res.status)
9096
+ });
9097
+ }
8992
9098
  return res.json();
8993
9099
  },
8994
9100
  async execute(request) {
@@ -8997,7 +9103,14 @@ function buildTransport(config) {
8997
9103
  { params: request.payload },
8998
9104
  request.signal
8999
9105
  );
9000
- if (!res.ok) throw new Error(`Execute failed (${res.status}): ${await res.text()}`);
9106
+ if (!res.ok) {
9107
+ const { text, json } = await readErrorBody(res);
9108
+ const detail = json ? json.message ?? text : text;
9109
+ throw new ApiError(`Execute failed (${res.status}): ${detail}`, {
9110
+ status: res.status,
9111
+ code: reasonFrom(json, res.status)
9112
+ });
9113
+ }
9001
9114
  return res.json();
9002
9115
  },
9003
9116
  async options(workflow, payload) {
@@ -9035,13 +9148,19 @@ function prepareRequest(model, params2) {
9035
9148
  const payload = resolved.buildPayload(validatedCtx);
9036
9149
  return { ctx, workflow: resolved.workflow, payload, contract };
9037
9150
  }
9038
- function parseResult(completed, model, contract) {
9151
+ function throwIfTerminalFailure(completed, model) {
9039
9152
  if (completed.status === "FAILED") {
9040
- throw new Error(`${model.name} failed: ${completed.error ?? "unknown error"}`);
9153
+ throw new ApiError(`${model.name} failed: ${completed.error ?? "unknown error"}`, {
9154
+ status: completed.statusCode ?? 502,
9155
+ code: completed.reason ?? "generation_failed"
9156
+ });
9041
9157
  }
9042
9158
  if (completed.status === "CANCELED") {
9043
- throw new Error(`${model.name} was canceled`);
9159
+ throw new ApiError(`${model.name} was canceled`, { status: 499, code: "canceled" });
9044
9160
  }
9161
+ }
9162
+ function parseResult(completed, model, contract) {
9163
+ throwIfTerminalFailure(completed, model);
9045
9164
  throwIfErrorResult(completed.result, model.name);
9046
9165
  const parsed = contract?.output ? contract.output.parse(completed.result) : completed.result;
9047
9166
  const multiItems = extractAllResults(parsed);
@@ -9054,22 +9173,23 @@ function parseResult(completed, model, contract) {
9054
9173
  }
9055
9174
  const url = extractUrl(parsed);
9056
9175
  if (!url) {
9057
- throw new Error(`${model.name}: unexpected response \u2014 no result URL`);
9176
+ throw new ApiError(`${model.name}: unexpected response \u2014 no result URL`, {
9177
+ status: 502,
9178
+ code: "invalid_response"
9179
+ });
9058
9180
  }
9059
9181
  return { url, results: [{ url }], model: model.id, handle: completed.handle, raw: parsed, usage: completed.usage };
9060
9182
  }
9061
9183
  function parseTextResult(completed, model) {
9062
- if (completed.status === "FAILED") {
9063
- throw new Error(`${model.name} failed: ${completed.error ?? "unknown error"}`);
9064
- }
9065
- if (completed.status === "CANCELED") {
9066
- throw new Error(`${model.name} was canceled`);
9067
- }
9184
+ throwIfTerminalFailure(completed, model);
9068
9185
  throwIfErrorResult(completed.result, model.name);
9069
9186
  throwIfErrorResult(completed.raw, model.name);
9070
9187
  const text = extractText(completed.result) ?? extractText(completed.raw);
9071
9188
  if (text == null) {
9072
- throw new Error(`${model.name}: unexpected response \u2014 no text`);
9189
+ throw new ApiError(`${model.name}: unexpected response \u2014 no text`, {
9190
+ status: 502,
9191
+ code: "invalid_response"
9192
+ });
9073
9193
  }
9074
9194
  return { text, model: model.id, handle: completed.handle, raw: completed.raw ?? completed.result, usage: completed.usage };
9075
9195
  }
@@ -10204,7 +10324,10 @@ function createClient(config) {
10204
10324
  async generate(model, params2, options) {
10205
10325
  const resolved = resolveModel(model);
10206
10326
  if (resolved.mode === "text") {
10207
- throw new Error(`${resolved.name} is a text model \u2014 use generateText() instead.`);
10327
+ throw new ApiError(`${resolved.name} is a text model \u2014 use generateText() instead.`, {
10328
+ status: 400,
10329
+ code: "wrong_model_mode"
10330
+ });
10208
10331
  }
10209
10332
  const { workflow, payload, contract } = prepareRequest(resolved, params2);
10210
10333
  const drive = buildDrivePayloadOptions(resolved, params2, options);
@@ -10223,7 +10346,10 @@ function createClient(config) {
10223
10346
  async generateText(model, params2, options) {
10224
10347
  const resolved = resolveModel(model);
10225
10348
  if (resolved.mode !== "text") {
10226
- throw new Error(`${resolved.name} is not a text model \u2014 use generate() instead.`);
10349
+ throw new ApiError(`${resolved.name} is not a text model \u2014 use generate() instead.`, {
10350
+ status: 400,
10351
+ code: "wrong_model_mode"
10352
+ });
10227
10353
  }
10228
10354
  const { workflow, payload } = prepareRequest(resolved, params2);
10229
10355
  const completed = await executeModel(resolved, workflow, payload, options);
@@ -10297,10 +10423,16 @@ function createClient(config) {
10297
10423
  options
10298
10424
  );
10299
10425
  if (done.status === "FAILED" || done.status === "CANCELED") {
10300
- throw new Error(done.error ?? `${workflow} failed with status ${done.status}`);
10426
+ throw new ApiError(done.error ?? `${workflow} failed with status ${done.status}`, {
10427
+ status: done.statusCode ?? (done.status === "CANCELED" ? 499 : 502),
10428
+ code: done.reason ?? (done.status === "CANCELED" ? "canceled" : "generation_failed")
10429
+ });
10301
10430
  }
10302
10431
  if (done.result === void 0) {
10303
- throw new Error(`${workflow} completed but returned no result`);
10432
+ throw new ApiError(`${workflow} completed but returned no result`, {
10433
+ status: 502,
10434
+ code: "invalid_response"
10435
+ });
10304
10436
  }
10305
10437
  return done.result;
10306
10438
  },
@@ -11397,4 +11529,4 @@ function decodeDeepLinkPayload(encoded) {
11397
11529
  return deserializePayload(encoded);
11398
11530
  }
11399
11531
 
11400
- export { ALL_MODELS, ExecutionMode as ApiRunMode, DEFAULT_VISIBLE_RELEASES, KLING_DUAL_IMAGE_EFFECTS, Model, Models, buildFilename, buildGenerationAttributes, catalog, createClient, decodeDeepLinkPayload, encodeDeepLinkPayload, findModel, getModel, getModelsByMode, getVoiceById, inferResourceType, isVisibleForReleases, parseGeneration, releaseOf, toAvatarOption, toVoiceOption };
11532
+ export { ALL_MODELS, ApiError, ExecutionMode as ApiRunMode, DEFAULT_VISIBLE_RELEASES, KLING_DUAL_IMAGE_EFFECTS, Model, Models, buildFilename, buildGenerationAttributes, catalog, createClient, decodeDeepLinkPayload, encodeDeepLinkPayload, findModel, getModel, getModelsByMode, getVoiceById, inferResourceType, isVisibleForReleases, parseGeneration, releaseOf, toAvatarOption, toVoiceOption };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picsart/ai-sdk",
3
- "version": "5.24.1",
3
+ "version": "5.25.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe SDK for 100+ AI models — image, video, audio, and text generation with Picsart",
6
6
  "license": "MIT",