@toloka-platform/sdk 0.2.0-preview.3 → 0.2.0-preview.4
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/dist/cli/toloka.js +136 -136
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/cli-v2.md +3 -2
- package/docs/cli.md +32 -4
- package/openapi/gen/models/baseModel.ts +1 -0
- package/openapi/gen/models/currentUser.ts +2 -0
- package/openapi/gen/models/currentUserTeamsItem.ts +6 -0
- package/openapi/gen/models/gistingTrainingConfiguration.ts +6 -2
- package/openapi/gen/models/gistingTrainingConfigurationBatchSize.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationInput.ts +6 -2
- package/openapi/gen/models/gistingTrainingConfigurationInputBatchSize.ts +2 -0
- package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +2 -0
- package/openapi/gen/models/index.ts +9 -4
- package/openapi/gen/models/sftTrainingConfiguration.ts +6 -3
- package/openapi/gen/models/sftTrainingConfigurationBatchSize.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationInput.ts +6 -3
- package/openapi/gen/models/sftTrainingConfigurationInputBatchSize.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationInputLoraRank.ts +2 -0
- package/openapi/gen/models/sftTrainingConfigurationLoraRank.ts +2 -0
- package/package.json +1 -1
- package/skills/catalog.json +10 -10
- package/skills/toloka-build/SKILL.md +28 -17
- package/skills/toloka-build/references/node-semantics.md +7 -7
- package/skills/toloka-build/references/workflow-and-cli.md +13 -10
- package/skills/toloka-fine-tuning/SKILL.md +145 -62
- package/skills/toloka-get-started/SKILL.md +35 -20
- package/skills/toloka-gisting/SKILL.md +156 -66
- package/src/cli/analytics.ts +50 -23
- package/src/cli/auth.ts +31 -12
- package/src/cli/auto-update.ts +12 -6
- package/src/cli/check.ts +76 -29
- package/src/cli/cli-entry.ts +3 -2
- package/src/cli/cli-error.ts +43 -0
- package/src/cli/commands/dataset.ts +13 -3
- package/src/cli/commands/models.ts +6 -7
- package/src/cli/commands/pipeline.ts +6 -7
- package/src/cli/commands/project.ts +9 -16
- package/src/cli/commands/report.ts +6 -2
- package/src/cli/commands/secret.ts +5 -4
- package/src/cli/commands/source.ts +4 -4
- package/src/cli/commands/status.ts +2 -3
- package/src/cli/commands/training.ts +50 -17
- package/src/cli/config.ts +2 -2
- package/src/cli/context.ts +55 -17
- package/src/cli/credentials.ts +12 -4
- package/src/cli/data.ts +96 -34
- package/src/cli/delivery-orders.ts +18 -1
- package/src/cli/file-system.ts +6 -2
- package/src/cli/format-error.ts +45 -2
- package/src/cli/init.ts +13 -7
- package/src/cli/models.ts +43 -12
- package/src/cli/program.ts +4 -55
- package/src/cli/project-context.ts +117 -32
- package/src/cli/project.ts +80 -28
- package/src/cli/report.ts +24 -9
- package/src/cli/runs.ts +33 -8
- package/src/cli/settings.ts +13 -5
- package/src/cli/skill.ts +61 -22
- package/src/cli/source.ts +3 -2
- package/src/cli/terminal-io.ts +13 -6
- package/src/cli/trainings.ts +129 -35
- package/src/cli/version.ts +2 -2
- package/src/client.ts +3 -9
- package/src/transport.ts +13 -1
- package/openapi/gen/models/gistingTrainingConfigurationInputRegime.ts +0 -2
- package/openapi/gen/models/gistingTrainingConfigurationRegime.ts +0 -2
- package/openapi/gen/models/sftTrainingConfigurationInputRegime.ts +0 -2
- package/openapi/gen/models/sftTrainingConfigurationRegime.ts +0 -2
package/src/cli/source.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { assert } from "es-toolkit";
|
|
2
1
|
import { readdir, stat } from "node:fs/promises";
|
|
3
2
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
3
|
import { pathToFileURL } from "node:url";
|
|
5
4
|
import { validatePipelineDefinition } from "../authoring";
|
|
6
5
|
import type { PipelineDefinition } from "../authoring";
|
|
6
|
+
import { assertCli } from "./cli-error";
|
|
7
7
|
|
|
8
8
|
export type ProjectPipelineSource = {
|
|
9
9
|
readonly sourceDirectory: string;
|
|
@@ -64,9 +64,10 @@ export async function loadProjectSource(sourceRoot: string): Promise<ProjectSour
|
|
|
64
64
|
const moduleUrl = pathToFileURL(modulePath);
|
|
65
65
|
moduleUrl.searchParams.set("version", `${moduleStat.mtimeMs}-${moduleStat.size}`);
|
|
66
66
|
const module: unknown = await import(moduleUrl.href);
|
|
67
|
-
|
|
67
|
+
assertCli(
|
|
68
68
|
typeof module === "object" && module !== null,
|
|
69
69
|
`Invalid ${modulePath}: expected a module`,
|
|
70
|
+
{ hint: { description: "Make the file a TypeScript module that exports the pipeline definition." } },
|
|
70
71
|
);
|
|
71
72
|
const definition: unknown = Reflect.get(module, "default");
|
|
72
73
|
validatePipelineDefinition(definition);
|
package/src/cli/terminal-io.ts
CHANGED
|
@@ -2,6 +2,15 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import type { AuthPrompt } from "./auth";
|
|
3
3
|
import type { CliIo } from "./context";
|
|
4
4
|
import { pickPipeline } from "./runs";
|
|
5
|
+
import { TolokaCliError } from "./cli-error";
|
|
6
|
+
|
|
7
|
+
const stdinClosedError = () =>
|
|
8
|
+
new TolokaCliError("Standard input closed before an API key was entered.", {
|
|
9
|
+
hint: {
|
|
10
|
+
description:
|
|
11
|
+
"Run toloka login in an interactive terminal, or pipe the key with toloka login --api-key-stdin.",
|
|
12
|
+
},
|
|
13
|
+
});
|
|
5
14
|
|
|
6
15
|
const readStandardInput = async (): Promise<string> => {
|
|
7
16
|
process.stdin.setEncoding("utf8");
|
|
@@ -35,7 +44,7 @@ const readMaskedLine = async (question: string): Promise<string> => {
|
|
|
35
44
|
|
|
36
45
|
const onEnd = () => {
|
|
37
46
|
finish();
|
|
38
|
-
reject(
|
|
47
|
+
reject(stdinClosedError());
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
const onError = (error: Error) => {
|
|
@@ -53,11 +62,9 @@ const readMaskedLine = async (question: string): Promise<string> => {
|
|
|
53
62
|
if (character === "\u0003" || character === "\u0004") {
|
|
54
63
|
finish();
|
|
55
64
|
reject(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
: "Standard input closed before an API key was entered.",
|
|
60
|
-
),
|
|
65
|
+
character === "\u0003"
|
|
66
|
+
? new TolokaCliError("Canceled.", { hint: { disabled: true } })
|
|
67
|
+
: stdinClosedError(),
|
|
61
68
|
);
|
|
62
69
|
return;
|
|
63
70
|
}
|
package/src/cli/trainings.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
|
|
6
|
-
import { assert } from "es-toolkit";
|
|
7
|
-
|
|
8
6
|
import type {
|
|
9
7
|
PotatoClient,
|
|
10
8
|
Training,
|
|
@@ -14,6 +12,12 @@ import type {
|
|
|
14
12
|
TrainingRunMetrics,
|
|
15
13
|
} from "../client";
|
|
16
14
|
import type { BaseModelId, TrainingConfigurationInput } from "../../openapi/gen/models";
|
|
15
|
+
import { assertCli, RETRY_OR_REPORT_HINT, TolokaCliError } from "./cli-error";
|
|
16
|
+
import type { CliErrorHint } from "./cli-error";
|
|
17
|
+
|
|
18
|
+
const ESTIMATE_FILE_HINT: CliErrorHint = {
|
|
19
|
+
description: "Regenerate it with toloka training cost <training> --json > <file>.",
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
type TrainingMethod = TrainingConfigurationInput["method"];
|
|
19
23
|
type TrainingSource = TrainingConfigurationInput["source"];
|
|
@@ -30,9 +34,12 @@ export type TrainingDraftOptions = {
|
|
|
30
34
|
validationDatasetId?: string;
|
|
31
35
|
trainingFilter?: string;
|
|
32
36
|
validationFilter?: string;
|
|
33
|
-
|
|
37
|
+
loraRank?: SftConfigurationInput["loraRank"];
|
|
38
|
+
gistTokens?: GistingConfigurationInput["gistTokens"];
|
|
34
39
|
epochs?: number;
|
|
35
|
-
|
|
40
|
+
learningRate?: number;
|
|
41
|
+
batchSize?: SftConfigurationInput["batchSize"];
|
|
42
|
+
maxSequenceLength?: number;
|
|
36
43
|
promptFilePath?: string;
|
|
37
44
|
fetch?: typeof globalThis.fetch;
|
|
38
45
|
};
|
|
@@ -72,6 +79,13 @@ const formatTraining = (training: Training): string =>
|
|
|
72
79
|
"Method",
|
|
73
80
|
training.configuration.method === "sft" ? "Supervised fine-tuning (SFT)" : "Prompt gisting",
|
|
74
81
|
],
|
|
82
|
+
training.configuration.method === "sft"
|
|
83
|
+
? ["LoRA rank", String(training.configuration.loraRank)]
|
|
84
|
+
: ["Gist tokens", String(training.configuration.gistTokens)],
|
|
85
|
+
["Epochs", String(training.configuration.epochs)],
|
|
86
|
+
["Learning rate", String(training.configuration.learningRate)],
|
|
87
|
+
["Batch size", String(training.configuration.batchSize)],
|
|
88
|
+
["Max sequence length", String(training.configuration.maxSequenceLength)],
|
|
75
89
|
["Output Model", training.modelId ?? "-"],
|
|
76
90
|
["Archived at", training.archivedAt ?? "-"],
|
|
77
91
|
["Created at", training.createdAt],
|
|
@@ -115,27 +129,32 @@ type ReadyEstimateFile = {
|
|
|
115
129
|
};
|
|
116
130
|
|
|
117
131
|
const parseReadyEstimateFile = (value: unknown): ReadyEstimateFile => {
|
|
118
|
-
|
|
132
|
+
assertCli(
|
|
119
133
|
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
120
134
|
"The estimate file must contain a JSON object.",
|
|
135
|
+
{ hint: ESTIMATE_FILE_HINT },
|
|
121
136
|
);
|
|
122
|
-
|
|
137
|
+
assertCli(
|
|
123
138
|
"status" in value && value.status === "ready",
|
|
124
139
|
"The estimate file is not a ready Training Cost Estimate.",
|
|
140
|
+
{ hint: ESTIMATE_FILE_HINT },
|
|
125
141
|
);
|
|
126
|
-
|
|
142
|
+
assertCli(
|
|
127
143
|
"trainingId" in value && typeof value.trainingId === "string",
|
|
128
144
|
"The estimate file has no Training ID.",
|
|
145
|
+
{ hint: ESTIMATE_FILE_HINT },
|
|
129
146
|
);
|
|
130
|
-
|
|
147
|
+
assertCli(
|
|
131
148
|
"inputSetId" in value && typeof value.inputSetId === "string",
|
|
132
149
|
"The estimate file has no Input Set ID.",
|
|
150
|
+
{ hint: ESTIMATE_FILE_HINT },
|
|
133
151
|
);
|
|
134
|
-
|
|
152
|
+
assertCli(
|
|
135
153
|
"fingerprint" in value &&
|
|
136
154
|
typeof value.fingerprint === "string" &&
|
|
137
155
|
/^[0-9a-f]{64}$/.test(value.fingerprint),
|
|
138
156
|
"The estimate file has no valid fingerprint.",
|
|
157
|
+
{ hint: ESTIMATE_FILE_HINT },
|
|
139
158
|
);
|
|
140
159
|
|
|
141
160
|
return {
|
|
@@ -171,9 +190,10 @@ const resolveTraining = async (
|
|
|
171
190
|
): Promise<Training> => {
|
|
172
191
|
if (nameOrId.startsWith("trg.")) {
|
|
173
192
|
const training = await client.trainings.get({ trainingId: nameOrId });
|
|
174
|
-
|
|
193
|
+
assertCli(
|
|
175
194
|
training.projectId === projectId,
|
|
176
195
|
`Training ${nameOrId} was not found in project ${projectId}.`,
|
|
196
|
+
{ hint: { description: "Run toloka training list; a Training belongs to one project." } },
|
|
177
197
|
);
|
|
178
198
|
return training;
|
|
179
199
|
}
|
|
@@ -182,7 +202,9 @@ const resolveTraining = async (
|
|
|
182
202
|
for await (const training of client.trainings.listAll({ projectId, limit: 100 })) {
|
|
183
203
|
if (training.name === nameOrId) matches.push(training);
|
|
184
204
|
}
|
|
185
|
-
|
|
205
|
+
assertCli(matches.length === 1, `Training ${nameOrId} was not found or is ambiguous.`, {
|
|
206
|
+
hint: { description: "Run toloka training list and pass the training id (trg.…)." },
|
|
207
|
+
});
|
|
186
208
|
return matches[0]!;
|
|
187
209
|
};
|
|
188
210
|
|
|
@@ -199,12 +221,18 @@ const resolveSelection = async (
|
|
|
199
221
|
datasetId: string,
|
|
200
222
|
filter: string | undefined,
|
|
201
223
|
): Promise<TrainingSelection> => {
|
|
202
|
-
|
|
224
|
+
assertCli(datasetId, "A Dataset is required when specifying a filter.", {
|
|
225
|
+
hint: {
|
|
226
|
+
description:
|
|
227
|
+
"Pass --dataset together with --train-filter, and --validation-dataset together with --validation-filter.",
|
|
228
|
+
},
|
|
229
|
+
});
|
|
203
230
|
if (filter === undefined) return { datasetId };
|
|
204
231
|
const separator = filter.indexOf("=");
|
|
205
|
-
|
|
232
|
+
assertCli(
|
|
206
233
|
separator > 0 && separator < filter.length - 1,
|
|
207
234
|
"Dataset filters must use FIELD=VALUE syntax.",
|
|
235
|
+
{ hint: { description: "Write the filter as field=value, for example split=train." } },
|
|
208
236
|
);
|
|
209
237
|
const fieldName = filter.slice(0, separator);
|
|
210
238
|
const rawValue = filter.slice(separator + 1);
|
|
@@ -213,9 +241,10 @@ const resolveSelection = async (
|
|
|
213
241
|
if (field.name === fieldName) matches.push(field);
|
|
214
242
|
}
|
|
215
243
|
const [field] = matches;
|
|
216
|
-
|
|
244
|
+
assertCli(
|
|
217
245
|
field && matches.length === 1,
|
|
218
246
|
`Dataset field ${fieldName} was not found or is ambiguous.`,
|
|
247
|
+
{ hint: { description: "Run toloka dataset fields <dataset> to see the field names." } },
|
|
219
248
|
);
|
|
220
249
|
return {
|
|
221
250
|
datasetId,
|
|
@@ -240,14 +269,17 @@ const sourceFromOptions = (
|
|
|
240
269
|
options: Pick<TrainingDraftOptions, "baseModelId" | "modelVersionId">,
|
|
241
270
|
fallback?: TrainingSource,
|
|
242
271
|
): TrainingSource => {
|
|
243
|
-
|
|
272
|
+
assertCli(
|
|
244
273
|
!(options.baseModelId && options.modelVersionId),
|
|
245
274
|
"Choose exactly one of --base-model and --model-version.",
|
|
275
|
+
{ hint: { disabled: true } },
|
|
246
276
|
);
|
|
247
277
|
if (options.baseModelId) return { type: "base_model", baseModelId: options.baseModelId };
|
|
248
278
|
if (options.modelVersionId)
|
|
249
279
|
return { type: "model_version", modelVersionId: options.modelVersionId };
|
|
250
|
-
|
|
280
|
+
assertCli(fallback, "Choose exactly one of --base-model and --model-version.", {
|
|
281
|
+
hint: { disabled: true },
|
|
282
|
+
});
|
|
251
283
|
return fallback;
|
|
252
284
|
};
|
|
253
285
|
|
|
@@ -258,20 +290,29 @@ const uploadPromptFile = async (
|
|
|
258
290
|
fetchImplementation: typeof globalThis.fetch,
|
|
259
291
|
): Promise<string> => {
|
|
260
292
|
const metadata = await stat(filePath);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
assertCli(metadata.isFile(), "--prompt-file must point to a file.", {
|
|
294
|
+
hint: { description: "Pass the absolute path of the prompt text file." },
|
|
295
|
+
});
|
|
296
|
+
assertCli(metadata.size > 0, "The gisting prompt file is empty.", {
|
|
297
|
+
hint: { description: "Put the full prompt text into the file before creating the Training." },
|
|
298
|
+
});
|
|
299
|
+
assertCli(
|
|
264
300
|
metadata.size <= GISTING_PROMPT_MAX_BYTES,
|
|
265
301
|
"The gisting prompt file exceeds the 256 KiB limit.",
|
|
302
|
+
{ hint: { description: "Shorten the prompt to under 256 KiB." } },
|
|
266
303
|
);
|
|
267
304
|
const payload = await readFile(filePath);
|
|
268
305
|
let prompt: string;
|
|
269
306
|
try {
|
|
270
307
|
prompt = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
271
308
|
} catch {
|
|
272
|
-
throw new
|
|
309
|
+
throw new TolokaCliError("The gisting prompt file must contain valid UTF-8 text.", {
|
|
310
|
+
hint: { description: "Save the prompt file as UTF-8 text." },
|
|
311
|
+
});
|
|
273
312
|
}
|
|
274
|
-
|
|
313
|
+
assertCli(prompt.trim().length > 0, "The gisting prompt file is empty.", {
|
|
314
|
+
hint: { description: "Put the full prompt text into the file before creating the Training." },
|
|
315
|
+
});
|
|
275
316
|
const started = await client.files.startUpload({
|
|
276
317
|
projectId,
|
|
277
318
|
filename: basename(filePath),
|
|
@@ -283,7 +324,9 @@ const uploadPromptFile = async (
|
|
|
283
324
|
headers: { "x-ms-blob-type": "BlockBlob", "Content-Type": "text/plain; charset=utf-8" },
|
|
284
325
|
body: payload,
|
|
285
326
|
});
|
|
286
|
-
|
|
327
|
+
assertCli(response.ok, `Failed to upload gisting prompt: HTTP ${response.status}.`, {
|
|
328
|
+
hint: RETRY_OR_REPORT_HINT,
|
|
329
|
+
});
|
|
287
330
|
return (await client.files.finalizeUpload({ projectId, fileId: started.file.id })).id;
|
|
288
331
|
} catch (error) {
|
|
289
332
|
await client.files.delete({ projectId, fileId: started.file.id }).catch(() => undefined);
|
|
@@ -301,7 +344,9 @@ const resolveSelections = async ({
|
|
|
301
344
|
options: TrainingDraftOptions;
|
|
302
345
|
}): Promise<ResolvedSelections> => {
|
|
303
346
|
const trainingDatasetId = options.datasetId ?? current?.training.datasetId;
|
|
304
|
-
|
|
347
|
+
assertCli(trainingDatasetId, "--dataset is required.", {
|
|
348
|
+
hint: { description: "Pass --dataset <dataset-id>; toloka dataset list shows the ids." },
|
|
349
|
+
});
|
|
305
350
|
const training =
|
|
306
351
|
options.datasetId === undefined && options.trainingFilter === undefined && current
|
|
307
352
|
? current.training
|
|
@@ -317,6 +362,14 @@ const resolveSelections = async ({
|
|
|
317
362
|
return { training, validation };
|
|
318
363
|
};
|
|
319
364
|
|
|
365
|
+
const sharedHyperparameters = (options: TrainingDraftOptions) => ({
|
|
366
|
+
...(options.learningRate === undefined ? {} : { learningRate: options.learningRate }),
|
|
367
|
+
...(options.batchSize === undefined ? {} : { batchSize: options.batchSize }),
|
|
368
|
+
...(options.maxSequenceLength === undefined
|
|
369
|
+
? {}
|
|
370
|
+
: { maxSequenceLength: options.maxSequenceLength }),
|
|
371
|
+
});
|
|
372
|
+
|
|
320
373
|
const buildSftConfiguration = ({
|
|
321
374
|
source,
|
|
322
375
|
selections,
|
|
@@ -328,15 +381,28 @@ const buildSftConfiguration = ({
|
|
|
328
381
|
current: SftConfigurationInput | undefined;
|
|
329
382
|
options: TrainingDraftOptions;
|
|
330
383
|
}): SftConfigurationInput => {
|
|
331
|
-
|
|
384
|
+
assertCli(options.promptFilePath === undefined, "--prompt-file applies to gisting only.", {
|
|
385
|
+
hint: {
|
|
386
|
+
description: "Pass --method gisting to use --prompt-file, or omit --prompt-file for SFT.",
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
assertCli(
|
|
390
|
+
options.gistTokens === undefined,
|
|
391
|
+
"--gist-tokens applies to gisting; SFT takes --lora-rank.",
|
|
392
|
+
{
|
|
393
|
+
hint: {
|
|
394
|
+
description: "Use --lora-rank for SFT, or pass --method gisting to use --gist-tokens.",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
);
|
|
332
398
|
return {
|
|
333
399
|
...current,
|
|
334
400
|
method: "sft",
|
|
335
401
|
source,
|
|
336
402
|
...selections,
|
|
337
|
-
...(options.
|
|
403
|
+
...(options.loraRank === undefined ? {} : { loraRank: options.loraRank }),
|
|
338
404
|
...(options.epochs === undefined ? {} : { epochs: options.epochs }),
|
|
339
|
-
...(options
|
|
405
|
+
...sharedHyperparameters(options),
|
|
340
406
|
};
|
|
341
407
|
};
|
|
342
408
|
|
|
@@ -355,7 +421,15 @@ const buildGistingConfiguration = async ({
|
|
|
355
421
|
current: GistingConfigurationInput | undefined;
|
|
356
422
|
options: TrainingDraftOptions;
|
|
357
423
|
}): Promise<GistingConfigurationInput> => {
|
|
358
|
-
|
|
424
|
+
assertCli(
|
|
425
|
+
options.loraRank === undefined,
|
|
426
|
+
"--lora-rank applies to SFT; gisting takes --gist-tokens.",
|
|
427
|
+
{
|
|
428
|
+
hint: {
|
|
429
|
+
description: "Use --gist-tokens for gisting, or omit --method gisting to create SFT.",
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
);
|
|
359
433
|
const promptFileId = options.promptFilePath
|
|
360
434
|
? await uploadPromptFile(
|
|
361
435
|
client,
|
|
@@ -364,15 +438,18 @@ const buildGistingConfiguration = async ({
|
|
|
364
438
|
options.fetch ?? globalThis.fetch,
|
|
365
439
|
)
|
|
366
440
|
: current?.promptFileId;
|
|
367
|
-
|
|
441
|
+
assertCli(promptFileId, "--prompt-file is required for gisting.", {
|
|
442
|
+
hint: { description: "Pass --prompt-file <absolute-path> with the prompt text." },
|
|
443
|
+
});
|
|
368
444
|
return {
|
|
369
445
|
...current,
|
|
370
446
|
method: "gisting",
|
|
371
447
|
source,
|
|
372
448
|
...selections,
|
|
373
449
|
promptFileId,
|
|
374
|
-
...(options.
|
|
450
|
+
...(options.gistTokens === undefined ? {} : { gistTokens: options.gistTokens }),
|
|
375
451
|
...(options.epochs === undefined ? {} : { epochs: options.epochs }),
|
|
452
|
+
...sharedHyperparameters(options),
|
|
376
453
|
};
|
|
377
454
|
};
|
|
378
455
|
|
|
@@ -453,10 +530,13 @@ export const createTraining = async (
|
|
|
453
530
|
},
|
|
454
531
|
io: TrainingIo,
|
|
455
532
|
): Promise<void> => {
|
|
456
|
-
|
|
457
|
-
|
|
533
|
+
assertCli(input.datasetId, "--dataset is required.", {
|
|
534
|
+
hint: { description: "Pass --dataset <dataset-id>; toloka dataset list shows the ids." },
|
|
535
|
+
});
|
|
536
|
+
assertCli(
|
|
458
537
|
!input.validationFilter || input.validationDatasetId,
|
|
459
538
|
"--validation-filter requires --validation-dataset.",
|
|
539
|
+
{ hint: { disabled: true } },
|
|
460
540
|
);
|
|
461
541
|
|
|
462
542
|
const configuration = await buildTrainingConfiguration({
|
|
@@ -526,6 +606,9 @@ export const estimateTrainingCost = async (
|
|
|
526
606
|
io: TrainingIo,
|
|
527
607
|
): Promise<void> => {
|
|
528
608
|
const { estimate } = await prepareTrainingCostEstimate(client, input);
|
|
609
|
+
assertCli(estimate.status === "ready", formatCostEstimate(estimate), {
|
|
610
|
+
hint: { description: "Fix the blocked issues above, then run toloka training cost again." },
|
|
611
|
+
});
|
|
529
612
|
writeOutput(io, input.json, estimate, formatCostEstimate(estimate));
|
|
530
613
|
};
|
|
531
614
|
|
|
@@ -549,10 +632,19 @@ export const createTrainingRun = async (
|
|
|
549
632
|
projectId: input.projectId,
|
|
550
633
|
nameOrId: input.nameOrId,
|
|
551
634
|
});
|
|
552
|
-
|
|
635
|
+
assertCli(prepared.estimate.status === "ready", formatCostEstimate(prepared.estimate), {
|
|
636
|
+
hint: {
|
|
637
|
+
description: "Fix the blocked issues above, then run toloka training cost again.",
|
|
638
|
+
},
|
|
639
|
+
});
|
|
553
640
|
return parseReadyEstimateFile(prepared.estimate);
|
|
554
641
|
})();
|
|
555
|
-
|
|
642
|
+
assertCli(estimate.trainingId === training.id, "The estimate file belongs to another Training.", {
|
|
643
|
+
hint: {
|
|
644
|
+
description:
|
|
645
|
+
"Run toloka training cost <training> --json > <file> for this Training, get its maximumCharge approved, then re-run with that file.",
|
|
646
|
+
},
|
|
647
|
+
});
|
|
556
648
|
const run = await client.trainings.createRun({
|
|
557
649
|
trainingId: training.id,
|
|
558
650
|
inputSetId: estimate.inputSetId,
|
|
@@ -657,8 +749,10 @@ const formatMetricValue = (point: {
|
|
|
657
749
|
};
|
|
658
750
|
|
|
659
751
|
const METRICS_UNAVAILABLE: Record<"not_configured" | "unresolved", string> = {
|
|
660
|
-
not_configured:
|
|
661
|
-
|
|
752
|
+
not_configured:
|
|
753
|
+
"This Training Run has no metrics: it started before metrics were available, or the server records none.",
|
|
754
|
+
unresolved:
|
|
755
|
+
"Metrics are temporarily unreachable. The Training Run itself is unaffected; try again later.",
|
|
662
756
|
};
|
|
663
757
|
|
|
664
758
|
const formatTrainingRunMetrics = (metrics: TrainingRunMetrics): string => {
|
package/src/cli/version.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { realpathSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { assert } from "es-toolkit";
|
|
6
5
|
import { SDK_VERSION } from "./sdk-version";
|
|
6
|
+
import { assertCli, REPORT_HINT } from "./cli-error";
|
|
7
7
|
|
|
8
8
|
export type CliInstallChannel = "global-registry" | "linked" | "project-local";
|
|
9
9
|
|
|
@@ -36,7 +36,7 @@ const classifyInstallChannel = (
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const cliVersionInfo = (entryPath = process.argv[1], options: VersionInfoOptions = {}) => {
|
|
39
|
-
|
|
39
|
+
assertCli(entryPath, "CLI entry path is unavailable", { hint: REPORT_HINT });
|
|
40
40
|
const globalBinDirectory =
|
|
41
41
|
options.globalBinDirectory ??
|
|
42
42
|
resolve(process.env.BUN_INSTALL ?? join(homedir(), ".bun"), "bin");
|
package/src/client.ts
CHANGED
|
@@ -228,12 +228,6 @@ export type WaitForTarget =
|
|
|
228
228
|
const createClientImplementation = (config: ClientConfig) => {
|
|
229
229
|
const requestOptions = createRequestOptions(config.baseUrl, config.apiKey, config.fetch);
|
|
230
230
|
|
|
231
|
-
const emptyJsonRequestOptions = (): RequestInit => {
|
|
232
|
-
const headers = new Headers(requestOptions.headers);
|
|
233
|
-
headers.set("Content-Type", "application/json");
|
|
234
|
-
return { ...requestOptions, headers, body: "{}" };
|
|
235
|
-
};
|
|
236
|
-
|
|
237
231
|
function waitFor(
|
|
238
232
|
target: DatasetUploadWaitTarget,
|
|
239
233
|
options?: WaitForOptions,
|
|
@@ -564,9 +558,9 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
564
558
|
startUpload: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetUploadBody) =>
|
|
565
559
|
wire.createDatasetUpload(datasetId, body, requestOptions),
|
|
566
560
|
finalizeUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
567
|
-
wire.finalizeDatasetUpload(uploadId,
|
|
561
|
+
wire.finalizeDatasetUpload(uploadId, requestOptions),
|
|
568
562
|
cancelUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
569
|
-
wire.cancelDatasetUpload(uploadId,
|
|
563
|
+
wire.cancelDatasetUpload(uploadId, requestOptions),
|
|
570
564
|
getUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
571
565
|
wire.getDatasetUpload(uploadId, requestOptions),
|
|
572
566
|
startExport: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetExportBody) =>
|
|
@@ -577,7 +571,7 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
577
571
|
|
|
578
572
|
const runs = {
|
|
579
573
|
start: ({ pipelineId }: { pipelineId: PipelineId }) =>
|
|
580
|
-
wire.createPipelineRun(pipelineId,
|
|
574
|
+
wire.createPipelineRun(pipelineId, requestOptions),
|
|
581
575
|
get: ({ runId }: { runId: RunId }) => wire.getPipelineRun(runId, requestOptions),
|
|
582
576
|
cancel: ({ runId }: { runId: RunId }) => wire.cancelPipelineRun(runId, requestOptions),
|
|
583
577
|
listPage: ({ pipelineId, ...params }: { pipelineId: PipelineId } & ListPipelineRunsParams) =>
|
package/src/transport.ts
CHANGED
|
@@ -25,6 +25,15 @@ export const createRequestOptions = (
|
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const prepareRequestInit = (requestOptions: RequestInit, headers: Headers): RequestInit => {
|
|
29
|
+
const method = (requestOptions.method ?? "GET").toUpperCase();
|
|
30
|
+
if (method === "GET" || method === "HEAD" || requestOptions.body !== undefined) {
|
|
31
|
+
return { ...requestOptions, headers };
|
|
32
|
+
}
|
|
33
|
+
headers.set("Content-Type", "application/json");
|
|
34
|
+
return { ...requestOptions, headers, body: "{}" };
|
|
35
|
+
};
|
|
36
|
+
|
|
28
37
|
export function potatoFetch<T>(url: string, options: TransportRequestInit): Promise<T>;
|
|
29
38
|
export async function potatoFetch(url: string, options: TransportRequestInit): Promise<unknown> {
|
|
30
39
|
const { fetch: fetchImplementation = globalThis.fetch, ...requestOptions } = options;
|
|
@@ -33,7 +42,10 @@ export async function potatoFetch(url: string, options: TransportRequestInit): P
|
|
|
33
42
|
assert(baseUrl, "The SDK base URL is missing");
|
|
34
43
|
headers.delete(BASE_URL_HEADER);
|
|
35
44
|
|
|
36
|
-
const response = await fetchImplementation(
|
|
45
|
+
const response = await fetchImplementation(
|
|
46
|
+
new URL(url, baseUrl),
|
|
47
|
+
prepareRequestInit(requestOptions, headers),
|
|
48
|
+
);
|
|
37
49
|
const text = await response.text();
|
|
38
50
|
|
|
39
51
|
if (!response.ok) {
|