@toloka-platform/sdk 0.1.0-preview.5 → 0.1.0-preview.6

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 (40) hide show
  1. package/dist/cli/toloka.js +121 -120
  2. package/dist/cli/toloka.js.jsc +0 -0
  3. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  4. package/dist/cli/toloka.js.sha256 +1 -1
  5. package/docs/cli.md +26 -1
  6. package/docs/datasets.md +8 -9
  7. package/docs/limitations.md +5 -8
  8. package/docs/troubleshooting.md +0 -11
  9. package/openapi/gen/api.ts +779 -895
  10. package/openapi/gen/models/baseModel.ts +9 -0
  11. package/openapi/gen/models/baseModelId.ts +2 -0
  12. package/openapi/gen/models/baseModelIdProperty.ts +2 -0
  13. package/openapi/gen/models/createProjectModelBody.ts +8 -0
  14. package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
  15. package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  16. package/openapi/gen/models/index.ts +16 -0
  17. package/openapi/gen/models/listBaseModels200.ts +5 -0
  18. package/openapi/gen/models/listModelVersions200.ts +7 -0
  19. package/openapi/gen/models/listModelVersionsParams.ts +5 -0
  20. package/openapi/gen/models/listProjectModels200.ts +7 -0
  21. package/openapi/gen/models/listProjectModelsParams.ts +5 -0
  22. package/openapi/gen/models/model.ts +16 -0
  23. package/openapi/gen/models/modelArchivedAt.ts +2 -0
  24. package/openapi/gen/models/modelDescription.ts +2 -0
  25. package/openapi/gen/models/modelId.ts +2 -0
  26. package/openapi/gen/models/modelVersion.ts +9 -0
  27. package/openapi/gen/models/modelVersionId.ts +2 -0
  28. package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  29. package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  30. package/package.json +1 -1
  31. package/skills/catalog.json +4 -4
  32. package/skills/toloka-build/SKILL.md +1 -1
  33. package/skills/toloka-build/references/workflow-and-cli.md +12 -1
  34. package/skills/toloka-report-issue/references/report-template.md +1 -1
  35. package/src/cli/data.ts +148 -25
  36. package/src/cli/models.ts +188 -0
  37. package/src/cli/program.ts +99 -1
  38. package/src/client.ts +109 -35
  39. package/src/errors.ts +25 -10
  40. package/src/transport.ts +16 -13
@@ -0,0 +1,9 @@
1
+ import type { BaseModelIdProperty } from './baseModelIdProperty';
2
+
3
+ export interface BaseModel {
4
+ id: BaseModelIdProperty;
5
+ displayName: string;
6
+ provider: string;
7
+ upstreamModel: string;
8
+ upstreamRevision: string;
9
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export type BaseModelId = 'qwen3-4b' | 'qwen3-8b' | 'qwen3-32b' | 'qwen3-235b';
@@ -0,0 +1,2 @@
1
+
2
+ export type BaseModelIdProperty = 'qwen3-4b' | 'qwen3-8b' | 'qwen3-32b' | 'qwen3-235b';
@@ -0,0 +1,8 @@
1
+ import type { CreateProjectModelBodyDescription } from './createProjectModelBodyDescription';
2
+ import type { BaseModelId } from './baseModelId';
3
+
4
+ export type CreateProjectModelBody = {
5
+ name: string;
6
+ description?: CreateProjectModelBodyDescription;
7
+ baseModelId: BaseModelId;
8
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export type CreateProjectModelBodyDescription = string | null;
@@ -6,4 +6,5 @@ export type GenerationNodeSettingsAudienceAndPricingOneOfOneone = {
6
6
  generationModelId?: GenerationNodeSettingsAudienceAndPricingOneOfOneoneGenerationModelId;
7
7
  reasoningEffort?: GenerationNodeSettingsAudienceAndPricingOneOfOneoneReasoningEffort;
8
8
  llmInstruction?: string;
9
+ maxCostPerAttempt: number;
9
10
  };
@@ -19,6 +19,9 @@ export * from './attachPipelineDatasetBodyItemFilterFieldValuesItemSourcesItem';
19
19
  export * from './attachPipelineDatasetBodyItemFilterFieldValuesItemSourcesItemOneOf';
20
20
  export * from './attachPipelineDatasetBodyItemFilterFieldValuesItemSourcesItemOneOfTwo';
21
21
  export * from './attachPipelineDatasetBodyItemFilterFieldValuesItemValue';
22
+ export * from './baseModel';
23
+ export * from './baseModelId';
24
+ export * from './baseModelIdProperty';
22
25
  export * from './checkJob';
23
26
  export * from './checkJobError';
24
27
  export * from './checkJobResult';
@@ -96,6 +99,8 @@ export * from './createProjectBody';
96
99
  export * from './createProjectBodyDeliveryOrderId';
97
100
  export * from './createProjectFileUpload200';
98
101
  export * from './createProjectFileUploadBody';
102
+ export * from './createProjectModelBody';
103
+ export * from './createProjectModelBodyDescription';
99
104
  export * from './createdDatasetFields';
100
105
  export * from './currentUser';
101
106
  export * from './currentUserEmail';
@@ -249,10 +254,13 @@ export * from './jsonSchema';
249
254
  export * from './jsonSchemaAdditionalProperties';
250
255
  export * from './jsonSchemaType';
251
256
  export * from './jsonSchemaTypeAnyOfItem';
257
+ export * from './listBaseModels200';
252
258
  export * from './listDatasetFields200';
253
259
  export * from './listDatasetFieldsParams';
254
260
  export * from './listDatasetItems200';
255
261
  export * from './listDatasetItemsParams';
262
+ export * from './listModelVersions200';
263
+ export * from './listModelVersionsParams';
256
264
  export * from './listPipelineChecks200';
257
265
  export * from './listPipelineRunItems200';
258
266
  export * from './listPipelineRunItemsParams';
@@ -261,6 +269,8 @@ export * from './listPipelineRunsParams';
261
269
  export * from './listProjectDatasets200';
262
270
  export * from './listProjectDatasetsParams';
263
271
  export * from './listProjectFiles200';
272
+ export * from './listProjectModels200';
273
+ export * from './listProjectModelsParams';
264
274
  export * from './listProjectPipelines200';
265
275
  export * from './listProjectPipelinesParams';
266
276
  export * from './listProjectSecrets200';
@@ -277,6 +287,12 @@ export * from './matchNodeSettingsConditionsItemPredicatesItemOperator';
277
287
  export * from './matchNodeSettingsConditionsItemPredicatesItemValue';
278
288
  export * from './matchNodeSettingsFieldProxies';
279
289
  export * from './matchNodeSettingsFieldProxiesInputItem';
290
+ export * from './model';
291
+ export * from './modelArchivedAt';
292
+ export * from './modelDescription';
293
+ export * from './modelId';
294
+ export * from './modelVersion';
295
+ export * from './modelVersionId';
280
296
  export * from './pagination';
281
297
  export * from './patchPipelineNodeBody';
282
298
  export * from './patchPipelineNodeBodySettings';
@@ -0,0 +1,5 @@
1
+ import type { BaseModel } from './baseModel';
2
+
3
+ export type ListBaseModels200 = {
4
+ baseModels: BaseModel[];
5
+ };
@@ -0,0 +1,7 @@
1
+ import type { ModelVersion } from './modelVersion';
2
+ import type { Pagination } from './pagination';
3
+
4
+ export type ListModelVersions200 = {
5
+ versions: ModelVersion[];
6
+ pagination: Pagination;
7
+ };
@@ -0,0 +1,5 @@
1
+
2
+ export type ListModelVersionsParams = {
3
+ limit?: number;
4
+ offset?: number;
5
+ };
@@ -0,0 +1,7 @@
1
+ import type { Model } from './model';
2
+ import type { Pagination } from './pagination';
3
+
4
+ export type ListProjectModels200 = {
5
+ models: Model[];
6
+ pagination: Pagination;
7
+ };
@@ -0,0 +1,5 @@
1
+
2
+ export type ListProjectModelsParams = {
3
+ limit?: number;
4
+ offset?: number;
5
+ };
@@ -0,0 +1,16 @@
1
+ import type { ModelId } from './modelId';
2
+ import type { ProjectId } from './projectId';
3
+ import type { ModelDescription } from './modelDescription';
4
+ import type { BaseModelId } from './baseModelId';
5
+ import type { ModelArchivedAt } from './modelArchivedAt';
6
+
7
+ export interface Model {
8
+ id: ModelId;
9
+ projectId: ProjectId;
10
+ name: string;
11
+ description: ModelDescription;
12
+ baseModelId: BaseModelId;
13
+ archivedAt: ModelArchivedAt;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export type ModelArchivedAt = string | null;
@@ -0,0 +1,2 @@
1
+
2
+ export type ModelDescription = string | null;
@@ -0,0 +1,2 @@
1
+
2
+ export type ModelId = string;
@@ -0,0 +1,9 @@
1
+ import type { ModelVersionId } from './modelVersionId';
2
+ import type { ModelId } from './modelId';
3
+
4
+ export interface ModelVersion {
5
+ id: ModelVersionId;
6
+ modelId: ModelId;
7
+ version: string;
8
+ createdAt: string;
9
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export type ModelVersionId = string;
@@ -6,4 +6,5 @@ export type PipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndP
6
6
  generationModelId?: PipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneoneGenerationModelId;
7
7
  reasoningEffort?: PipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneoneReasoningEffort;
8
8
  llmInstruction?: string;
9
+ maxCostPerAttempt: number;
9
10
  };
@@ -6,4 +6,5 @@ export type ValidatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudience
6
6
  generationModelId?: ValidatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneoneGenerationModelId;
7
7
  reasoningEffort?: ValidatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneoneReasoningEffort;
8
8
  llmInstruction?: string;
9
+ maxCostPerAttempt: number;
9
10
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toloka-platform/sdk",
3
- "version": "0.1.0-preview.5",
3
+ "version": "0.1.0-preview.6",
4
4
  "private": false,
5
5
  "description": "Preview. Build, inspect, and operate Toloka pipeline projects as TypeScript source. The public API may change.",
6
6
  "license": "Apache-2.0",
@@ -5,8 +5,8 @@
5
5
  "name": "Build Toloka pipelines as code",
6
6
  "description": "Build, inspect, and change Toloka platform pipeline projects as TypeScript source with @toloka-platform/sdk and toloka. Use when the user wants to create, adopt, inspect, plan, or apply a pipeline project as code.",
7
7
  "tags": ["pipelines", "authoring", "sdk"],
8
- "version": "0.1.0-preview.5",
9
- "digest": "135d0b6ff6aa7bf26782bc26e6adbe7ee70c9db1565722ee356618211543d0bc",
8
+ "version": "0.1.0-preview.6",
9
+ "digest": "d45d1124d27e19ee43c260ddf0c259ccdc343c5d641c2aeb538d6fedaeb99183",
10
10
  "files": ["SKILL.md", "references/node-semantics.md", "references/workflow-and-cli.md"]
11
11
  },
12
12
  {
@@ -14,8 +14,8 @@
14
14
  "name": "Report a Toloka problem",
15
15
  "description": "Report a problem with the Toloka platform, CLI, or SDK as a redacted Markdown issue report sent through the toloka CLI only after explicit approval. Use when the user hits unexpected behavior, an error, or a bug and wants Toloka engineers to investigate.",
16
16
  "tags": ["support", "issue", "bug", "report"],
17
- "version": "0.1.0-preview.5",
18
- "digest": "9a673337ad1cbbcec7c2fadb2d49d38e923c797d049818fdaa15ff30ffd153f3",
17
+ "version": "0.1.0-preview.6",
18
+ "digest": "b71d06333786826edd51b909dfe8a20633870f235d03df4aae83d20c26e99205",
19
19
  "files": ["SKILL.md", "references/report-template.md"]
20
20
  }
21
21
  ]
@@ -66,7 +66,7 @@ A successful `toloka validate` is necessary, not proof the pipeline can be launc
66
66
 
67
67
  ## Keep data and credentials out of source
68
68
 
69
- - Never put dataset items in pipeline source or `toloka.json`. After apply has created or resolved the dataset, upload JSONL objects with `toloka dataset upload <dataset> <file>`. If a later upload fails as schema-incompatible, run `toloka dataset fields <dataset>` and copy the printed `field()` declarations into source.
69
+ - Never put dataset items in pipeline source or `toloka.json`. After apply has created or resolved the dataset, upload `.json`, `.jsonl`, or `.csv` rows with `toloka dataset upload <dataset> <file>`. If a later upload fails as schema-incompatible, run `toloka dataset fields <dataset>` and copy the printed `field()` declarations into source.
70
70
  - Put secret references in node settings, but never secret values. Set values through stdin with `toloka secret set <name>`, or use `toloka secret set <name> --from-env <variable>`.
71
71
 
72
72
  ## Credentials
@@ -109,6 +109,13 @@ toloka pipeline runs [name] [--limit <n>]
109
109
  toloka pipeline cost [name] [--item-limit <n>]
110
110
  toloka pipeline results <runId> [--limit <n>] [--failed]
111
111
  toloka pipeline cancel <runId>
112
+ toloka base-model list [--json]
113
+ toloka base-model get <id> [--json]
114
+ toloka model list [--json]
115
+ toloka model get <name-or-id> [--json]
116
+ toloka model versions <name-or-id> [--json]
117
+ toloka model create <name> --base <base-model-id> [--description <text>] [--json]
118
+ toloka model archive <name-or-id> [--json]
112
119
  toloka skill list [--json]
113
120
  toloka skill search <query> [--json]
114
121
  toloka skill install <skill-id> [--force]
@@ -121,6 +128,10 @@ Select the platform project with `toloka project add` or `toloka project create`
121
128
 
122
129
  `toloka pipeline cost [name] [--item-limit <n>]` forecasts what a run of a live pipeline would bill. It does not start a run and does not change the graph. Name the live pipeline or id; a TTY may pick among several. The typed client equivalent is `client.pipelines.estimateCost({ pipelineId, itemLimit? })`.
123
130
 
131
+ ## Manage model checkpoints
132
+
133
+ The global Base Model Catalog is read-only and uses `toloka base-model`. `base-model list` prints aligned ID, name, provider, and upstream-model columns by default. `toloka model` manages project-owned lineages; creation requires one catalog `baseModelId` and creates metadata without a Model Version. Every Base Model and Model command accepts `--json` for machine-readable output. Model Version upload, validation, and download commands are deferred until the separate upload-processing contract is implemented.
134
+
124
135
  ## Adopt an existing live project
125
136
 
126
137
  1. Run `toloka init` in the intended source root if the agent entry points are not installed yet.
@@ -186,7 +197,7 @@ Declare the fields in source, then upload rows:
186
197
  toloka dataset upload <dataset> <file>
187
198
  ```
188
199
 
189
- The input file is JSONL: each non-empty line must parse as one JSON object. The dataset does not have to exist yet: when a pipeline in source binds it, upload creates it and declares that pipeline's columns, so rows land in real columns rather than in the raw `uploaded` blob. Upload refuses when no pipeline in source declares a name because it would have no column names to map onto. An id-shaped argument is never created; upload fails when that dataset id does not exist in the project. Upload appends items; apply never uploads, replaces, or deletes rows. Duplicate live names must be addressed by id.
200
+ The CLI infers the input format from its extension and uploads the original bytes without conversion: `.json` is an array of row objects, `.jsonl` has one object per non-empty line, and `.csv` uses its header row as field names with every value ingested as a string. JSONL is validated locally before upload. If transfer or finalization fails, the CLI cancels the started upload before returning the error, so another upload can start immediately and may reuse the same filename. The dataset does not have to exist yet: when a pipeline in source binds it, upload creates it and declares that pipeline's columns, so rows land in real columns rather than in the raw `uploaded` blob. Upload refuses when no pipeline in source declares a name because it would have no column names to map onto. An id-shaped argument is never created; upload fails when that dataset id does not exist in the project. Upload appends items; applying source never deletes them.
190
201
 
191
202
  If a later upload fails with a schema-incompatibility error, inspect the inferred schema from the first successful upload:
192
203
 
@@ -26,7 +26,7 @@ The CLI exits 1 with `Potato API request failed with status 500`. The same comma
26
26
 
27
27
  ## Environment
28
28
 
29
- - toloka CLI 0.1.0-preview.5 (`toloka --version`), Bun 1.4.0, macOS 15
29
+ - toloka CLI 0.1.0-preview.6 (`toloka --version`), Bun 1.4.0, macOS 15
30
30
  - Platform: https://platform.toloka.ai
31
31
 
32
32
  ## Redactions
package/src/cli/data.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { assert } from "es-toolkit";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname } from "node:path";
4
- import type { Dataset, InferredField, PotatoClient } from "../client";
4
+ import type { Dataset, InferredField, PotatoClient, UploadId } from "../client";
5
5
  import { PotatoApiError } from "../errors";
6
6
  import { generateFieldDeclarationsSource } from "../importer";
7
7
  import { readTolokaState } from "../state";
@@ -14,6 +14,14 @@ import type { ProjectSource } from "./source";
14
14
  const RAW_UPLOAD_FIELD_NAME = "uploaded";
15
15
  const DATASET_ID_PATTERN =
16
16
  /^ds\.[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
17
+ const FINALIZE_UPLOAD_ATTEMPTS = 3;
18
+
19
+ class DatasetUploadError extends Error {
20
+ constructor(message: string, cause: unknown) {
21
+ super(message, { cause });
22
+ this.name = "DatasetUploadError";
23
+ }
24
+ }
17
25
 
18
26
  const canonicalDatasetId = (token: string): string | undefined =>
19
27
  DATASET_ID_PATTERN.test(token) ? token.toLowerCase() : undefined;
@@ -22,6 +30,16 @@ type UploadFieldDeclaration = NonNullable<
22
30
  Parameters<PotatoClient["datasets"]["startUpload"]>[0]["fields"]
23
31
  >[number];
24
32
 
33
+ type DatasetUploadFormat = NonNullable<
34
+ Parameters<PotatoClient["datasets"]["startUpload"]>[0]["format"]
35
+ >;
36
+
37
+ const DATASET_UPLOAD_CONTENT_TYPES: Record<DatasetUploadFormat, string> = {
38
+ json: "application/json",
39
+ jsonl: "application/x-ndjson",
40
+ csv: "text/csv",
41
+ };
42
+
25
43
  export type UploadDatasetOptions = {
26
44
  readonly sourceRoot: string;
27
45
  readonly datasetName: string;
@@ -201,29 +219,138 @@ const resolveLiveDataset = async (
201
219
  return { datasetId: created.id, name: created.name, nameIsUnique: true };
202
220
  };
203
221
 
204
- const readJsonLines = async (filePath: string): Promise<ReadonlyArray<Record<string, unknown>>> => {
205
- const source = await readFile(filePath, "utf8");
206
- return source
207
- .split(/\r?\n/u)
208
- .map((line, index) => ({ line, lineNumber: index + 1 }))
209
- .filter(({ line }) => line.trim().length > 0)
210
- .map(({ line, lineNumber }) => {
222
+ const resolveDatasetUploadFormat = (filename: string): DatasetUploadFormat => {
223
+ const normalizedFilename = filename.toLowerCase();
224
+
225
+ if (normalizedFilename.endsWith(".json")) return "json";
226
+ if (normalizedFilename.endsWith(".jsonl")) return "jsonl";
227
+ if (normalizedFilename.endsWith(".csv")) return "csv";
228
+
229
+ assert(false, `${filename} must have a .json, .jsonl, or .csv extension`);
230
+ };
231
+
232
+ const readDatasetUpload = async (filePath: string) => {
233
+ const filename = basename(filePath);
234
+ const format = resolveDatasetUploadFormat(filename);
235
+ const payload = await readFile(filePath, "utf8");
236
+
237
+ if (format === "jsonl") {
238
+ for (const { line, lineNumber } of payload
239
+ .split(/\r?\n/u)
240
+ .map((line, index) => ({ line, lineNumber: index + 1 }))
241
+ .filter(({ line }) => line.length > 0)) {
211
242
  let value: unknown;
212
243
  try {
213
244
  value = JSON.parse(line);
214
245
  } catch {
215
- assert(false, `${basename(filePath)}:${lineNumber} is not valid JSON`);
246
+ assert(false, `${filename}:${lineNumber} is not valid JSON`);
216
247
  }
217
- assert(isJsonObject(value), `${basename(filePath)}:${lineNumber} must contain a JSON object`);
218
- return value;
248
+ assert(isJsonObject(value), `${filename}:${lineNumber} must contain a JSON object`);
249
+ }
250
+ }
251
+
252
+ return { payload, format, contentType: DATASET_UPLOAD_CONTENT_TYPES[format] };
253
+ };
254
+
255
+ type StartedDatasetUpload = { readonly uploadUrl: string; readonly job: { readonly id: UploadId } };
256
+
257
+ const cancelStartedUpload = async (
258
+ client: PotatoClient,
259
+ uploadId: StartedDatasetUpload["job"]["id"],
260
+ error: unknown,
261
+ ): Promise<never> => {
262
+ const reason = error instanceof Error ? error.message : String(error);
263
+
264
+ try {
265
+ await client.datasets.cancelUpload({ uploadId });
266
+ } catch (cancelError) {
267
+ const cancelReason = cancelError instanceof Error ? cancelError.message : String(cancelError);
268
+ throw new AggregateError(
269
+ [error, cancelError],
270
+ `Dataset upload ${uploadId} failed and could not be canceled. Upload error: ${reason}. Cancellation error: ${cancelReason}.`,
271
+ );
272
+ }
273
+
274
+ throw new DatasetUploadError(
275
+ `Dataset upload ${uploadId} failed and was canceled: ${reason}`,
276
+ error,
277
+ );
278
+ };
279
+
280
+ const transferAndFinalizeUpload = async ({
281
+ client,
282
+ started,
283
+ payload,
284
+ contentType,
285
+ fetch,
286
+ retryIntervalMs,
287
+ }: {
288
+ client: PotatoClient;
289
+ started: StartedDatasetUpload;
290
+ payload: string;
291
+ contentType: string;
292
+ fetch: typeof globalThis.fetch;
293
+ retryIntervalMs: number;
294
+ }): Promise<void> => {
295
+ let uploadResponse: Response;
296
+ try {
297
+ uploadResponse = await fetch(started.uploadUrl, {
298
+ method: "PUT",
299
+ headers: {
300
+ "Content-Type": contentType,
301
+ "x-ms-blob-type": "BlockBlob",
302
+ },
303
+ body: payload,
219
304
  });
305
+ } catch (error) {
306
+ return cancelStartedUpload(client, started.job.id, error);
307
+ }
308
+
309
+ if (!uploadResponse.ok) {
310
+ return cancelStartedUpload(
311
+ client,
312
+ started.job.id,
313
+ new DatasetUploadError(
314
+ `Dataset upload failed with HTTP ${uploadResponse.status}.`,
315
+ undefined,
316
+ ),
317
+ );
318
+ }
319
+
320
+ let transientFailures = 0;
321
+ while (true) {
322
+ try {
323
+ await client.datasets.finalizeUpload({ uploadId: started.job.id });
324
+ return;
325
+ } catch (error) {
326
+ if (!(error instanceof PotatoApiError && error.status === 503)) {
327
+ return cancelStartedUpload(client, started.job.id, error);
328
+ }
329
+
330
+ transientFailures += 1;
331
+ if (transientFailures === FINALIZE_UPLOAD_ATTEMPTS) {
332
+ return cancelStartedUpload(
333
+ client,
334
+ started.job.id,
335
+ new DatasetUploadError(
336
+ `Storage remained unavailable after ${FINALIZE_UPLOAD_ATTEMPTS} finalization attempts.`,
337
+ error,
338
+ ),
339
+ );
340
+ }
341
+
342
+ const { promise, resolve } = Promise.withResolvers<void>();
343
+ setTimeout(resolve, retryIntervalMs);
344
+ await promise;
345
+ }
346
+ }
220
347
  };
221
348
 
222
349
  export async function uploadDataset(
223
350
  client: PotatoClient,
224
351
  options: UploadDatasetOptions,
225
352
  ): Promise<UploadDatasetResult> {
226
- const rows = await readJsonLines(options.filePath);
353
+ const { payload, format, contentType } = await readDatasetUpload(options.filePath);
227
354
  const resolved = await resolveLiveDataset(client, options, true);
228
355
  const datasetId = resolved.datasetId;
229
356
 
@@ -236,25 +363,21 @@ export async function uploadDataset(
236
363
  if (!fieldsByName.has(field.name)) fieldsByName.set(field.name, field);
237
364
  }
238
365
  const fields = [...fieldsByName.values()];
239
- const payload = JSON.stringify(rows);
240
- const payloadBytes = new TextEncoder().encode(payload);
241
366
  const started = await client.datasets.startUpload({
242
367
  datasetId,
243
368
  filename: basename(options.filePath),
244
- format: "json",
245
- declaredSizeBytes: payloadBytes.byteLength,
369
+ format,
370
+ declaredSizeBytes: new TextEncoder().encode(payload).byteLength,
246
371
  fields,
247
372
  });
248
- const uploadResponse = await (options.fetch ?? globalThis.fetch)(started.uploadUrl, {
249
- method: "PUT",
250
- headers: {
251
- "Content-Type": "application/json",
252
- "x-ms-blob-type": "BlockBlob",
253
- },
254
- body: payloadBytes,
373
+ await transferAndFinalizeUpload({
374
+ client,
375
+ started,
376
+ payload,
377
+ contentType,
378
+ fetch: options.fetch ?? globalThis.fetch,
379
+ retryIntervalMs: options.waitFor?.intervalMs ?? 1000,
255
380
  });
256
- assert(uploadResponse.ok, `Dataset upload failed with HTTP ${uploadResponse.status}.`);
257
- await client.datasets.finalizeUpload({ uploadId: started.job.id });
258
381
  const completed = await client.waitFor(
259
382
  { type: "datasetUpload", uploadId: started.job.id },
260
383
  options.waitFor,