@scenario-labs/sdk 0.3.0 → 1.1.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 (232) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +40 -8
  3. package/client.d.mts +36 -16
  4. package/client.d.mts.map +1 -1
  5. package/client.d.ts +36 -16
  6. package/client.d.ts.map +1 -1
  7. package/client.js +10 -0
  8. package/client.js.map +1 -1
  9. package/client.mjs +10 -0
  10. package/client.mjs.map +1 -1
  11. package/core/pagination.d.mts +153 -0
  12. package/core/pagination.d.mts.map +1 -0
  13. package/core/pagination.d.ts +153 -0
  14. package/core/pagination.d.ts.map +1 -0
  15. package/core/pagination.js +276 -0
  16. package/core/pagination.js.map +1 -0
  17. package/core/pagination.mjs +263 -0
  18. package/core/pagination.mjs.map +1 -0
  19. package/index.d.mts +5 -1
  20. package/index.d.mts.map +1 -1
  21. package/index.d.ts +5 -1
  22. package/index.d.ts.map +1 -1
  23. package/index.js +14 -1
  24. package/index.js.map +1 -1
  25. package/index.mjs +5 -1
  26. package/index.mjs.map +1 -1
  27. package/lib/asset.d.mts +55 -0
  28. package/lib/asset.d.mts.map +1 -0
  29. package/lib/asset.d.ts +55 -0
  30. package/lib/asset.d.ts.map +1 -0
  31. package/lib/asset.js +60 -0
  32. package/lib/asset.js.map +1 -0
  33. package/lib/asset.mjs +54 -0
  34. package/lib/asset.mjs.map +1 -0
  35. package/lib/job.d.mts +13 -1
  36. package/lib/job.d.mts.map +1 -1
  37. package/lib/job.d.ts +13 -1
  38. package/lib/job.d.ts.map +1 -1
  39. package/lib/job.js +18 -1
  40. package/lib/job.js.map +1 -1
  41. package/lib/job.mjs +16 -0
  42. package/lib/job.mjs.map +1 -1
  43. package/lib/models.d.mts +34 -2
  44. package/lib/models.d.mts.map +1 -1
  45. package/lib/models.d.ts +34 -2
  46. package/lib/models.d.ts.map +1 -1
  47. package/lib/models.js +39 -2
  48. package/lib/models.js.map +1 -1
  49. package/lib/models.mjs +37 -1
  50. package/lib/models.mjs.map +1 -1
  51. package/lib/scenario.d.mts +14 -0
  52. package/lib/scenario.d.mts.map +1 -1
  53. package/lib/scenario.d.ts +14 -0
  54. package/lib/scenario.d.ts.map +1 -1
  55. package/lib/scenario.js +14 -0
  56. package/lib/scenario.js.map +1 -1
  57. package/lib/scenario.mjs +14 -0
  58. package/lib/scenario.mjs.map +1 -1
  59. package/lib/upload.d.mts +143 -0
  60. package/lib/upload.d.mts.map +1 -0
  61. package/lib/upload.d.ts +143 -0
  62. package/lib/upload.d.ts.map +1 -0
  63. package/lib/upload.js +242 -0
  64. package/lib/upload.js.map +1 -0
  65. package/lib/upload.mjs +203 -0
  66. package/lib/upload.mjs.map +1 -0
  67. package/lib/workflow.d.mts +13 -2
  68. package/lib/workflow.d.mts.map +1 -1
  69. package/lib/workflow.d.ts +13 -2
  70. package/lib/workflow.d.ts.map +1 -1
  71. package/lib/workflow.js +17 -4
  72. package/lib/workflow.js.map +1 -1
  73. package/lib/workflow.mjs +17 -4
  74. package/lib/workflow.mjs.map +1 -1
  75. package/lib/workflow_import_validator.d.mts +40 -0
  76. package/lib/workflow_import_validator.d.mts.map +1 -0
  77. package/lib/workflow_import_validator.d.ts +40 -0
  78. package/lib/workflow_import_validator.d.ts.map +1 -0
  79. package/lib/workflow_import_validator.js +69 -0
  80. package/lib/workflow_import_validator.js.map +1 -0
  81. package/lib/workflow_import_validator.mjs +64 -0
  82. package/lib/workflow_import_validator.mjs.map +1 -0
  83. package/package.json +11 -1
  84. package/pagination.d.mts +2 -0
  85. package/pagination.d.mts.map +1 -0
  86. package/pagination.d.ts +2 -0
  87. package/pagination.d.ts.map +1 -0
  88. package/pagination.js +6 -0
  89. package/pagination.js.map +1 -0
  90. package/pagination.mjs +2 -0
  91. package/pagination.mjs.map +1 -0
  92. package/resources/assets/assets.d.mts +646 -669
  93. package/resources/assets/assets.d.mts.map +1 -1
  94. package/resources/assets/assets.d.ts +646 -669
  95. package/resources/assets/assets.d.ts.map +1 -1
  96. package/resources/assets/assets.js +3 -2
  97. package/resources/assets/assets.js.map +1 -1
  98. package/resources/assets/assets.mjs +3 -2
  99. package/resources/assets/assets.mjs.map +1 -1
  100. package/resources/assets/index.d.mts +1 -1
  101. package/resources/assets/index.d.mts.map +1 -1
  102. package/resources/assets/index.d.ts +1 -1
  103. package/resources/assets/index.d.ts.map +1 -1
  104. package/resources/assets/index.js.map +1 -1
  105. package/resources/assets/index.mjs.map +1 -1
  106. package/resources/assets/public.d.mts +12 -12
  107. package/resources/assets/public.d.mts.map +1 -1
  108. package/resources/assets/public.d.ts +12 -12
  109. package/resources/assets/public.d.ts.map +1 -1
  110. package/resources/collections/collections.d.mts +38 -49
  111. package/resources/collections/collections.d.mts.map +1 -1
  112. package/resources/collections/collections.d.ts +38 -49
  113. package/resources/collections/collections.d.ts.map +1 -1
  114. package/resources/collections/collections.js +5 -1
  115. package/resources/collections/collections.js.map +1 -1
  116. package/resources/collections/collections.mjs +5 -1
  117. package/resources/collections/collections.mjs.map +1 -1
  118. package/resources/collections/index.d.mts +1 -1
  119. package/resources/collections/index.d.mts.map +1 -1
  120. package/resources/collections/index.d.ts +1 -1
  121. package/resources/collections/index.d.ts.map +1 -1
  122. package/resources/collections/index.js.map +1 -1
  123. package/resources/collections/index.mjs.map +1 -1
  124. package/resources/generate.d.mts +1027 -242
  125. package/resources/generate.d.mts.map +1 -1
  126. package/resources/generate.d.ts +1027 -242
  127. package/resources/generate.d.ts.map +1 -1
  128. package/resources/generate.js +7 -0
  129. package/resources/generate.js.map +1 -1
  130. package/resources/generate.mjs +7 -0
  131. package/resources/generate.mjs.map +1 -1
  132. package/resources/index.d.mts +8 -8
  133. package/resources/index.d.mts.map +1 -1
  134. package/resources/index.d.ts +8 -8
  135. package/resources/index.d.ts.map +1 -1
  136. package/resources/index.js.map +1 -1
  137. package/resources/index.mjs.map +1 -1
  138. package/resources/jobs.d.mts +369 -380
  139. package/resources/jobs.d.mts.map +1 -1
  140. package/resources/jobs.d.ts +369 -380
  141. package/resources/jobs.d.ts.map +1 -1
  142. package/resources/jobs.js +2 -1
  143. package/resources/jobs.js.map +1 -1
  144. package/resources/jobs.mjs +2 -1
  145. package/resources/jobs.mjs.map +1 -1
  146. package/resources/models/description.d.mts +2 -2
  147. package/resources/models/description.d.mts.map +1 -1
  148. package/resources/models/description.d.ts +2 -2
  149. package/resources/models/description.d.ts.map +1 -1
  150. package/resources/models/examples.d.mts +4 -4
  151. package/resources/models/examples.d.mts.map +1 -1
  152. package/resources/models/examples.d.ts +4 -4
  153. package/resources/models/examples.d.ts.map +1 -1
  154. package/resources/models/index.d.mts +1 -1
  155. package/resources/models/index.d.mts.map +1 -1
  156. package/resources/models/index.d.ts +1 -1
  157. package/resources/models/index.d.ts.map +1 -1
  158. package/resources/models/index.js.map +1 -1
  159. package/resources/models/index.mjs.map +1 -1
  160. package/resources/models/models.d.mts +845 -856
  161. package/resources/models/models.d.mts.map +1 -1
  162. package/resources/models/models.d.ts +845 -856
  163. package/resources/models/models.d.ts.map +1 -1
  164. package/resources/models/models.js +2 -1
  165. package/resources/models/models.js.map +1 -1
  166. package/resources/models/models.mjs +2 -1
  167. package/resources/models/models.mjs.map +1 -1
  168. package/resources/models/train.d.mts +2 -2
  169. package/resources/models/train.d.mts.map +1 -1
  170. package/resources/models/train.d.ts +2 -2
  171. package/resources/models/train.d.ts.map +1 -1
  172. package/resources/search.d.mts +215 -295
  173. package/resources/search.d.mts.map +1 -1
  174. package/resources/search.d.ts +215 -295
  175. package/resources/search.d.ts.map +1 -1
  176. package/resources/search.js +9 -2
  177. package/resources/search.js.map +1 -1
  178. package/resources/search.mjs +9 -2
  179. package/resources/search.mjs.map +1 -1
  180. package/resources/tags.d.mts +23 -35
  181. package/resources/tags.d.mts.map +1 -1
  182. package/resources/tags.d.ts +23 -35
  183. package/resources/tags.d.ts.map +1 -1
  184. package/resources/tags.js +2 -1
  185. package/resources/tags.js.map +1 -1
  186. package/resources/tags.mjs +2 -1
  187. package/resources/tags.mjs.map +1 -1
  188. package/resources/usages.d.mts +3 -3
  189. package/resources/usages.d.mts.map +1 -1
  190. package/resources/usages.d.ts +3 -3
  191. package/resources/usages.d.ts.map +1 -1
  192. package/resources/workflows.d.mts +688 -704
  193. package/resources/workflows.d.mts.map +1 -1
  194. package/resources/workflows.d.ts +688 -704
  195. package/resources/workflows.d.ts.map +1 -1
  196. package/resources/workflows.js +5 -1
  197. package/resources/workflows.js.map +1 -1
  198. package/resources/workflows.mjs +5 -1
  199. package/resources/workflows.mjs.map +1 -1
  200. package/src/client.ts +107 -2
  201. package/src/core/pagination.ts +497 -0
  202. package/src/index.ts +11 -1
  203. package/src/lib/asset.ts +85 -0
  204. package/src/lib/job.ts +28 -1
  205. package/src/lib/models.ts +62 -2
  206. package/src/lib/scenario.ts +14 -0
  207. package/src/lib/upload.ts +344 -0
  208. package/src/lib/workflow.ts +29 -4
  209. package/src/lib/workflow_import_validator.ts +104 -0
  210. package/src/pagination.ts +2 -0
  211. package/src/resources/assets/assets.ts +930 -909
  212. package/src/resources/assets/index.ts +2 -0
  213. package/src/resources/assets/public.ts +17 -7
  214. package/src/resources/collections/collections.ts +45 -54
  215. package/src/resources/collections/index.ts +1 -0
  216. package/src/resources/generate.ts +1227 -136
  217. package/src/resources/index.ts +11 -1
  218. package/src/resources/jobs.ts +613 -622
  219. package/src/resources/models/description.ts +4 -0
  220. package/src/resources/models/examples.ts +8 -0
  221. package/src/resources/models/index.ts +1 -0
  222. package/src/resources/models/models.ts +1066 -1064
  223. package/src/resources/models/train.ts +4 -0
  224. package/src/resources/search.ts +239 -327
  225. package/src/resources/tags.ts +32 -39
  226. package/src/resources/usages.ts +5 -0
  227. package/src/resources/workflows.ts +850 -864
  228. package/src/version.ts +1 -1
  229. package/version.d.mts +1 -1
  230. package/version.d.ts +1 -1
  231. package/version.js +1 -1
  232. package/version.mjs +1 -1
package/src/lib/models.ts CHANGED
@@ -1,8 +1,55 @@
1
1
  import type { APIPromise } from '../core/api-promise';
2
2
  import type { RequestOptions } from '../internal/request-options';
3
- import { Models } from '../resources/models/models';
3
+ import type { GenerateRunModelParams, GenerateRunModelResponse } from '../resources/generate';
4
+ import { Models, type ModelRetrieveParams, type ModelRetrieveResponse } from '../resources/models/models';
4
5
  import { Train, type TrainTriggerParams, type TrainTriggerResponse } from '../resources/models/train';
5
6
  import { type WithJob, enhanceJob } from './job';
7
+ // Type-only import to break the circular `scenario.ts` ↔ `models.ts` graph at runtime.
8
+ import type { Scenario } from './scenario';
9
+
10
+ /**
11
+ * Enhanced Model entity with helper methods.
12
+ * Has all original model fields plus `.run()`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const { model } = await client.models.retrieve(modelId);
17
+ * const gen = await model.run({ body: { prompt: 'a red car' } });
18
+ * const completed = await gen.job.wait();
19
+ * ```
20
+ */
21
+ export class ModelEntity {
22
+ /** @internal */ declare readonly _client: Scenario;
23
+
24
+ /**
25
+ * Generate with this model. Shortcut for
26
+ * `client.generate.runModel(this.id, params)`.
27
+ *
28
+ * Returns an enhanced response where `response.job` has `.wait()`.
29
+ */
30
+ run(
31
+ this: ModelEntity & ModelRetrieveResponse.Model,
32
+ params: GenerateRunModelParams,
33
+ options?: RequestOptions,
34
+ ): APIPromise<WithJob<GenerateRunModelResponse>> {
35
+ return this._client.generate.runModel(this.id, params, options);
36
+ }
37
+
38
+ /** @internal Create a ModelEntity wrapping raw model data. */
39
+ static from(
40
+ client: Scenario,
41
+ data: ModelRetrieveResponse.Model,
42
+ ): ModelRetrieveResponse.Model & ModelEntity {
43
+ const entity = Object.assign(Object.create(ModelEntity.prototype), data);
44
+ Object.defineProperty(entity, '_client', { value: client, enumerable: false });
45
+ return entity;
46
+ }
47
+ }
48
+
49
+ /** Enhanced retrieve response where `model` has `.run()` on top of its original fields. */
50
+ export type EnhancedModelRetrieveResponse = Omit<ModelRetrieveResponse, 'model'> & {
51
+ model: ModelRetrieveResponse.Model & ModelEntity;
52
+ };
6
53
 
7
54
  /**
8
55
  * Enhanced Train resource.
@@ -20,8 +67,21 @@ export class EnhancedTrain extends Train {
20
67
 
21
68
  /**
22
69
  * Enhanced Models resource.
23
- * All original methods inherited. `train` is an {@link EnhancedTrain}.
70
+ * All original methods inherited.
71
+ * - `train` is an {@link EnhancedTrain}
72
+ * - `retrieve()` returns a response where `model` has `.run()`
24
73
  */
25
74
  export class EnhancedModels extends Models {
26
75
  override train: EnhancedTrain = new EnhancedTrain(this._client);
76
+
77
+ override retrieve(
78
+ modelID: string,
79
+ query: ModelRetrieveParams | null | undefined = {},
80
+ options?: RequestOptions,
81
+ ): APIPromise<EnhancedModelRetrieveResponse> {
82
+ return super.retrieve(modelID, query, options)._thenUnwrap((data) => ({
83
+ ...data,
84
+ model: ModelEntity.from(this._client as Scenario, data.model),
85
+ }));
86
+ }
27
87
  }
@@ -1,6 +1,9 @@
1
1
  import { Scenario as BaseScenario } from '../client';
2
+ import { EnhancedAssets } from './asset';
2
3
  import { EnhancedGenerate } from './generate';
4
+ import { EnhancedJobs } from './job';
3
5
  import { EnhancedModels } from './models';
6
+ import { EnhancedUploads } from './upload';
4
7
  import { EnhancedWorkflows } from './workflow';
5
8
 
6
9
  /**
@@ -30,10 +33,21 @@ import { EnhancedWorkflows } from './workflow';
30
33
  * // Enhanced: generate job.wait()
31
34
  * const gen = await client.generate.runModel(modelId, { body });
32
35
  * const done = await gen.job.wait();
36
+ *
37
+ * // Enhanced: upload.wait()
38
+ * const res = await client.uploads.retrieve(uploadId);
39
+ * const imported = await res.upload.wait();
40
+ *
41
+ * // Enhanced: asset.download()
42
+ * const a = await client.assets.retrieve(assetId);
43
+ * const bytes = await a.asset.download();
33
44
  * ```
34
45
  */
35
46
  export class Scenario extends BaseScenario {
36
47
  override workflows: EnhancedWorkflows = new EnhancedWorkflows(this);
37
48
  override generate: EnhancedGenerate = new EnhancedGenerate(this);
38
49
  override models: EnhancedModels = new EnhancedModels(this);
50
+ override uploads: EnhancedUploads = new EnhancedUploads(this);
51
+ override assets: EnhancedAssets = new EnhancedAssets(this);
52
+ override jobs: EnhancedJobs = new EnhancedJobs(this);
39
53
  }
@@ -0,0 +1,344 @@
1
+ import type { Scenario } from '../client';
2
+ import type { APIPromise } from '../core/api-promise';
3
+ import type { RequestOptions } from '../internal/request-options';
4
+ import {
5
+ Uploads,
6
+ type UploadCreateParams,
7
+ type UploadCreateResponse,
8
+ type UploadRetrieveResponse,
9
+ type UploadTriggerActionParams,
10
+ type UploadTriggerActionResponse,
11
+ } from '../resources/uploads';
12
+ import type { AssetRetrieveResponse } from '../resources/assets/assets';
13
+ import type { ModelRetrieveResponse } from '../resources/models/models';
14
+
15
+ type UploadKind = NonNullable<UploadCreateParams['kind']>;
16
+
17
+ /** The resolved entity returned after a successful upload, narrowed by `kind`. */
18
+ export type UploadResult<K extends UploadKind> =
19
+ K extends 'model' ? ModelRetrieveResponse : AssetRetrieveResponse;
20
+
21
+ /**
22
+ * Accepted upload inputs.
23
+ * - `string` — a local file path, read from disk (Node only; throws in browsers).
24
+ * - `Uint8Array` — raw bytes. `Buffer` is a `Uint8Array` and works too.
25
+ * - `Blob` / `File` — browser-native, or Node ≥ 18 via `fs.openAsBlob()` /
26
+ * constructors.
27
+ */
28
+ export type UploadFileInput = string | Uint8Array | Blob;
29
+
30
+ export interface UploadFileParams<K extends UploadKind> {
31
+ /**
32
+ * The file to upload. Accepts a local path (Node), a `Uint8Array` / `Buffer`,
33
+ * or a `Blob` / `File`.
34
+ */
35
+ file: UploadFileInput;
36
+ /** Original filename (e.g. `"my-model.safetensors"`). */
37
+ fileName: string;
38
+ /** MIME type (e.g. `"application/octet-stream"`, `"image/png"`). */
39
+ contentType: string;
40
+ /** The kind of upload. Determines the returned entity type. */
41
+ kind: K;
42
+ /**
43
+ * Extra asset options. Ignored when `kind === 'model'` — models don't
44
+ * produce an asset, so these options are not applicable server-side.
45
+ */
46
+ assetOptions?: UploadCreateParams['assetOptions'];
47
+ /**
48
+ * Maximum number of parts uploaded to S3 in parallel.
49
+ * @default 4
50
+ */
51
+ partConcurrency?: number;
52
+ /**
53
+ * Polling interval for the final "upload imported" check, in ms.
54
+ * @default 2000
55
+ */
56
+ pollIntervalMs?: number;
57
+ /**
58
+ * Maximum time to wait for the upload to reach a terminal state, in ms.
59
+ * Throws if exceeded.
60
+ * @default 300000
61
+ */
62
+ pollTimeoutMs?: number;
63
+ }
64
+
65
+ // AWS S3 multipart upload limits.
66
+ const MIN_PART_SIZE = 6_000_000; // 6 MB (spec minimum is 5 MiB; a bit over to be safe)
67
+ const MAX_PART_SIZE = 5_368_709_120; // 5 GiB
68
+ const MAX_PARTS = 10_000;
69
+ const MAX_FILE_SIZE = 5_497_558_138_880; // 5 TiB
70
+
71
+ type UploadStatus = UploadRetrieveResponse.Upload['status'];
72
+
73
+ // `validated` is intermediate — the server has validated the upload but
74
+ // hasn't created the asset/model yet, so `entityId` isn't populated.
75
+ // Real terminals are `imported` (asset/model created), `complete`, and
76
+ // `failed`. Everything else means "keep polling".
77
+ const TERMINAL_STATUSES: Set<UploadStatus> = new Set(['complete', 'failed', 'imported']);
78
+
79
+ export interface UploadWaitOptions {
80
+ /** Polling interval in milliseconds. Default: 2000 */
81
+ intervalMs?: number;
82
+ /** Maximum wait time in milliseconds. Default: 300000 */
83
+ timeoutMs?: number;
84
+ }
85
+
86
+ /** Upload methods added on top of the original upload fields. */
87
+ export class UploadMethods {
88
+ /** @internal */ declare readonly _client: Scenario;
89
+
90
+ /**
91
+ * Poll until the upload has been processed into an entity (asset or model),
92
+ * or reaches a terminal state (`imported`, `complete`, `failed`). Resolves
93
+ * with the latest upload data so you can read `entityId`, `status`, or
94
+ * `errorMessage` without a follow-up retrieve.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const res = await client.uploads.retrieve(uploadId);
99
+ * const done = await res.upload.wait();
100
+ * if (done.status === 'failed') throw new Error(done.errorMessage);
101
+ * console.log(done.entityId);
102
+ * ```
103
+ */
104
+ async wait(this: Upload, options?: UploadWaitOptions): Promise<Upload> {
105
+ const final = await pollUntilImported(
106
+ () => this._client.uploads.retrieve(this.id),
107
+ options?.intervalMs ?? 2_000,
108
+ options?.timeoutMs ?? 300_000,
109
+ );
110
+ return UploadMethods.from(this._client, final.upload);
111
+ }
112
+
113
+ /** @internal Create an Upload from raw upload data. */
114
+ static from(client: Scenario, data: UploadRetrieveResponse['upload']): Upload {
115
+ const upload = Object.assign(Object.create(UploadMethods.prototype), data) as Upload;
116
+ Object.defineProperty(upload, '_client', { value: client, enumerable: false });
117
+ return upload;
118
+ }
119
+ }
120
+
121
+ /** An upload with all original fields plus `.wait()`. */
122
+ export type Upload = UploadRetrieveResponse.Upload & UploadMethods;
123
+ export const Upload = UploadMethods;
124
+
125
+ /**
126
+ * @internal Helper type: adds `.wait()` to the `upload` field of a response.
127
+ * Uses intersection so the original response type is preserved — a `WithUpload<T>` is still assignable to `T`.
128
+ */
129
+ export type WithUpload<T extends { upload: unknown }> = T & { upload: UploadMethods };
130
+
131
+ /**
132
+ * @internal Wrap `_thenUnwrap` to replace `response.upload` with an enhanced Upload.
133
+ */
134
+ export function enhanceUpload<T extends { upload: { id: string; status: string } }>(
135
+ client: Scenario,
136
+ promise: APIPromise<T>,
137
+ ): APIPromise<WithUpload<T>> {
138
+ return promise._thenUnwrap((data) => ({
139
+ ...data,
140
+ upload: Upload.from(client, data.upload as UploadRetrieveResponse['upload']),
141
+ }));
142
+ }
143
+
144
+ /**
145
+ * Enhanced Uploads resource.
146
+ * All original methods (`create`, `retrieve`, `triggerAction`) are inherited,
147
+ * with each response's `upload` field enriched with a `.wait()` helper.
148
+ * Adds `uploadFile()` — a one-call wrapper around the 4-step upload flow:
149
+ * init → PUT parts to S3 → trigger complete → poll until imported.
150
+ */
151
+ export class EnhancedUploads extends Uploads {
152
+ override create(
153
+ body: UploadCreateParams,
154
+ options?: RequestOptions,
155
+ ): APIPromise<WithUpload<UploadCreateResponse>> {
156
+ return enhanceUpload(this._client, super.create(body, options));
157
+ }
158
+
159
+ override retrieve(
160
+ uploadID: string,
161
+ options?: RequestOptions,
162
+ ): APIPromise<WithUpload<UploadRetrieveResponse>> {
163
+ return enhanceUpload(this._client, super.retrieve(uploadID, options));
164
+ }
165
+
166
+ override triggerAction(
167
+ uploadID: string,
168
+ body: UploadTriggerActionParams,
169
+ options?: RequestOptions,
170
+ ): APIPromise<WithUpload<UploadTriggerActionResponse>> {
171
+ return enhanceUpload(this._client, super.triggerAction(uploadID, body, options));
172
+ }
173
+
174
+ /**
175
+ * Upload a file end-to-end. Computes an optimal part size (within AWS's
176
+ * 5 MiB / 5 GiB / 10,000-part bounds), uploads all parts in parallel,
177
+ * triggers completion, polls until the server has validated and imported
178
+ * the file, and returns the resolved entity.
179
+ *
180
+ * The returned type narrows on `kind`: `'model'` returns a Model,
181
+ * everything else returns an Asset.
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * // From a file path (Node)
186
+ * const { asset } = await client.uploads.uploadFile({
187
+ * file: './photo.jpg',
188
+ * fileName: 'photo.jpg',
189
+ * contentType: 'image/jpeg',
190
+ * kind: 'image',
191
+ * });
192
+ * console.log(asset.id, asset.url);
193
+ *
194
+ * // Or from bytes / Blob — anything you already have in memory
195
+ * await client.uploads.uploadFile({
196
+ * file: new Blob([pngBytes], { type: 'image/png' }),
197
+ * fileName: 'generated.png',
198
+ * contentType: 'image/png',
199
+ * kind: 'image',
200
+ * });
201
+ * ```
202
+ */
203
+ async uploadFile<K extends UploadKind>(
204
+ params: UploadFileParams<K>,
205
+ options?: RequestOptions,
206
+ ): Promise<UploadResult<K>> {
207
+ const bytes = await normalizeToBytes(params.file);
208
+ const fileSize = bytes.byteLength;
209
+
210
+ if (fileSize === 0) {
211
+ throw new Error('Cannot upload an empty file');
212
+ }
213
+ if (fileSize > MAX_FILE_SIZE) {
214
+ throw new Error(
215
+ `File size (${fileSize} bytes) exceeds the maximum allowed size of ${MAX_FILE_SIZE} bytes (5 TiB)`,
216
+ );
217
+ }
218
+
219
+ // Pick the smallest legal part size that fits within MAX_PARTS.
220
+ const requiredPartSize = Math.ceil(fileSize / MAX_PARTS);
221
+ const partSize = Math.max(MIN_PART_SIZE, requiredPartSize);
222
+ if (partSize > MAX_PART_SIZE) {
223
+ throw new Error(
224
+ `File size (${fileSize} bytes) requires a part size of ${partSize} bytes, ` +
225
+ `which exceeds the maximum part size of ${MAX_PART_SIZE} bytes (5 GiB)`,
226
+ );
227
+ }
228
+ const partsCount = Math.max(1, Math.ceil(fileSize / partSize));
229
+
230
+ // Step 1 — init upload session and receive presigned S3 URLs.
231
+ const createBody: UploadCreateParams = {
232
+ fileName: params.fileName,
233
+ fileSize,
234
+ contentType: params.contentType,
235
+ kind: params.kind,
236
+ parts: partsCount,
237
+ ...(params.kind !== 'model' && params.assetOptions ? { assetOptions: params.assetOptions } : {}),
238
+ };
239
+ const createRes: UploadCreateResponse = await this.create(createBody, options);
240
+ const session = createRes.upload;
241
+ const uploadId = session.id;
242
+ const parts = session.parts;
243
+ if (parts?.length !== partsCount) {
244
+ throw new Error(`Expected ${partsCount} presigned part URL(s), got ${parts?.length ?? 0}`);
245
+ }
246
+
247
+ // Step 2 — PUT each chunk to its presigned S3 URL, with a concurrency cap.
248
+ const concurrency = Math.max(1, params.partConcurrency ?? 4);
249
+ await uploadPartsWithConcurrency(bytes, parts, partSize, concurrency);
250
+
251
+ // Step 3 — tell the server all parts are uploaded.
252
+ await this.triggerAction(uploadId, { action: 'complete' }, options);
253
+
254
+ // Step 4 — poll until the upload has been processed into an entity.
255
+ const final = await pollUntilImported(
256
+ () => this.retrieve(uploadId, options),
257
+ params.pollIntervalMs ?? 2_000,
258
+ params.pollTimeoutMs ?? 300_000,
259
+ );
260
+
261
+ if (final.upload.status === 'failed') {
262
+ throw new Error(`Upload ${uploadId} failed: ${final.upload.errorMessage ?? '<no error message>'}`);
263
+ }
264
+ const entityId = final.upload.entityId;
265
+ if (!entityId) {
266
+ throw new Error(`Upload ${uploadId} reached status "${final.upload.status}" but has no entityId`);
267
+ }
268
+
269
+ // Fetch and return the resolved entity (narrowed by kind).
270
+ if (params.kind === 'model') {
271
+ const model = await this._client.models.retrieve(entityId);
272
+ return model as UploadResult<K>;
273
+ }
274
+ const asset = await this._client.assets.retrieve(entityId);
275
+ return asset as UploadResult<K>;
276
+ }
277
+ }
278
+
279
+ // ─────────────────────── helpers ───────────────────────
280
+
281
+ async function normalizeToBytes(input: UploadFileInput): Promise<Uint8Array> {
282
+ if (typeof input === 'string') {
283
+ // Dynamic import so browser bundlers don't pull in node:fs.
284
+ const { readFile } = await import('node:fs/promises');
285
+ return new Uint8Array(await readFile(input));
286
+ }
287
+ if (input instanceof Uint8Array) {
288
+ return input;
289
+ }
290
+ if (typeof Blob !== 'undefined' && input instanceof Blob) {
291
+ return new Uint8Array(await input.arrayBuffer());
292
+ }
293
+ throw new Error('uploadFile: unsupported input type — expected a path string, Uint8Array, or Blob');
294
+ }
295
+
296
+ async function uploadPartsWithConcurrency(
297
+ bytes: Uint8Array,
298
+ parts: Array<{ number: number; url: string; expires: string }>,
299
+ partSize: number,
300
+ concurrency: number,
301
+ ): Promise<void> {
302
+ // Sort by part number so slicing lines up with the presigned URL order.
303
+ const sorted = [...parts].sort((a, b) => a.number - b.number);
304
+
305
+ let nextIndex = 0;
306
+ const workers = Array.from({ length: concurrency }, () => worker());
307
+ await Promise.all(workers);
308
+
309
+ async function worker() {
310
+ while (true) {
311
+ const i = nextIndex++;
312
+ if (i >= sorted.length) return;
313
+ const part = sorted[i];
314
+ if (!part) continue;
315
+ const start = i * partSize;
316
+ const end = Math.min(bytes.byteLength, start + partSize);
317
+ const chunk = bytes.subarray(start, end);
318
+ const res = await fetch(part.url, { method: 'PUT', body: chunk });
319
+ if (!res.ok) {
320
+ throw new Error(
321
+ `Failed to upload part ${part.number}/${sorted.length}: ${res.status} ${res.statusText}`,
322
+ );
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ async function pollUntilImported(
329
+ retrieve: () => Promise<UploadRetrieveResponse>,
330
+ intervalMs: number,
331
+ timeoutMs: number,
332
+ ): Promise<UploadRetrieveResponse> {
333
+ const deadline = Date.now() + timeoutMs;
334
+ while (Date.now() < deadline) {
335
+ const res = await retrieve();
336
+ const { status } = res.upload;
337
+ // Return as soon as we have an entityId OR the upload reached a terminal state.
338
+ if (res.upload.entityId || TERMINAL_STATUSES.has(status)) {
339
+ return res;
340
+ }
341
+ await new Promise((r) => setTimeout(r, intervalMs));
342
+ }
343
+ throw new Error(`Upload did not reach a terminal status within ${timeoutMs / 1000}s`);
344
+ }
@@ -10,23 +10,29 @@ import {
10
10
  } from '../resources/workflows';
11
11
  import { type WithJob, enhanceJob } from './job';
12
12
  import { validateWorkflowFlow } from './workflow_validator';
13
+ // Type-only import to break the circular `scenario.ts` ↔ `workflow.ts` graph at runtime.
14
+ import type { Scenario } from './scenario';
13
15
 
14
16
  type DefinitionFlow = WorkflowRetrieveResponse.Workflow.Flow;
15
17
  type NodeType = DefinitionFlow['type'];
16
18
 
17
19
  /**
18
20
  * Enhanced workflow object with helper methods.
19
- * Has all original workflow fields plus `findNode()`, `getNodesByType()`.
21
+ * Has all original workflow fields plus `findNode()`, `getNodesByType()`,
22
+ * `validate()`, and `run()`.
20
23
  *
21
24
  * @example
22
25
  * ```ts
23
26
  * const response = await client.workflows.retrieve(workflowId);
24
27
  * response.workflow.findNode('final_image');
25
28
  * response.workflow.getNodesByType('custom-model');
29
+ * const gen = await response.workflow.run({ body: { prompt: '...' } });
26
30
  * response.workflow.name; // original fields still work
27
31
  * ```
28
32
  */
29
33
  export class WorkflowEntity {
34
+ /** @internal */ declare readonly _client: Scenario;
35
+
30
36
  /** Find a node in the workflow definition by ID. */
31
37
  findNode(
32
38
  this: WorkflowEntity & WorkflowRetrieveResponse.Workflow,
@@ -48,9 +54,28 @@ export class WorkflowEntity {
48
54
  validateWorkflowFlow(this.flow);
49
55
  }
50
56
 
57
+ /**
58
+ * Run this workflow. Shortcut for
59
+ * `client.workflows.run(this.id, params)`.
60
+ *
61
+ * Returns an enhanced response where `response.job` has `.wait()`.
62
+ */
63
+ run(
64
+ this: WorkflowEntity & WorkflowRetrieveResponse.Workflow,
65
+ params: WorkflowRunParams,
66
+ options?: RequestOptions,
67
+ ): APIPromise<WithJob<WorkflowRunResponse>> {
68
+ return this._client.workflows.run(this.id, params, options);
69
+ }
70
+
51
71
  /** @internal Create a WorkflowEntity from raw workflow data. */
52
- static from(data: WorkflowRetrieveResponse.Workflow): WorkflowRetrieveResponse.Workflow & WorkflowEntity {
53
- return Object.assign(Object.create(WorkflowEntity.prototype), data);
72
+ static from(
73
+ client: Scenario,
74
+ data: WorkflowRetrieveResponse.Workflow,
75
+ ): WorkflowRetrieveResponse.Workflow & WorkflowEntity {
76
+ const entity = Object.assign(Object.create(WorkflowEntity.prototype), data);
77
+ Object.defineProperty(entity, '_client', { value: client, enumerable: false });
78
+ return entity;
54
79
  }
55
80
  }
56
81
 
@@ -75,7 +100,7 @@ export class EnhancedWorkflows extends Workflows {
75
100
  ): APIPromise<EnhancedWorkflowRetrieveResponse> {
76
101
  return super.retrieve(workflowID, options)._thenUnwrap((data) => ({
77
102
  ...data,
78
- workflow: WorkflowEntity.from(data.workflow),
103
+ workflow: WorkflowEntity.from(this._client as Scenario, data.workflow),
79
104
  }));
80
105
  }
81
106
 
@@ -0,0 +1,104 @@
1
+ export class WorkflowImportError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'WorkflowImportError';
5
+ }
6
+ }
7
+
8
+ export type WorkflowEditorNodeType =
9
+ | 'text'
10
+ | 'asset'
11
+ | 'model'
12
+ | 'llm'
13
+ | 'transformText'
14
+ | 'splitText'
15
+ | 'ifElse'
16
+ | 'groupItems'
17
+ | 'sliceAssets'
18
+ | 'forEach'
19
+ | 'forEachEnd'
20
+ | 'stickyNote';
21
+
22
+ export const VALID_EDITOR_NODE_TYPES: readonly WorkflowEditorNodeType[] = [
23
+ 'text',
24
+ 'asset',
25
+ 'model',
26
+ 'llm',
27
+ 'transformText',
28
+ 'splitText',
29
+ 'ifElse',
30
+ 'groupItems',
31
+ 'sliceAssets',
32
+ 'forEach',
33
+ 'forEachEnd',
34
+ 'stickyNote',
35
+ ];
36
+
37
+ export interface WorkflowExportData {
38
+ version: '1.0';
39
+ name: string;
40
+ description: string;
41
+ editorInfo: {
42
+ nodes: Array<{ type: WorkflowEditorNodeType; [key: string]: unknown }>;
43
+ edges: Array<{ [key: string]: unknown }>;
44
+ inputKeys: string[];
45
+ };
46
+ inputs: unknown[];
47
+ tagSet: string[];
48
+ uiConfig?: object;
49
+ exportedAt: string;
50
+ exportedBy: string;
51
+ }
52
+
53
+ /**
54
+ * Validates a workflow export payload (e.g. from a JSON file import).
55
+ *
56
+ * Throws a `WorkflowImportError` with a human-readable message on the first
57
+ * violation found. Returns the validated data cast to `WorkflowExportData`.
58
+ *
59
+ * Checks performed:
60
+ * 1. Data is a non-null object
61
+ * 2. `version` field is present and equals "1.0"
62
+ * 3. `editorInfo` has non-empty `nodes` and `edges` arrays
63
+ * 4. Each node's `type` is a recognized editor node type
64
+ * 5. `inputs` is an array
65
+ */
66
+ export function validateEditorInfo(data: unknown): WorkflowExportData {
67
+ if (typeof data !== 'object' || data === null) {
68
+ throw new WorkflowImportError('Invalid JSON format');
69
+ }
70
+
71
+ const workflow = data as Partial<WorkflowExportData>;
72
+
73
+ if (!workflow.version) {
74
+ throw new WorkflowImportError('Missing version field');
75
+ }
76
+
77
+ if (workflow.version !== '1.0') {
78
+ throw new WorkflowImportError(`Unsupported version: ${workflow.version}`);
79
+ }
80
+
81
+ if (!workflow.editorInfo || !workflow.editorInfo.nodes || !workflow.editorInfo.edges) {
82
+ throw new WorkflowImportError('Missing editorInfo structure');
83
+ }
84
+
85
+ if (!Array.isArray(workflow.editorInfo.nodes)) {
86
+ throw new WorkflowImportError('Invalid nodes array');
87
+ }
88
+
89
+ if (!Array.isArray(workflow.editorInfo.edges)) {
90
+ throw new WorkflowImportError('Invalid edges array');
91
+ }
92
+
93
+ if (!workflow.inputs || !Array.isArray(workflow.inputs)) {
94
+ throw new WorkflowImportError('Invalid inputs array');
95
+ }
96
+
97
+ for (const node of workflow.editorInfo.nodes) {
98
+ if (!(VALID_EDITOR_NODE_TYPES as readonly string[]).includes(node.type)) {
99
+ throw new WorkflowImportError(`Invalid node type: ${node.type}`);
100
+ }
101
+ }
102
+
103
+ return workflow as WorkflowExportData;
104
+ }
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import from ./core/pagination instead */
2
+ export * from './core/pagination';