Skip to content

Commit d567d6a

Browse files
committed
feat: setup model train/deploy cli commend
1 parent 1fd08fe commit d567d6a

55 files changed

Lines changed: 6065 additions & 74 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/cli/src/commands/catalog.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ import searchWeb from "./search/web.ts";
3434
import speechSynthesize from "./speech/synthesize.ts";
3535
import speechRecognize from "./speech/recognize.ts";
3636
import fileUpload from "./file/upload.ts";
37+
import datasetUpload from "./dataset/upload.ts";
38+
import datasetList from "./dataset/list.ts";
39+
import datasetGet from "./dataset/get.ts";
40+
import datasetDelete from "./dataset/delete.ts";
41+
import datasetValidate from "./dataset/validate.ts";
42+
import finetuneCreate from "./finetune/create.ts";
43+
import finetuneList from "./finetune/list.ts";
44+
import finetuneGet from "./finetune/get.ts";
45+
import finetuneCancel from "./finetune/cancel.ts";
46+
import finetuneDelete from "./finetune/delete.ts";
47+
import finetuneLogs from "./finetune/logs.ts";
48+
import finetuneCheckpoints from "./finetune/checkpoints.ts";
49+
import finetuneExport from "./finetune/export.ts";
50+
import finetuneWatch from "./finetune/watch.ts";
51+
import finetuneCapability from "./finetune/capability.ts";
52+
import deployCreate from "./deploy/create.ts";
53+
import deployList from "./deploy/list.ts";
54+
import deployGet from "./deploy/get.ts";
55+
import deployModels from "./deploy/models.ts";
56+
import deployScale from "./deploy/scale.ts";
57+
import deployUpdate from "./deploy/update.ts";
58+
import deployDelete from "./deploy/delete.ts";
3759
import consoleCall from "./console/call.ts";
3860
import usageFree from "./usage/free.ts";
3961
import usageFreetier from "./usage/freetier.ts";
@@ -79,6 +101,28 @@ export const commands: Record<string, Command> = {
79101
"speech synthesize": speechSynthesize,
80102
"speech recognize": speechRecognize,
81103
"file upload": fileUpload,
104+
"dataset upload": datasetUpload,
105+
"dataset list": datasetList,
106+
"dataset get": datasetGet,
107+
"dataset delete": datasetDelete,
108+
"dataset validate": datasetValidate,
109+
"finetune create": finetuneCreate,
110+
"finetune list": finetuneList,
111+
"finetune get": finetuneGet,
112+
"finetune cancel": finetuneCancel,
113+
"finetune delete": finetuneDelete,
114+
"finetune logs": finetuneLogs,
115+
"finetune checkpoints": finetuneCheckpoints,
116+
"finetune export": finetuneExport,
117+
"finetune watch": finetuneWatch,
118+
"finetune capability": finetuneCapability,
119+
"deploy create": deployCreate,
120+
"deploy list": deployList,
121+
"deploy get": deployGet,
122+
"deploy models": deployModels,
123+
"deploy scale": deployScale,
124+
"deploy update": deployUpdate,
125+
"deploy delete": deployDelete,
82126
"console call": consoleCall,
83127
"usage free": usageFree,
84128
"usage freetier": usageFreetier,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
defineCommand,
3+
detectOutputFormat,
4+
deleteDataset,
5+
isInteractive,
6+
BailianError,
7+
ExitCode,
8+
type Config,
9+
type GlobalFlags,
10+
} from "bailian-cli-core";
11+
import { failIfMissing, promptConfirm } from "../../output/prompt.ts";
12+
import { emitResult, emitBare } from "../../output/output.ts";
13+
14+
export default defineCommand({
15+
name: "dataset delete",
16+
description: "Delete a dataset file by ID",
17+
usage: "bl dataset delete --file-id <id> [--yes]",
18+
options: [
19+
{ flag: "--file-id <id>", description: "Dataset file ID (required)", required: true },
20+
{ flag: "--yes", description: "Skip the confirmation prompt", type: "boolean" },
21+
],
22+
examples: [
23+
"bl dataset delete --file-id file-id-xxx",
24+
"bl dataset delete --file-id file-id-xxx --yes",
25+
],
26+
async run(config: Config, flags: GlobalFlags) {
27+
const fileId = flags.fileId as string | undefined;
28+
if (!fileId) failIfMissing("file-id", "bl dataset delete --file-id <id>");
29+
30+
const format = detectOutputFormat(config.output);
31+
const yes = Boolean(flags.yes);
32+
33+
if (config.dryRun) {
34+
emitResult({ action: "dataset.delete", file_id: fileId }, format);
35+
return;
36+
}
37+
38+
if (!yes) {
39+
if (isInteractive({ nonInteractive: config.nonInteractive })) {
40+
const ok = await promptConfirm({
41+
message: `Permanently delete dataset file ${fileId}? This cannot be undone.`,
42+
initialValue: false,
43+
});
44+
if (!ok) {
45+
emitBare("Aborted.");
46+
return;
47+
}
48+
} else {
49+
throw new BailianError(
50+
`Refusing to delete ${fileId} without --yes in non-interactive mode.`,
51+
ExitCode.USAGE,
52+
"Pass --yes to skip the confirmation prompt.",
53+
);
54+
}
55+
}
56+
57+
const response = await deleteDataset(config, fileId!);
58+
59+
if (config.quiet || format === "text") {
60+
emitBare(`Deleted ${fileId}.`);
61+
} else {
62+
emitResult(response, format);
63+
}
64+
},
65+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
defineCommand,
3+
detectOutputFormat,
4+
getDataset,
5+
type Config,
6+
type GlobalFlags,
7+
} from "bailian-cli-core";
8+
import { failIfMissing } from "../../output/prompt.ts";
9+
import { emitResult, emitBare } from "../../output/output.ts";
10+
11+
export default defineCommand({
12+
name: "dataset get",
13+
description: "Get details of a single dataset file",
14+
usage: "bl dataset get --file-id <id>",
15+
options: [{ flag: "--file-id <id>", description: "Dataset file ID (required)", required: true }],
16+
examples: [
17+
"bl dataset get --file-id file-xxx",
18+
"bl dataset get --file-id file-xxx --output json",
19+
],
20+
async run(config: Config, flags: GlobalFlags) {
21+
const fileId = flags.fileId as string | undefined;
22+
if (!fileId) failIfMissing("file-id", "bl dataset get --file-id <id>");
23+
24+
const format = detectOutputFormat(config.output);
25+
26+
if (config.dryRun) {
27+
emitResult({ action: "dataset.get", file_id: fileId }, format);
28+
return;
29+
}
30+
31+
const response = await getDataset(config, fileId!);
32+
const file = response.data;
33+
34+
if (!file) {
35+
emitBare(`No data returned for ${fileId}`);
36+
return;
37+
}
38+
39+
const sizeKb = file.size !== undefined ? `${(file.size / 1024).toFixed(1)} KB` : "?";
40+
const item = {
41+
file_id: file.file_id ?? fileId,
42+
name: file.name ?? "",
43+
size: sizeKb,
44+
md5: file.md5 ?? "",
45+
purpose: file.purpose ?? "",
46+
created_at: file.gmt_create ?? "",
47+
description: file.description ?? "",
48+
};
49+
50+
if (format === "json") {
51+
emitResult(item, format);
52+
return;
53+
}
54+
55+
// text / quiet
56+
emitBare(`file_id: ${item.file_id}`);
57+
emitBare(`name: ${item.name}`);
58+
emitBare(`size: ${item.size}`);
59+
if (item.md5) emitBare(`md5: ${item.md5}`);
60+
if (item.purpose) emitBare(`purpose: ${item.purpose}`);
61+
if (item.created_at) emitBare(`created_at: ${item.created_at}`);
62+
if (item.description) emitBare(`description: ${item.description}`);
63+
},
64+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
defineCommand,
3+
detectOutputFormat,
4+
listDatasets,
5+
type Config,
6+
type GlobalFlags,
7+
} from "bailian-cli-core";
8+
import { emitResult, emitBare } from "../../output/output.ts";
9+
import { formatTable } from "../../output/table.ts";
10+
11+
export default defineCommand({
12+
name: "dataset list",
13+
description: "List uploaded dataset files",
14+
usage: "bl dataset list [--page <n>] [--page-size <n>] [--purpose <name>]",
15+
options: [
16+
{ flag: "--page <n>", description: "Page number (default: 1)", type: "number" },
17+
{
18+
flag: "--page-size <n>",
19+
description: "Results per page (default: 10, max 100)",
20+
type: "number",
21+
},
22+
{
23+
flag: "--purpose <name>",
24+
description: 'Filter by purpose (e.g. "fine-tune", "evaluation"). Omit to list all.',
25+
},
26+
],
27+
examples: [
28+
"bl dataset list",
29+
"bl dataset list --purpose fine-tune",
30+
"bl dataset list --purpose evaluation --page-size 20",
31+
"bl dataset list --output json",
32+
],
33+
async run(config: Config, flags: GlobalFlags) {
34+
const format = detectOutputFormat(config.output);
35+
const pageNo = flags.page !== undefined ? (flags.page as number) : undefined;
36+
const pageSize = flags.pageSize !== undefined ? (flags.pageSize as number) : undefined;
37+
const purpose = (flags.purpose as string | undefined) || undefined;
38+
39+
if (config.dryRun) {
40+
emitResult({ action: "dataset.list", page: pageNo, page_size: pageSize, purpose }, format);
41+
return;
42+
}
43+
44+
const response = await listDatasets(config, { pageNo, pageSize, purpose });
45+
const files = response.data?.files ?? [];
46+
const total = response.data?.total;
47+
48+
// Normalize to consistent structure for both text/json output.
49+
const items = files.map((f) => ({
50+
file_id: f.file_id ?? "",
51+
name: f.name ?? "",
52+
size: f.size !== undefined ? `${(f.size / 1024).toFixed(1)} KB` : "?",
53+
purpose: f.purpose ?? "",
54+
}));
55+
56+
if (format === "json") {
57+
emitResult({ items, total }, format);
58+
return;
59+
}
60+
61+
// text / quiet
62+
if (items.length === 0) {
63+
emitBare("No dataset files found.");
64+
return;
65+
}
66+
const headers = ["FILE_ID", "NAME", "SIZE", "PURPOSE"];
67+
const rows = items.map((i) => [i.file_id, i.name, i.size, i.purpose]);
68+
for (const line of formatTable(headers, rows)) emitBare(line);
69+
if (total !== undefined) emitBare(`\nTotal: ${total}`);
70+
},
71+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
defineCommand,
3+
detectOutputFormat,
4+
uploadDataset,
5+
validateDataset,
6+
MAX_DATASET_BYTES,
7+
BailianError,
8+
ExitCode,
9+
type Config,
10+
type GlobalFlags,
11+
type DatasetFile,
12+
type ValidationResult,
13+
} from "bailian-cli-core";
14+
import { failIfMissing } from "../../output/prompt.ts";
15+
import { emitResult, emitBare } from "../../output/output.ts";
16+
17+
/**
18+
* Format a single validation issue as a one-line string.
19+
*/
20+
function formatIssue(issue: ValidationResult["errors"][number]): string {
21+
const where: string[] = [];
22+
if (issue.line !== undefined) where.push(`line ${issue.line}`);
23+
if (issue.path) where.push(issue.path);
24+
const tag = where.length ? ` [${where.join(" · ")}]` : "";
25+
return ` ${issue.severity.toUpperCase()} ${issue.code}${tag}: ${issue.message}`;
26+
}
27+
28+
export default defineCommand({
29+
name: "dataset upload",
30+
description: "Upload a dataset file (.jsonl) to Bailian",
31+
usage: "bl dataset upload --file <path> [--purpose <name>] [--no-validate] [--full-validate]",
32+
options: [
33+
{
34+
flag: "--file <path>",
35+
description: "Local .jsonl dataset file (≤300MB)",
36+
required: true,
37+
},
38+
{
39+
flag: "--purpose <name>",
40+
description: 'Dataset purpose tag (default: "fine-tune"; e.g. "evaluation")',
41+
},
42+
{
43+
flag: "--no-validate",
44+
description: "Skip the local JSONL pre-flight check (not recommended)",
45+
type: "boolean",
46+
},
47+
{
48+
flag: "--full-validate",
49+
description: "JSON.parse every line instead of sampling (slower)",
50+
type: "boolean",
51+
},
52+
],
53+
examples: [
54+
"bl dataset upload --file train.jsonl",
55+
"bl dataset upload --file eval.jsonl --purpose evaluation",
56+
"bl dataset upload --file train.jsonl --full-validate",
57+
"bl dataset upload --file train.jsonl --no-validate",
58+
],
59+
notes: [
60+
"Only .jsonl is supported in this release. The default validator expects a",
61+
'ChatML schema (each line a JSON object with a "messages" array). Other',
62+
"purposes may carry a different schema in the future and would be served",
63+
"by a purpose-specific validator at that point.",
64+
"The dataset upload cap is 300MB per file.",
65+
"Upload uses the OpenAI-compatible /compatible-mode/v1/files endpoint so",
66+
"the purpose tag is persisted (the DashScope-native /api/v1/files drops it).",
67+
],
68+
async run(config: Config, flags: GlobalFlags) {
69+
const filePath = flags.file as string | undefined;
70+
if (!filePath) failIfMissing("file", "bl dataset upload --file <path>");
71+
72+
const purpose = (flags.purpose as string | undefined) || "fine-tune";
73+
const skipValidate = Boolean(flags.noValidate);
74+
const fullValidate = Boolean(flags.fullValidate);
75+
const format = detectOutputFormat(config.output);
76+
77+
if (!skipValidate) {
78+
const result = await validateDataset(filePath!, { fullValidate });
79+
if (!result.valid) {
80+
const lines = [
81+
`Dataset validation failed for ${filePath}`,
82+
...result.errors.slice(0, 10).map(formatIssue),
83+
];
84+
if (result.errors.length > 10) {
85+
lines.push(` … and ${result.errors.length - 10} more error(s).`);
86+
}
87+
lines.push(
88+
"",
89+
"Hint: re-run `bl dataset validate --file <path>` for the full report,",
90+
" or pass --no-validate to skip this check at your own risk.",
91+
);
92+
throw new BailianError(lines.join("\n"), ExitCode.GENERAL);
93+
}
94+
// Surface warnings to stderr but keep going.
95+
if (result.warnings.length > 0 && !config.quiet) {
96+
process.stderr.write(
97+
`Dataset validation passed with ${result.warnings.length} warning(s):\n`,
98+
);
99+
for (const warning of result.warnings.slice(0, 5))
100+
process.stderr.write(`${formatIssue(warning)}\n`);
101+
if (result.warnings.length > 5) {
102+
process.stderr.write(` … and ${result.warnings.length - 5} more.\n`);
103+
}
104+
}
105+
}
106+
107+
if (config.dryRun) {
108+
emitResult(
109+
{
110+
action: "dataset.upload",
111+
file: filePath,
112+
purpose,
113+
max_bytes: MAX_DATASET_BYTES,
114+
validate: !skipValidate,
115+
},
116+
format,
117+
);
118+
return;
119+
}
120+
121+
const uploaded: DatasetFile = await uploadDataset(config, {
122+
filePath: filePath!,
123+
purpose,
124+
});
125+
126+
if (config.quiet) {
127+
emitBare(uploaded.file_id);
128+
} else if (format === "text") {
129+
emitBare(`Uploaded ${uploaded.name} → file_id=${uploaded.file_id}`);
130+
} else {
131+
emitResult(uploaded, format);
132+
}
133+
},
134+
});

0 commit comments

Comments
 (0)