@toloka-platform/sdk 0.1.0-preview.4 → 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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/toloka.js +132 -126
  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 +4 -0
  7. package/docs/cli.md +82 -6
  8. package/docs/datasets.md +8 -9
  9. package/docs/getting-started.md +2 -2
  10. package/docs/limitations.md +5 -8
  11. package/docs/troubleshooting.md +0 -11
  12. package/openapi/gen/api.ts +782 -876
  13. package/openapi/gen/models/baseModel.ts +9 -0
  14. package/openapi/gen/models/baseModelId.ts +2 -0
  15. package/openapi/gen/models/baseModelIdProperty.ts +2 -0
  16. package/openapi/gen/models/createDatasetBody.ts +2 -0
  17. package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
  18. package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
  19. package/openapi/gen/models/createProjectModelBody.ts +8 -0
  20. package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
  21. package/openapi/gen/models/dataset.ts +2 -0
  22. package/openapi/gen/models/datasetProfile.ts +3 -0
  23. package/openapi/gen/models/datasetProfileStamp.ts +6 -0
  24. package/openapi/gen/models/datasetProfileStampName.ts +2 -0
  25. package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  26. package/openapi/gen/models/index.ts +21 -0
  27. package/openapi/gen/models/listBaseModels200.ts +5 -0
  28. package/openapi/gen/models/listModelVersions200.ts +7 -0
  29. package/openapi/gen/models/listModelVersionsParams.ts +5 -0
  30. package/openapi/gen/models/listProjectModels200.ts +7 -0
  31. package/openapi/gen/models/listProjectModelsParams.ts +5 -0
  32. package/openapi/gen/models/model.ts +16 -0
  33. package/openapi/gen/models/modelArchivedAt.ts +2 -0
  34. package/openapi/gen/models/modelDescription.ts +2 -0
  35. package/openapi/gen/models/modelId.ts +2 -0
  36. package/openapi/gen/models/modelVersion.ts +9 -0
  37. package/openapi/gen/models/modelVersionId.ts +2 -0
  38. package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  39. package/openapi/gen/models/runCost.ts +7 -0
  40. package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
  41. package/package.json +2 -1
  42. package/skills/catalog.json +22 -0
  43. package/skills/{SKILL.md → toloka-build/SKILL.md} +2 -2
  44. package/skills/{references → toloka-build/references}/workflow-and-cli.md +26 -5
  45. package/skills/toloka-report-issue/SKILL.md +25 -0
  46. package/skills/toloka-report-issue/references/report-template.md +34 -0
  47. package/src/cli/analytics.ts +53 -1
  48. package/src/cli/data.ts +148 -25
  49. package/src/cli/file-system.ts +34 -0
  50. package/src/cli/format-error.ts +19 -0
  51. package/src/cli/init.ts +149 -194
  52. package/src/cli/models.ts +188 -0
  53. package/src/cli/program.ts +202 -22
  54. package/src/cli/report.ts +144 -0
  55. package/src/cli/skill.ts +363 -0
  56. package/src/client.ts +109 -35
  57. package/src/errors.ts +25 -10
  58. package/src/pipeline-graph-v1.contract.json +5 -1
  59. package/src/transport.ts +16 -13
  60. /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
@@ -0,0 +1,363 @@
1
+ import { assert } from "es-toolkit";
2
+ import { createHash } from "node:crypto";
3
+ import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { assertSafeDestination, entryStats, hasErrorCode } from "./file-system";
7
+ import { formatCliError } from "./format-error";
8
+
9
+ export const BUNDLED_SKILL_CATALOG_ROOT = fileURLToPath(new URL("../../skills", import.meta.url));
10
+ export const SKILL_CATALOG_MANIFEST = "catalog.json";
11
+ export const SKILL_INSTALL_ROOT = ".agents/skills";
12
+ export const SKILL_ENTRY_POINT = "SKILL.md";
13
+
14
+ const SKILL_CATALOG_DIRECTORY = "skills";
15
+ const MAX_SKILL_COUNT = 500;
16
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
17
+ const MAX_SKILL_ID_LENGTH = 64;
18
+ const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
19
+ const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
20
+
21
+ export type SkillDescriptor = {
22
+ readonly id: string;
23
+ readonly name: string;
24
+ readonly description: string;
25
+ readonly tags: readonly string[];
26
+ readonly version: string;
27
+ readonly digest: string;
28
+ readonly files: readonly string[];
29
+ };
30
+
31
+ export type SkillCatalog = {
32
+ readonly root: string;
33
+ readonly manifestPath: string;
34
+ readonly skills: readonly SkillDescriptor[];
35
+ };
36
+
37
+ export type SkillStatus = "not-installed" | "current" | "stale";
38
+
39
+ export type SkillListEntry = SkillDescriptor & { readonly status: SkillStatus };
40
+
41
+ export type InstalledSkill = {
42
+ readonly skill: SkillDescriptor;
43
+ readonly directory: string;
44
+ readonly replaced: boolean;
45
+ };
46
+
47
+ export type InstallSkillOptions = {
48
+ readonly catalog: SkillCatalog;
49
+ readonly sourceRoot: string;
50
+ readonly skillId: string;
51
+ readonly force?: boolean;
52
+ readonly rename?: typeof rename;
53
+ };
54
+
55
+ export type SkillIo = {
56
+ readonly stdout: (text: string) => void;
57
+ };
58
+
59
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
60
+ typeof value === "object" && value !== null && !Array.isArray(value);
61
+
62
+ const isStringArray = (value: unknown): value is string[] =>
63
+ Array.isArray(value) && value.every((item) => typeof item === "string");
64
+
65
+ const isNonEmptyString = (value: unknown): value is string =>
66
+ typeof value === "string" && value.trim().length > 0;
67
+
68
+ const isSafeSkillId = (value: unknown): value is string =>
69
+ typeof value === "string" && value.length <= MAX_SKILL_ID_LENGTH && SKILL_ID_PATTERN.test(value);
70
+
71
+ const isSafeRelativePath = (path: string) =>
72
+ !path.includes("\\") &&
73
+ !path.includes("\0") &&
74
+ path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
75
+
76
+ const parseDescriptor = (value: unknown, invalid: (detail: string) => string): SkillDescriptor => {
77
+ assert(isRecord(value), invalid("every skill must be an object."));
78
+ const { id, name, description, tags, version, digest, files } = value;
79
+ assert(
80
+ isSafeSkillId(id),
81
+ invalid(
82
+ `skill id ${JSON.stringify(id)} must be at most ${MAX_SKILL_ID_LENGTH} lowercase letters, digits, and single hyphens.`,
83
+ ),
84
+ );
85
+ assert(isNonEmptyString(name), invalid(`skill ${id} needs a name.`));
86
+ assert(isNonEmptyString(description), invalid(`skill ${id} needs a description.`));
87
+ assert(isStringArray(tags), invalid(`skill ${id} tags must be strings.`));
88
+ assert(isNonEmptyString(version), invalid(`skill ${id} needs a version.`));
89
+ assert(
90
+ typeof digest === "string" && DIGEST_PATTERN.test(digest),
91
+ invalid(`skill ${id} digest must be the sha256 hex digest of its files.`),
92
+ );
93
+ assert(
94
+ isStringArray(files) && files.includes(SKILL_ENTRY_POINT),
95
+ invalid(`skill ${id} files must include ${SKILL_ENTRY_POINT}.`),
96
+ );
97
+ assert(
98
+ files.every(isSafeRelativePath),
99
+ invalid(`skill ${id} files must be relative paths inside its directory.`),
100
+ );
101
+ assert(new Set(files).size === files.length, invalid(`skill ${id} lists a file more than once.`));
102
+ return { id, name, description, tags, version, digest, files };
103
+ };
104
+
105
+ const parseManifest = (source: string, manifestPath: string): unknown => {
106
+ try {
107
+ return JSON.parse(source);
108
+ } catch (error) {
109
+ throw new Error(`Invalid skill catalog ${manifestPath}: ${formatCliError(error)}`);
110
+ }
111
+ };
112
+
113
+ const isFileAt = async (path: string): Promise<boolean> => {
114
+ try {
115
+ return (await stat(path)).isFile();
116
+ } catch (error) {
117
+ if (hasErrorCode(error, "ENOENT", "ENOTDIR")) return false;
118
+ throw error;
119
+ }
120
+ };
121
+
122
+ const findSkillCatalogRootAbove = async (directory: string): Promise<string | undefined> => {
123
+ const candidate = join(directory, SKILL_CATALOG_DIRECTORY);
124
+ if (await isFileAt(join(candidate, SKILL_CATALOG_MANIFEST))) return candidate;
125
+ const parent = dirname(directory);
126
+ return parent === directory ? undefined : findSkillCatalogRootAbove(parent);
127
+ };
128
+
129
+ export async function resolveBundledSkillCatalogRoot(
130
+ runtimeEntry: string | undefined = process.argv[1],
131
+ ): Promise<string> {
132
+ const entry = runtimeEntry === undefined ? undefined : await realpath(runtimeEntry);
133
+ const runtimeRoot =
134
+ entry === undefined ? undefined : await findSkillCatalogRootAbove(dirname(entry));
135
+ if (runtimeRoot !== undefined) return runtimeRoot;
136
+ const searched =
137
+ entry === undefined
138
+ ? "process.argv[1] names no runtime entry"
139
+ : `none exists between ${entry} and the filesystem root`;
140
+ assert(
141
+ await isFileAt(join(BUNDLED_SKILL_CATALOG_ROOT, SKILL_CATALOG_MANIFEST)),
142
+ `Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and the source checkout catalog ${BUNDLED_SKILL_CATALOG_ROOT} is absent. Reinstall @toloka-platform/sdk.`,
143
+ );
144
+ return BUNDLED_SKILL_CATALOG_ROOT;
145
+ }
146
+
147
+ const loadBundledSkillCatalog = async (): Promise<SkillCatalog> =>
148
+ loadSkillCatalog(await resolveBundledSkillCatalogRoot());
149
+
150
+ export async function loadSkillCatalog(catalogRoot: string): Promise<SkillCatalog> {
151
+ const manifestPath = join(catalogRoot, SKILL_CATALOG_MANIFEST);
152
+ const invalid = (detail: string) => `Invalid skill catalog ${manifestPath}: ${detail}`;
153
+ const { size } = await stat(manifestPath);
154
+ assert(
155
+ size <= MAX_MANIFEST_BYTES,
156
+ invalid(`the manifest is ${size} bytes; the limit is ${MAX_MANIFEST_BYTES} bytes.`),
157
+ );
158
+ const manifest = parseManifest(await readFile(manifestPath, "utf8"), manifestPath);
159
+ assert(
160
+ isRecord(manifest) && Array.isArray(manifest.skills),
161
+ invalid("expected an object with a skills array."),
162
+ );
163
+ assert(
164
+ manifest.skills.length <= MAX_SKILL_COUNT,
165
+ invalid(`${manifest.skills.length} skills exceed the limit of ${MAX_SKILL_COUNT}.`),
166
+ );
167
+ const skills = manifest.skills.map((skill) => parseDescriptor(skill, invalid));
168
+ const ids = new Set<string>();
169
+ for (const skill of skills) {
170
+ assert(!ids.has(skill.id), invalid(`skill id ${skill.id} appears more than once.`));
171
+ ids.add(skill.id);
172
+ }
173
+ return { root: catalogRoot, manifestPath, skills };
174
+ }
175
+
176
+ export async function computeSkillDigest(
177
+ directory: string,
178
+ files: readonly string[],
179
+ ): Promise<string> {
180
+ const hash = createHash("sha256");
181
+ for (const path of files.toSorted()) {
182
+ hash.update(path);
183
+ hash.update("\0");
184
+ hash.update(await readFile(join(directory, path)));
185
+ hash.update("\0");
186
+ }
187
+ return hash.digest("hex");
188
+ }
189
+
190
+ export const searchSkills = (
191
+ skills: readonly SkillDescriptor[],
192
+ query: string,
193
+ ): SkillDescriptor[] => {
194
+ const needle = query.trim().toLowerCase();
195
+ return skills.filter((skill) =>
196
+ [skill.id, skill.name, skill.description, ...skill.tags].some((field) =>
197
+ field.toLowerCase().includes(needle),
198
+ ),
199
+ );
200
+ };
201
+
202
+ export async function skillStatus(
203
+ sourceRoot: string,
204
+ skill: SkillDescriptor,
205
+ ): Promise<SkillStatus> {
206
+ const directory = join(sourceRoot, SKILL_INSTALL_ROOT, skill.id);
207
+ if ((await entryStats(directory)) === undefined) return "not-installed";
208
+ try {
209
+ return (await computeSkillDigest(directory, skill.files)) === skill.digest
210
+ ? "current"
211
+ : "stale";
212
+ } catch (error) {
213
+ if (hasErrorCode(error, "ENOENT", "ENOTDIR")) return "stale";
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ export const listSkills = ({
219
+ sourceRoot,
220
+ skills,
221
+ }: {
222
+ readonly sourceRoot: string;
223
+ readonly skills: readonly SkillDescriptor[];
224
+ }): Promise<SkillListEntry[]> =>
225
+ Promise.all(
226
+ skills.map(async (skill) => ({ ...skill, status: await skillStatus(sourceRoot, skill) })),
227
+ );
228
+
229
+ const swapDirectories = async (
230
+ renameEntry: typeof rename,
231
+ replacement: string,
232
+ destination: string,
233
+ backup: string,
234
+ ) => {
235
+ await renameEntry(destination, backup);
236
+ try {
237
+ await renameEntry(replacement, destination);
238
+ } catch (error) {
239
+ try {
240
+ await renameEntry(backup, destination);
241
+ } catch (restoreError) {
242
+ throw new Error(
243
+ `Failed to replace ${destination}; the previous version is preserved at ${backup}. ${formatCliError(restoreError)}`,
244
+ { cause: error },
245
+ );
246
+ }
247
+ throw error;
248
+ }
249
+ await rm(backup, { recursive: true, force: true });
250
+ };
251
+
252
+ export async function installSkill({
253
+ catalog,
254
+ sourceRoot,
255
+ skillId,
256
+ force = false,
257
+ rename: renameEntry = rename,
258
+ }: InstallSkillOptions): Promise<InstalledSkill> {
259
+ const skill = catalog.skills.find((candidate) => candidate.id === skillId);
260
+ assert(
261
+ skill,
262
+ `Skill ${JSON.stringify(skillId)} is not in the catalog. Run \`toloka skill list\` to see the available skills.`,
263
+ );
264
+ const relativeDestination = `${SKILL_INSTALL_ROOT}/${skill.id}`;
265
+ await assertSafeDestination(sourceRoot, relativeDestination);
266
+ const destination = join(sourceRoot, relativeDestination);
267
+ const existing = await entryStats(destination);
268
+ assert(
269
+ existing === undefined || force,
270
+ `${relativeDestination} already exists. Re-run with --force to replace it with the catalog version of ${skill.id}.`,
271
+ );
272
+
273
+ const skillsRoot = dirname(destination);
274
+ await mkdir(skillsRoot, { recursive: true });
275
+ const temporary = await mkdtemp(join(skillsRoot, `.${skill.id}.tmp-`));
276
+ try {
277
+ for (const path of skill.files) {
278
+ const target = join(temporary, path);
279
+ await mkdir(dirname(target), { recursive: true });
280
+ await copyFile(join(catalog.root, skill.id, path), target);
281
+ }
282
+ assert(
283
+ (await computeSkillDigest(temporary, skill.files)) === skill.digest,
284
+ `Skill ${skill.id} failed its digest check: the packaged files do not match the catalog digest ${skill.digest}. Nothing was installed.`,
285
+ );
286
+ if (existing === undefined) {
287
+ await renameEntry(temporary, destination);
288
+ } else {
289
+ await swapDirectories(renameEntry, temporary, destination, `${temporary}.backup`);
290
+ }
291
+ } catch (error) {
292
+ await rm(temporary, { recursive: true, force: true });
293
+ throw error;
294
+ }
295
+ return { skill, directory: destination, replaced: existing !== undefined };
296
+ }
297
+
298
+ const formatEntries = (entries: readonly SkillListEntry[], json: boolean): string => {
299
+ if (json) return JSON.stringify(entries, undefined, 2);
300
+ const idWidth = Math.max(...entries.map((entry) => entry.id.length));
301
+ const statusWidth = Math.max(...entries.map((entry) => entry.status.length));
302
+ return entries
303
+ .flatMap((entry) => [
304
+ `${entry.id.padEnd(idWidth)} ${entry.status.padEnd(statusWidth)} ${entry.name}`,
305
+ ` ${entry.description}`,
306
+ ])
307
+ .join("\n");
308
+ };
309
+
310
+ export async function skillListCommand({
311
+ sourceRoot,
312
+ json,
313
+ io,
314
+ }: {
315
+ readonly sourceRoot: string;
316
+ readonly json: boolean;
317
+ readonly io: SkillIo;
318
+ }): Promise<void> {
319
+ const catalog = await loadBundledSkillCatalog();
320
+ io.stdout(formatEntries(await listSkills({ sourceRoot, skills: catalog.skills }), json));
321
+ }
322
+
323
+ export async function skillSearchCommand({
324
+ sourceRoot,
325
+ query,
326
+ json,
327
+ io,
328
+ }: {
329
+ readonly sourceRoot: string;
330
+ readonly query: string;
331
+ readonly json: boolean;
332
+ readonly io: SkillIo;
333
+ }): Promise<void> {
334
+ const catalog = await loadBundledSkillCatalog();
335
+ const matches = await listSkills({ sourceRoot, skills: searchSkills(catalog.skills, query) });
336
+ if (json || matches.length > 0) {
337
+ io.stdout(formatEntries(matches, json));
338
+ return;
339
+ }
340
+ io.stdout(
341
+ `No skills match ${JSON.stringify(query)}. Run \`toloka skill list\` to see the catalog.`,
342
+ );
343
+ }
344
+
345
+ export async function skillInstallCommand({
346
+ sourceRoot,
347
+ skillId,
348
+ force,
349
+ io,
350
+ }: {
351
+ readonly sourceRoot: string;
352
+ readonly skillId: string;
353
+ readonly force: boolean;
354
+ readonly io: SkillIo;
355
+ }): Promise<void> {
356
+ const catalog = await loadBundledSkillCatalog();
357
+ const installed = await installSkill({ catalog, sourceRoot, skillId, force });
358
+ const relativeDestination = `${SKILL_INSTALL_ROOT}/${installed.skill.id}`;
359
+ const fileCount = installed.skill.files.length;
360
+ io.stdout(
361
+ `${installed.replaced ? "Replaced" : "Installed"} ${installed.skill.id} in ${relativeDestination} (${fileCount} ${fileCount === 1 ? "file" : "files"}). Start with ${relativeDestination}/${SKILL_ENTRY_POINT}.`,
362
+ );
363
+ }
package/src/client.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  CopyPipelineBody,
9
9
  CreateDatasetBody,
10
10
  CreateDatasetExportBody,
11
+ CreateProjectModelBody,
11
12
  CreateDatasetFieldBodyAnyOf,
12
13
  CreateDatasetUploadBody,
13
14
  CreatePipelineCheckBody,
@@ -21,13 +22,17 @@ import type {
21
22
  ImportPipelineBody,
22
23
  InferredField as GeneratedInferredField,
23
24
  ListDatasetFieldsParams,
25
+ ListModelVersionsParams,
24
26
  ListDatasetItemsParams,
25
27
  ListPipelineRunItemsParams,
26
28
  ListPipelineRunsParams,
27
29
  ListProjectDatasetsParams,
28
30
  ListProjectPipelinesParams,
31
+ ListProjectModelsParams,
29
32
  ListProjectsParams,
30
33
  Pipeline as GeneratedPipeline,
34
+ Model as GeneratedModel,
35
+ ModelVersion as GeneratedModelVersion,
31
36
  PatchPipelineNodeBody,
32
37
  PipelineGraphSnapshot,
33
38
  Project as GeneratedProject,
@@ -38,6 +43,8 @@ import type {
38
43
  RunJob as GeneratedRunJob,
39
44
  RunSummary as GeneratedRunSummary,
40
45
  SetProjectSecretBody,
46
+ BaseModel as GeneratedBaseModel,
47
+ BaseModelId as GeneratedBaseModelId,
41
48
  UpdatePipelineBody,
42
49
  UpdateProjectBody,
43
50
  UploadJob as GeneratedUploadJob,
@@ -57,6 +64,8 @@ export type DatasetFieldId = Branded<string, "DatasetFieldId">;
57
64
  export type DatasetId = Branded<string, "DatasetId">;
58
65
  export type DatasetItemId = Branded<string, "DatasetItemId">;
59
66
  export type ExportId = Branded<string, "ExportId">;
67
+ export type ModelId = Branded<string, "ModelId">;
68
+ export type ModelVersionId = Branded<string, "ModelVersionId">;
60
69
  export type PipelineId = Branded<string, "PipelineId">;
61
70
  export type ProjectFileId = Branded<string, "ProjectFileId">;
62
71
  export type ProjectId = Branded<string, "ProjectId">;
@@ -76,6 +85,8 @@ type ResourceIdsByProperty = {
76
85
  exportId: ExportId;
77
86
  fieldId: DatasetFieldId;
78
87
  fileId: ProjectFileId;
88
+ modelId: ModelId;
89
+ modelVersionId: ModelVersionId;
79
90
  goldenDatasetId: DatasetId;
80
91
  itemId: RunItemId;
81
92
  pipelineId: PipelineId;
@@ -121,6 +132,10 @@ export type DatasetField = BrandedResource<GeneratedDatasetField, DatasetFieldId
121
132
  export type DatasetItem = BrandedResource<GeneratedDatasetItem, DatasetItemId>;
122
133
  export type ExportJob = BrandedResource<GeneratedExportJob, ExportId>;
123
134
  export type InferredField = GeneratedInferredField;
135
+ export type Model = BrandedResource<GeneratedModel, ModelId>;
136
+ export type ModelVersion = BrandedResource<GeneratedModelVersion, ModelVersionId>;
137
+ export type BaseModel = GeneratedBaseModel;
138
+ export type BaseModelId = GeneratedBaseModelId;
124
139
  export type Pipeline = BrandedResource<GeneratedPipeline, PipelineId>;
125
140
  export type Project = BrandedResource<GeneratedProject, ProjectId>;
126
141
  export type ProjectFile = BrandedResource<GeneratedProjectFile, ProjectFileId>;
@@ -178,6 +193,12 @@ export type WaitForTarget =
178
193
  const createClientImplementation = (config: ClientConfig) => {
179
194
  const requestOptions = createRequestOptions(config.baseUrl, config.apiKey, config.fetch);
180
195
 
196
+ const emptyJsonRequestOptions = (): RequestInit => {
197
+ const headers = new Headers(requestOptions.headers);
198
+ headers.set("Content-Type", "application/json");
199
+ return { ...requestOptions, headers, body: "{}" };
200
+ };
201
+
181
202
  function waitFor(
182
203
  target: DatasetUploadWaitTarget,
183
204
  options?: WaitForOptions,
@@ -296,6 +317,56 @@ const createClientImplementation = (config: ClientConfig) => {
296
317
  wire.copyPipeline(pipelineId, body, requestOptions),
297
318
  };
298
319
 
320
+ const baseModels = {
321
+ list: async () => (await wire.listBaseModels(requestOptions)).baseModels,
322
+ get: ({ id }: { id: BaseModelId }) => wire.getBaseModel(id, requestOptions),
323
+ };
324
+
325
+ const models = {
326
+ listPage: ({ projectId, ...params }: { projectId: ProjectId } & ListProjectModelsParams) =>
327
+ wire.listProjectModels(projectId, params, requestOptions),
328
+ listAll: ({ projectId, ...params }: { projectId: ProjectId } & ListProjectModelsParams) =>
329
+ listAllPages(async (pageOptions) => {
330
+ const page = await wire.listProjectModels(
331
+ projectId,
332
+ { ...params, ...pageOptions },
333
+ requestOptions,
334
+ );
335
+ return { items: page.models, pagination: page.pagination };
336
+ }, params),
337
+ get: ({ projectId, modelId }: { projectId: ProjectId; modelId: ModelId }) =>
338
+ wire.getProjectModel(projectId, modelId, requestOptions),
339
+ listVersionsPage: ({
340
+ projectId,
341
+ modelId,
342
+ ...params
343
+ }: { projectId: ProjectId; modelId: ModelId } & ListModelVersionsParams) =>
344
+ wire.listModelVersions(projectId, modelId, params, requestOptions),
345
+ listAllVersions: ({
346
+ projectId,
347
+ modelId,
348
+ ...params
349
+ }: { projectId: ProjectId; modelId: ModelId } & ListModelVersionsParams) =>
350
+ listAllPages(async (pageOptions) => {
351
+ const page = await wire.listModelVersions(
352
+ projectId,
353
+ modelId,
354
+ { ...params, ...pageOptions },
355
+ requestOptions,
356
+ );
357
+ return { items: page.versions, pagination: page.pagination };
358
+ }, params),
359
+ create: ({ projectId, ...body }: { projectId: ProjectId } & CreateProjectModelBody) =>
360
+ wire.createProjectModel(projectId, body, requestOptions),
361
+ getVersion: ({ modelVersionId }: { modelVersionId: ModelVersionId }) =>
362
+ wire.getModelVersion(modelVersionId, requestOptions),
363
+ archive: ({ projectId, modelId }: { projectId: ProjectId; modelId: ModelId }) => {
364
+ const headers = new Headers(requestOptions.headers);
365
+ headers.set("Content-Type", "application/json");
366
+ return wire.archiveProjectModel(projectId, modelId, { ...requestOptions, headers });
367
+ },
368
+ };
369
+
299
370
  const datasets = {
300
371
  create: ({ projectId, ...body }: { projectId: ProjectId } & CreateDatasetBody) =>
301
372
  wire.createDataset(projectId, body, requestOptions),
@@ -363,7 +434,9 @@ const createClientImplementation = (config: ClientConfig) => {
363
434
  startUpload: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetUploadBody) =>
364
435
  wire.createDatasetUpload(datasetId, body, requestOptions),
365
436
  finalizeUpload: ({ uploadId }: { uploadId: UploadId }) =>
366
- wire.finalizeDatasetUpload(uploadId, requestOptions),
437
+ wire.finalizeDatasetUpload(uploadId, emptyJsonRequestOptions()),
438
+ cancelUpload: ({ uploadId }: { uploadId: UploadId }) =>
439
+ wire.cancelDatasetUpload(uploadId, emptyJsonRequestOptions()),
367
440
  getUpload: ({ uploadId }: { uploadId: UploadId }) =>
368
441
  wire.getDatasetUpload(uploadId, requestOptions),
369
442
  startExport: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetExportBody) =>
@@ -373,15 +446,8 @@ const createClientImplementation = (config: ClientConfig) => {
373
446
  };
374
447
 
375
448
  const runs = {
376
- start: ({ pipelineId }: { pipelineId: PipelineId }) => {
377
- const headers = new Headers(requestOptions.headers);
378
- headers.set("Content-Type", "application/json");
379
- return wire.createPipelineRun(pipelineId, {
380
- ...requestOptions,
381
- headers,
382
- body: "{}",
383
- });
384
- },
449
+ start: ({ pipelineId }: { pipelineId: PipelineId }) =>
450
+ wire.createPipelineRun(pipelineId, emptyJsonRequestOptions()),
385
451
  get: ({ runId }: { runId: RunId }) => wire.getPipelineRun(runId, requestOptions),
386
452
  cancel: ({ runId }: { runId: RunId }) => wire.cancelPipelineRun(runId, requestOptions),
387
453
  listPage: ({ pipelineId, ...params }: { pipelineId: PipelineId } & ListPipelineRunsParams) =>
@@ -446,6 +512,8 @@ const createClientImplementation = (config: ClientConfig) => {
446
512
  projects,
447
513
  pipelines,
448
514
  datasets,
515
+ baseModels,
516
+ models,
449
517
  runs,
450
518
  secrets,
451
519
  files,
@@ -462,31 +530,37 @@ type PublicOutput<T> = T extends GeneratedCheckJob
462
530
  ? ExportJob
463
531
  : T extends GeneratedUploadJob
464
532
  ? UploadJob
465
- : T extends GeneratedProject
466
- ? Project
467
- : T extends GeneratedPipeline
468
- ? Pipeline
469
- : T extends GeneratedDataset
470
- ? Dataset
471
- : T extends GeneratedDatasetField
472
- ? DatasetField
473
- : T extends GeneratedDatasetItem
474
- ? DatasetItem
475
- : T extends GeneratedProjectFile
476
- ? ProjectFile
477
- : T extends GeneratedRunItem
478
- ? RunItem
479
- : T extends GeneratedRunSummary
480
- ? RunSummary
481
- : T extends Promise<infer Value>
482
- ? Promise<PublicOutput<Value>>
483
- : T extends AsyncGenerator<infer Value, infer Return, infer Next>
484
- ? AsyncGenerator<PublicOutput<Value>, PublicOutput<Return>, Next>
485
- : T extends readonly unknown[]
486
- ? { [K in keyof T]: PublicOutput<T[K]> }
487
- : T extends object
488
- ? { [K in keyof T]: PublicOutput<T[K]> }
489
- : T;
533
+ : T extends GeneratedModel
534
+ ? Model
535
+ : T extends GeneratedModelVersion
536
+ ? ModelVersion
537
+ : T extends GeneratedProject
538
+ ? Project
539
+ : T extends GeneratedPipeline
540
+ ? Pipeline
541
+ : T extends GeneratedDataset
542
+ ? Dataset
543
+ : T extends GeneratedDatasetField
544
+ ? DatasetField
545
+ : T extends GeneratedDatasetItem
546
+ ? DatasetItem
547
+ : T extends GeneratedProjectFile
548
+ ? ProjectFile
549
+ : T extends GeneratedRunItem
550
+ ? RunItem
551
+ : T extends GeneratedRunSummary
552
+ ? RunSummary
553
+ : T extends Blob
554
+ ? Blob
555
+ : T extends Promise<infer Value>
556
+ ? Promise<PublicOutput<Value>>
557
+ : T extends AsyncGenerator<infer Value, infer Return, infer Next>
558
+ ? AsyncGenerator<PublicOutput<Value>, PublicOutput<Return>, Next>
559
+ : T extends readonly unknown[]
560
+ ? { [K in keyof T]: PublicOutput<T[K]> }
561
+ : T extends object
562
+ ? { [K in keyof T]: PublicOutput<T[K]> }
563
+ : T;
490
564
 
491
565
  type PublicClientMember<T> = T extends (...args: infer Args) => infer Result
492
566
  ? (...args: { [K in keyof Args]: PublicInput<Args[K]> }) => PublicOutput<Result>
package/src/errors.ts CHANGED
@@ -1,28 +1,43 @@
1
- export type PotatoApiErrorCode = 'http' | 'wait_timeout';
1
+ export type PotatoApiErrorCode = "http" | "wait_timeout";
2
2
 
3
3
  export class PotatoApiError extends Error {
4
4
  readonly code: PotatoApiErrorCode;
5
5
  readonly status: number | undefined;
6
6
  readonly body: unknown;
7
+ readonly requestId: string | undefined;
7
8
 
8
- private constructor(message: string, code: PotatoApiErrorCode, status: number | undefined, body: unknown) {
9
+ private constructor(
10
+ message: string,
11
+ code: PotatoApiErrorCode,
12
+ status: number | undefined,
13
+ body: unknown,
14
+ requestId?: string,
15
+ ) {
9
16
  super(message);
10
- this.name = 'PotatoApiError';
17
+ this.name = "PotatoApiError";
11
18
  this.code = code;
12
19
  this.status = status;
13
20
  this.body = body;
21
+ this.requestId = requestId;
14
22
  }
15
23
 
16
- static fromResponse(status: number, body: unknown) {
17
- return new PotatoApiError(`Potato API request failed with status ${status}`, 'http', status, body);
24
+ static fromResponse(status: number, body: unknown, requestId?: string) {
25
+ const requestSuffix = requestId === undefined ? "" : ` (request id: ${requestId})`;
26
+ return new PotatoApiError(
27
+ `Potato API request failed with status ${status}${requestSuffix}`,
28
+ "http",
29
+ status,
30
+ body,
31
+ requestId,
32
+ );
18
33
  }
19
34
 
20
35
  static waitTimeout(timeoutMs: number) {
21
36
  return new PotatoApiError(
22
37
  `Potato API job did not reach a terminal status within ${timeoutMs} ms`,
23
- 'wait_timeout',
38
+ "wait_timeout",
24
39
  undefined,
25
- { timeoutMs }
40
+ { timeoutMs },
26
41
  );
27
42
  }
28
43
  }
@@ -33,17 +48,17 @@ export type PipelineGraphValidationIssue = {
33
48
  };
34
49
 
35
50
  export class PipelineGraphValidationError extends Error {
36
- readonly code = 'invalid_pipeline_graph';
51
+ readonly code = "invalid_pipeline_graph";
37
52
  readonly issues: readonly PipelineGraphValidationIssue[];
38
53
 
39
54
  private constructor(message: string, issues: readonly PipelineGraphValidationIssue[]) {
40
55
  super(message);
41
- this.name = 'PipelineGraphValidationError';
56
+ this.name = "PipelineGraphValidationError";
42
57
  this.issues = issues;
43
58
  }
44
59
 
45
60
  static fromIssues(issues: readonly PipelineGraphValidationIssue[]) {
46
- const details = issues.map(({ path, reason }) => `- ${path}: ${reason}`).join('\n');
61
+ const details = issues.map(({ path, reason }) => `- ${path}: ${reason}`).join("\n");
47
62
  return new PipelineGraphValidationError(`Pipeline graph validation failed\n${details}`, issues);
48
63
  }
49
64
  }
@@ -1633,10 +1633,14 @@
1633
1633
  "llmInstruction": {
1634
1634
  "default": "",
1635
1635
  "type": "string"
1636
+ },
1637
+ "maxCostPerAttempt": {
1638
+ "type": "number"
1636
1639
  }
1637
1640
  },
1638
1641
  "required": [
1639
- "audienceType"
1642
+ "audienceType",
1643
+ "maxCostPerAttempt"
1640
1644
  ],
1641
1645
  "additionalProperties": false
1642
1646
  }