@toloka-platform/sdk 0.2.0-preview.3 → 0.2.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 (75) hide show
  1. package/README.md +5 -6
  2. package/dist/cli/toloka.js +136 -136
  3. package/dist/cli/toloka.js.jsc +0 -0
  4. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  5. package/dist/cli/toloka.js.sha256 +1 -1
  6. package/docs/cli-v2.md +3 -2
  7. package/docs/cli.md +35 -7
  8. package/docs/getting-started.md +4 -2
  9. package/docs/limitations.md +2 -2
  10. package/openapi/gen/models/baseModel.ts +1 -0
  11. package/openapi/gen/models/currentUser.ts +2 -0
  12. package/openapi/gen/models/currentUserTeamsItem.ts +6 -0
  13. package/openapi/gen/models/gistingTrainingConfiguration.ts +6 -2
  14. package/openapi/gen/models/gistingTrainingConfigurationBatchSize.ts +2 -0
  15. package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +2 -0
  16. package/openapi/gen/models/gistingTrainingConfigurationInput.ts +6 -2
  17. package/openapi/gen/models/gistingTrainingConfigurationInputBatchSize.ts +2 -0
  18. package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +2 -0
  19. package/openapi/gen/models/index.ts +9 -4
  20. package/openapi/gen/models/sftTrainingConfiguration.ts +6 -3
  21. package/openapi/gen/models/sftTrainingConfigurationBatchSize.ts +2 -0
  22. package/openapi/gen/models/sftTrainingConfigurationInput.ts +6 -3
  23. package/openapi/gen/models/sftTrainingConfigurationInputBatchSize.ts +2 -0
  24. package/openapi/gen/models/sftTrainingConfigurationInputLoraRank.ts +2 -0
  25. package/openapi/gen/models/sftTrainingConfigurationLoraRank.ts +2 -0
  26. package/package.json +2 -2
  27. package/skills/catalog.json +11 -11
  28. package/skills/toloka-build/SKILL.md +29 -18
  29. package/skills/toloka-build/references/node-semantics.md +7 -7
  30. package/skills/toloka-build/references/workflow-and-cli.md +15 -12
  31. package/skills/toloka-fine-tuning/SKILL.md +145 -62
  32. package/skills/toloka-get-started/SKILL.md +37 -21
  33. package/skills/toloka-gisting/SKILL.md +156 -66
  34. package/skills/toloka-report-issue/references/report-template.md +1 -1
  35. package/src/cli/analytics.ts +50 -23
  36. package/src/cli/auth.ts +31 -12
  37. package/src/cli/auto-update.ts +12 -6
  38. package/src/cli/check.ts +76 -29
  39. package/src/cli/cli-entry.ts +3 -2
  40. package/src/cli/cli-error.ts +43 -0
  41. package/src/cli/commands/dataset.ts +13 -3
  42. package/src/cli/commands/models.ts +6 -7
  43. package/src/cli/commands/pipeline.ts +6 -7
  44. package/src/cli/commands/project.ts +9 -16
  45. package/src/cli/commands/report.ts +6 -2
  46. package/src/cli/commands/secret.ts +5 -4
  47. package/src/cli/commands/source.ts +4 -4
  48. package/src/cli/commands/status.ts +2 -3
  49. package/src/cli/commands/training.ts +50 -17
  50. package/src/cli/config.ts +2 -2
  51. package/src/cli/context.ts +55 -17
  52. package/src/cli/credentials.ts +12 -4
  53. package/src/cli/data.ts +96 -34
  54. package/src/cli/delivery-orders.ts +18 -1
  55. package/src/cli/file-system.ts +6 -2
  56. package/src/cli/format-error.ts +45 -2
  57. package/src/cli/init.ts +13 -7
  58. package/src/cli/models.ts +43 -12
  59. package/src/cli/program.ts +5 -56
  60. package/src/cli/project-context.ts +117 -32
  61. package/src/cli/project.ts +80 -28
  62. package/src/cli/report.ts +24 -9
  63. package/src/cli/runs.ts +33 -8
  64. package/src/cli/settings.ts +13 -5
  65. package/src/cli/skill.ts +61 -22
  66. package/src/cli/source.ts +3 -2
  67. package/src/cli/terminal-io.ts +13 -6
  68. package/src/cli/trainings.ts +129 -35
  69. package/src/cli/version.ts +2 -2
  70. package/src/client.ts +3 -9
  71. package/src/transport.ts +13 -1
  72. package/openapi/gen/models/gistingTrainingConfigurationInputRegime.ts +0 -2
  73. package/openapi/gen/models/gistingTrainingConfigurationRegime.ts +0 -2
  74. package/openapi/gen/models/sftTrainingConfigurationInputRegime.ts +0 -2
  75. package/openapi/gen/models/sftTrainingConfigurationRegime.ts +0 -2
@@ -1,4 +1,5 @@
1
1
  import { PotatoApiError } from "../errors";
2
+ import { formatHint, TolokaCliError } from "./cli-error";
2
3
 
3
4
  export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
4
5
  const details =
@@ -10,10 +11,52 @@ export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
10
11
  return details === undefined || details.length === 0 ? undefined : details;
11
12
  };
12
13
 
14
+ const apiErrorHint = (error: PotatoApiError): string | undefined => {
15
+ if (error.code === "wait_timeout") {
16
+ return "Hint: The job is still running. Check it later with its status command; commands that accept --timeout can wait longer.";
17
+ }
18
+ if (error.status === 401 || error.status === 403) {
19
+ return "Hint: Run toloka whoami to see which key and platform are in use; toloka login stores a new key.";
20
+ }
21
+ if (error.status === 404) {
22
+ return "Hint: Check the id against toloka project current and the matching list command; ids belong to one project on one platform.";
23
+ }
24
+ if (error.status === 429) {
25
+ return "Hint: The platform is rate limiting this key; wait a moment and retry.";
26
+ }
27
+ if (error.status !== undefined && error.status >= 500) {
28
+ return "Hint: The platform failed to handle the request. Retry; if it keeps failing, write a report as the toloka-report-issue skill describes and send it with toloka report <report.md>.";
29
+ }
30
+ return undefined;
31
+ };
32
+
33
+ const joinLines = (lines: ReadonlyArray<string | undefined>): string =>
34
+ lines.filter((line) => line !== undefined && line.length > 0).join("\n");
35
+
36
+ export const findPotatoApiError = (error: unknown): PotatoApiError | undefined => {
37
+ if (error instanceof PotatoApiError) return error;
38
+ if (error instanceof AggregateError) {
39
+ for (const nested of error.errors) {
40
+ const apiError = findPotatoApiError(nested);
41
+ if (apiError !== undefined) return apiError;
42
+ }
43
+ }
44
+ return error instanceof Error ? findPotatoApiError(error.cause) : undefined;
45
+ };
46
+
13
47
  export const formatCliError = (error: unknown): string => {
48
+ if (error instanceof TolokaCliError) {
49
+ const cause = findPotatoApiError(error.cause);
50
+ const causeLines =
51
+ cause === undefined
52
+ ? []
53
+ : cause.message === error.message
54
+ ? [apiErrorDetails(cause)]
55
+ : [cause.message, apiErrorDetails(cause)];
56
+ return joinLines([error.message, ...causeLines, formatHint(error.hint)]);
57
+ }
14
58
  if (error instanceof PotatoApiError) {
15
- const details = apiErrorDetails(error);
16
- return details === undefined ? error.message : `${error.message}\n${details}`;
59
+ return joinLines([error.message, apiErrorDetails(error), apiErrorHint(error)]);
17
60
  }
18
61
  return error instanceof Error ? error.message : String(error);
19
62
  };
package/src/cli/init.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { assert } from "es-toolkit";
2
1
  import { lstat, mkdir, readdir, readFile, realpath, rm, rmdir, writeFile } from "node:fs/promises";
3
2
  import { dirname, join } from "node:path";
4
3
  import { readTolokaState, writeTolokaState } from "../state";
5
4
  import { assertSafeDestination, entryStats, isMissingFileError } from "./file-system";
6
5
  import { SDK_PACKAGE_NAME, SDK_VERSION } from "./sdk-version";
7
6
  import { SKILL_ENTRY_POINT, SKILL_INSTALL_ROOT } from "./skill";
7
+ import { assertCli } from "./cli-error";
8
8
 
9
9
  const BOOTSTRAP_NAME = "toloka-bootstrap";
10
10
  const CLAUDE_POINTER_PATH = `.claude/skills/${BOOTSTRAP_NAME}/SKILL.md`;
@@ -106,14 +106,16 @@ const locateBlock = (source: string, markers: BlockMarkers, path: string) => {
106
106
  const beginIndex = source.indexOf(markers.begin);
107
107
  const endIndex = source.indexOf(markers.end);
108
108
  if (beginIndex === -1 && endIndex === -1) return undefined;
109
- assert(
109
+ assertCli(
110
110
  beginIndex !== -1 && endIndex !== -1 && beginIndex < endIndex,
111
111
  `Malformed Toloka block in ${path}: expected one ${markers.begin} followed by one ${markers.end}. Repair the file by hand, then re-run \`toloka init\`.`,
112
+ { hint: { disabled: true } },
112
113
  );
113
- assert(
114
+ assertCli(
114
115
  !source.includes(markers.begin, beginIndex + markers.begin.length) &&
115
116
  !source.includes(markers.end, endIndex + markers.end.length),
116
117
  `Multiple Toloka blocks in ${path}: keep exactly one ${markers.begin} ... ${markers.end} block, then re-run \`toloka init\`.`,
118
+ { hint: { disabled: true } },
117
119
  );
118
120
  return { beginIndex, endIndex: endIndex + markers.end.length };
119
121
  };
@@ -121,9 +123,10 @@ const locateBlock = (source: string, markers: BlockMarkers, path: string) => {
121
123
  const mergeBootstrap = (source: string, path: string) => {
122
124
  const current = locateBlock(source, CURRENT_BLOCK, path);
123
125
  const legacy = locateBlock(source, LEGACY_BLOCK, path);
124
- assert(
126
+ assertCli(
125
127
  current === undefined || legacy === undefined,
126
128
  `Ambiguous Toloka blocks in ${path}: both ${CURRENT_BLOCK.begin} and ${LEGACY_BLOCK.begin} are present. Keep one, then re-run \`toloka init\`.`,
129
+ { hint: { disabled: true } },
127
130
  );
128
131
  const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
129
132
  const block = BOOTSTRAP_BLOCK.replaceAll("\n", lineEnding);
@@ -146,9 +149,10 @@ const hasSdkDependency = async (targetDirectory: string) => {
146
149
  const source = await readOptionalFile(join(targetDirectory, "package.json"));
147
150
  if (source === undefined) return false;
148
151
  const manifest: unknown = JSON.parse(source);
149
- assert(
152
+ assertCli(
150
153
  typeof manifest === "object" && manifest !== null,
151
154
  "package.json must contain a JSON object.",
155
+ { hint: { description: "Fix package.json in the source root, then re-run toloka init." } },
152
156
  );
153
157
  return ["dependencies", "devDependencies"].some((key) => {
154
158
  const dependencies = Reflect.get(manifest, key);
@@ -170,9 +174,10 @@ const writeIfChanged = async (path: string, source: string) => {
170
174
  const assertRefreshablePointer = async (targetRoot: string, relativePath: string) => {
171
175
  const path = join(targetRoot, relativePath);
172
176
  const existing = await readOptionalFile(path);
173
- assert(
177
+ assertCli(
174
178
  existing === undefined || isGenerated(existing),
175
179
  `Refusing to overwrite ${path}: it carries no \`<!-- toloka:sdk-version ... -->\` marker, so it is not a pointer generated by ${SDK_PACKAGE_NAME}. Move it aside, then re-run \`toloka init\`.`,
180
+ { hint: { disabled: true } },
176
181
  );
177
182
  };
178
183
 
@@ -215,9 +220,10 @@ const bindCheckout = async (
215
220
  ): Promise<string | undefined> => {
216
221
  if (projectId === undefined) return undefined;
217
222
  const existing = await readTolokaState(targetRoot);
218
- assert(
223
+ assertCli(
219
224
  existing === undefined || existing.projectId === projectId,
220
225
  `This directory is already bound to ${existing?.projectId}.`,
226
+ { hint: { description: "Run toloka init without --project-id to keep the current binding, or remove toloka.json to bind another project." } },
221
227
  );
222
228
  if (existing !== undefined) return undefined;
223
229
  await writeTolokaState(targetRoot, { projectId, pipelines: {}, datasets: {} });
package/src/cli/models.ts CHANGED
@@ -5,9 +5,9 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { Readable, Transform } from "node:stream";
6
6
  import { pipeline } from "node:stream/promises";
7
7
 
8
- import { assert } from "es-toolkit";
9
8
 
10
9
  import type { BaseModel, BaseModelId, Model, ModelVersion, PotatoClient } from "../client";
10
+ import { assertCli, REPORT_HINT, RETRY_OR_REPORT_HINT } from "./cli-error";
11
11
 
12
12
  type ModelApi = Pick<PotatoClient, "baseModels" | "models">;
13
13
  type ModelIo = { readonly stdout: (text: string) => void };
@@ -46,6 +46,7 @@ const formatBaseModel = (model: BaseModel): string =>
46
46
  ["Provider", model.provider],
47
47
  ["Upstream model", model.upstreamModel],
48
48
  ["Upstream revision", model.upstreamRevision],
49
+ ["Upstream snapshot", model.upstreamUrl],
49
50
  ]);
50
51
 
51
52
  const formatModel = (model: Model): string =>
@@ -62,7 +63,11 @@ const formatModel = (model: Model): string =>
62
63
 
63
64
  export const parseBaseModelId = (value: string): BaseModelId => {
64
65
  const modelId = BASE_MODEL_IDS.find((candidate) => candidate === value);
65
- assert(modelId, `Unknown base model ${value}.`);
66
+ assertCli(
67
+ modelId,
68
+ `Unknown base model ${value}.`,
69
+ { hint: { description: "Run toloka base-model list to see the catalog." } },
70
+ );
66
71
  return modelId;
67
72
  };
68
73
 
@@ -76,7 +81,11 @@ const resolveModel = async (
76
81
  for await (const model of client.models.listAll({ projectId, limit: 100 })) {
77
82
  if (model.name === nameOrId) matches.push(model);
78
83
  }
79
- assert(matches.length === 1, `Model ${nameOrId} was not found or is ambiguous.`);
84
+ assertCli(
85
+ matches.length === 1,
86
+ `Model ${nameOrId} was not found or is ambiguous.`,
87
+ { hint: { description: "Run toloka model list and pass the model id (mdl.…)." } },
88
+ );
80
89
  return matches[0]!;
81
90
  };
82
91
 
@@ -87,8 +96,16 @@ const resolveModelVersion = async (
87
96
  ): Promise<ModelVersion> => {
88
97
  if (nameOrId.startsWith("mdv.")) return client.models.getVersion({ modelVersionId: nameOrId });
89
98
  const separator = nameOrId.lastIndexOf("@");
90
- assert(separator > 0, `Model Version ${nameOrId} was not found. Use mdv.<id> or Model@name.`);
91
- assert(projectId, "Select a project to resolve Model Version names.");
99
+ assertCli(
100
+ separator > 0,
101
+ `Model Version ${nameOrId} was not found. Use mdv.<id> or Model@name.`,
102
+ { hint: { disabled: true } },
103
+ );
104
+ assertCli(
105
+ projectId,
106
+ "Select a project to resolve Model Version names.",
107
+ { hint: { description: "Select a project with toloka project add or toloka project switch, or pass the version id (mdv.…) directly." } },
108
+ );
92
109
  const model = await resolveModel(client, projectId, nameOrId.slice(0, separator));
93
110
  const versionName = nameOrId.slice(separator + 1);
94
111
  const matches: ModelVersion[] = [];
@@ -99,7 +116,11 @@ const resolveModelVersion = async (
99
116
  })) {
100
117
  if (version.name === versionName) matches.push(version);
101
118
  }
102
- assert(matches.length === 1, `Model Version ${nameOrId} was not found or is ambiguous.`);
119
+ assertCli(
120
+ matches.length === 1,
121
+ `Model Version ${nameOrId} was not found or is ambiguous.`,
122
+ { hint: { description: "Run toloka model versions <model> and pass the version id (mdv.…)." } },
123
+ );
103
124
  return matches[0]!;
104
125
  };
105
126
 
@@ -116,12 +137,14 @@ export const listBaseModels = async (
116
137
  models.length === 0
117
138
  ? "No base models found."
118
139
  : formatTable([
119
- ["ID", "NAME", "PROVIDER", "UPSTREAM MODEL"],
140
+ ["ID", "NAME", "PROVIDER", "UPSTREAM MODEL", "REVISION", "UPSTREAM SNAPSHOT"],
120
141
  ...models.map((model) => [
121
142
  model.id,
122
143
  model.displayName,
123
144
  model.provider,
124
145
  model.upstreamModel,
146
+ model.upstreamRevision.slice(0, 12),
147
+ model.upstreamUrl,
125
148
  ]),
126
149
  ]),
127
150
  );
@@ -238,25 +261,31 @@ export const downloadModelVersion = async (
238
261
  for (const file of descriptor.files) {
239
262
  const target = resolve(root, file.path);
240
263
  const relativeTarget = relative(root, target);
241
- assert(
264
+ assertCli(
242
265
  relativeTarget !== "" &&
243
266
  relativeTarget !== ".." &&
244
267
  !relativeTarget.startsWith(`..${sep}`) &&
245
268
  !isAbsolute(relativeTarget),
246
269
  `Invalid Model Version file path: ${file.path}`,
270
+ { hint: REPORT_HINT },
247
271
  );
248
272
  let parent = root;
249
273
  for (const segment of file.path.split("/").slice(0, -1)) {
250
274
  parent = resolve(parent, segment);
251
275
  await mkdir(parent, { recursive: true });
252
276
  const realParent = await realpath(parent);
253
- assert(
277
+ assertCli(
254
278
  realParent === realRoot || realParent.startsWith(`${realRoot}${sep}`),
255
279
  `Invalid Model Version file path: ${file.path}`,
280
+ { hint: REPORT_HINT },
256
281
  );
257
282
  }
258
283
  const response = await fetch(file.url, { redirect: "error" });
259
- assert(response.ok && response.body, `Failed to download Model Version file ${file.path}.`);
284
+ assertCli(
285
+ response.ok && response.body,
286
+ `Failed to download Model Version file ${file.path}.`,
287
+ { hint: RETRY_OR_REPORT_HINT },
288
+ );
260
289
  const hash = createHash("sha256");
261
290
  let byteSize = 0;
262
291
  const verify = new Transform({
@@ -274,13 +303,15 @@ export const downloadModelVersion = async (
274
303
  0o600,
275
304
  );
276
305
  await pipeline(Readable.from(response.body), verify, handle.createWriteStream());
277
- assert(
306
+ assertCli(
278
307
  byteSize === file.byteSize,
279
308
  `Downloaded Model Version file ${file.path} has an unexpected size.`,
309
+ { hint: { description: "Delete the partial download and retry; if it keeps failing, run toloka report." } },
280
310
  );
281
- assert(
311
+ assertCli(
282
312
  hash.digest("hex") === file.sha256,
283
313
  `Downloaded Model Version file ${file.path} has an invalid hash.`,
314
+ { hint: { description: "Delete the partial download and retry; if it keeps failing, run toloka report." } },
284
315
  );
285
316
  await rename(temporary, target);
286
317
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { Command, CommanderError } from "commander";
2
- import { createClient, type PotatoClient } from "../client";
2
+ import type { PotatoClient } from "../client";
3
3
  import { commandNameFromArgv, createCliAnalytics } from "./analytics";
4
4
  import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
5
5
  import type { AutoUpdateHooks } from "./auto-update";
@@ -19,11 +19,9 @@ import { registerSourceCommands } from "./commands/source";
19
19
  import { registerStatusCommand } from "./commands/status";
20
20
  import { registerTrainingCommands } from "./commands/training";
21
21
  import { registerVersionCommand } from "./commands/version";
22
- import { resolveTolokaConfig, type TolokaEnvironment } from "./config";
23
- import { findCommandCredentials } from "./credentials";
22
+ import type { TolokaEnvironment } from "./config";
24
23
  import {
25
24
  CREDENTIAL_PRECEDENCE,
26
- resolveInternalUser,
27
25
  resolveSourceRoot,
28
26
  type CliIo,
29
27
  type CreatedProgram,
@@ -39,51 +37,6 @@ import { terminalIo } from "./terminal-io";
39
37
  export type { CliIo, CreatedProgram, ProgramContext } from "./context";
40
38
  export { terminalIo } from "./terminal-io";
41
39
 
42
- const argvFlag = (argv: readonly string[], flag: string): string | undefined => {
43
- const index = argv.indexOf(flag);
44
- if (index < 0) return undefined;
45
- const value = argv[index + 1];
46
- return value === undefined || value.startsWith("-") ? undefined : value;
47
- };
48
-
49
- const resolveInternalCommandAccess = async (
50
- argv: readonly string[],
51
- environment: TolokaEnvironment,
52
- injectedClient: PotatoClient | undefined,
53
- ): Promise<boolean | undefined> => {
54
- const commandName = commandNameFromArgv(argv);
55
- if (
56
- commandName !== "help" &&
57
- commandName !== "project create" &&
58
- !commandName.startsWith("delivery-order")
59
- ) {
60
- return undefined;
61
- }
62
- if (injectedClient !== undefined) return resolveInternalUser(injectedClient);
63
- try {
64
- const apiKey = argvFlag(argv, "--api-key");
65
- const baseUrl = argvFlag(argv, "--base-url");
66
- const { found } = await findCommandCredentials(
67
- environment,
68
- apiKey ?? environment.TOLOKA_API_KEY,
69
- );
70
- const config = resolveTolokaConfig(
71
- {
72
- ...(apiKey === undefined ? {} : { apiKey }),
73
- ...(baseUrl === undefined ? {} : { baseUrl }),
74
- },
75
- undefined,
76
- environment,
77
- found?.credentials,
78
- );
79
- return await resolveInternalUser(
80
- createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl }),
81
- );
82
- } catch {
83
- return undefined;
84
- }
85
- };
86
-
87
40
  export function createProgram(context: ProgramContext): CreatedProgram {
88
41
  const program = new Command("toloka");
89
42
  const exitCode = { value: 0 };
@@ -91,7 +44,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
91
44
 
92
45
  program
93
46
  .description(
94
- "Beta. Build and operate Toloka pipeline projects from TypeScript source. The public API may change before general availability.",
47
+ "Build and operate Toloka pipeline projects from TypeScript source. The public API may change between minor versions before 1.0.",
95
48
  )
96
49
  .version(SDK_VERSION, "-v, --version", "output the current version")
97
50
  .option(
@@ -137,7 +90,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
137
90
  registerConfigCommands(program, host);
138
91
  registerAuthCommands(program, host);
139
92
  registerProjectCommands(program, host);
140
- if (context.internalUser === true) registerDeliveryOrderCommands(program, host);
93
+ registerDeliveryOrderCommands(program, host);
141
94
  registerSourceCommands(program, host);
142
95
  registerSecretCommands(program, host);
143
96
  registerModelCommands(program, host);
@@ -263,11 +216,7 @@ export async function runTolokaCli(
263
216
  ...(injectedClient === undefined ? {} : { injectedClient }),
264
217
  ...(runChecks === undefined ? {} : { runChecks }),
265
218
  };
266
- const internalUser = await resolveInternalCommandAccess(argv, environment, injectedClient);
267
- const { program, exitCode } = createProgram({
268
- ...baseContext,
269
- ...(internalUser === undefined ? {} : { internalUser }),
270
- });
219
+ const { program, exitCode } = createProgram(baseContext);
271
220
  const startedAt = Date.now();
272
221
 
273
222
  try {