diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 2cbda30..bd18f4d 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -80,6 +80,8 @@ import { deployScale, deployUpdate, deployDelete, + deployPause, + deployResume, tokenPlanListSeats, tokenPlanCreateKey, tokenPlanAssignSeats, @@ -198,6 +200,8 @@ export const commands: Record = { "deploy scale": deployScale, "deploy update": deployUpdate, "deploy delete": deployDelete, + "deploy pause": deployPause, + "deploy resume": deployResume, "token-plan list-seats": tokenPlanListSeats, "token-plan create-key": tokenPlanCreateKey, "token-plan assign-seats": tokenPlanAssignSeats, diff --git a/packages/commands/src/commands/dataset/delete.ts b/packages/commands/src/commands/dataset/delete.ts index 5e18766..4c1e888 100644 --- a/packages/commands/src/commands/dataset/delete.ts +++ b/packages/commands/src/commands/dataset/delete.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, deleteDataset, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { fileId: { @@ -19,20 +19,18 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const fileId = flags.fileId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "dataset.delete", file_id: fileId }, format); + emitResult({ action: "dataset.delete", file_id: fileId }, "json"); return; } const response = await deleteDataset(ctx.client, fileId); - if (settings.quiet || format === "text") { - emitBare(`Deleted ${fileId}.`); - emitRequestId(response.request_id, settings.quiet); + if (settings.quiet) { + emitBare(fileId); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/dataset/get.ts b/packages/commands/src/commands/dataset/get.ts index 2c1cf71..f9a4145 100644 --- a/packages/commands/src/commands/dataset/get.ts +++ b/packages/commands/src/commands/dataset/get.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, getDataset, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, getDataset, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const GET_FLAGS = { fileId: { @@ -19,10 +19,9 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const fileId = flags.fileId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "dataset.get", file_id: fileId }, format); + emitResult({ action: "dataset.get", file_id: fileId }, "json"); return; } @@ -45,19 +44,10 @@ export default defineCommand({ description: file.description ?? "", }; - if (format === "json") { - emitResult({ ...item, request_id: response.request_id }, format); - return; + if (settings.quiet) { + emitBare(item.file_id); + } else { + emitResult({ ...item, request_id: response.request_id }, "json"); } - - // text / quiet - emitBare(`file_id: ${item.file_id}`); - emitBare(`name: ${item.name}`); - emitBare(`size: ${item.size}`); - if (item.md5) emitBare(`md5: ${item.md5}`); - if (item.purpose) emitBare(`purpose: ${item.purpose}`); - if (item.created_at) emitBare(`created_at: ${item.created_at}`); - if (item.description) emitBare(`description: ${item.description}`); - emitRequestId(response.request_id, settings.quiet); }, }); diff --git a/packages/commands/src/commands/dataset/list.ts b/packages/commands/src/commands/dataset/list.ts index 7bc858b..63bbfeb 100644 --- a/packages/commands/src/commands/dataset/list.ts +++ b/packages/commands/src/commands/dataset/list.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, listDatasets, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime"; +import { defineCommand, listDatasets, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const LIST_FLAGS = { page: { type: "number", valueHint: "", description: "Page number (default: 1)" }, @@ -23,7 +23,6 @@ export default defineCommand({ exampleArgs: ["", "--purpose fine-tune", "--purpose evaluation --page-size 20", "--output json"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { emitResult( @@ -33,7 +32,7 @@ export default defineCommand({ page_size: flags.pageSize, purpose: flags.purpose, }, - format, + "json", ); return; } @@ -46,7 +45,6 @@ export default defineCommand({ const files = response.data?.files ?? []; const total = response.data?.total; - // Normalize to consistent structure for both text/json output. const items = files.map((item) => ({ file_id: item.file_id ?? "", name: item.name ?? "", @@ -54,20 +52,10 @@ export default defineCommand({ purpose: item.purpose ?? "", })); - if (format === "json") { - emitResult({ items, total, request_id: response.request_id }, format); - return; + if (settings.quiet) { + for (const item of items) emitBare(item.file_id); + } else { + emitResult({ items, total, request_id: response.request_id }, "json"); } - - // text / quiet - if (items.length === 0) { - emitBare("No dataset files found."); - return; - } - const headers = ["FILE_ID", "NAME", "SIZE", "PURPOSE"]; - const rows = items.map((i) => [i.file_id, i.name, i.size, i.purpose]); - for (const line of formatTable(headers, rows)) emitBare(line); - if (total !== undefined) emitBare(`\nTotal: ${total}`); - emitRequestId(response.request_id, settings.quiet); }, }); diff --git a/packages/commands/src/commands/dataset/upload.ts b/packages/commands/src/commands/dataset/upload.ts index a3c6104..1363509 100644 --- a/packages/commands/src/commands/dataset/upload.ts +++ b/packages/commands/src/commands/dataset/upload.ts @@ -1,6 +1,5 @@ import { defineCommand, - detectOutputFormat, uploadDataset, validateDataset, parseDatasetSchemaFlag, @@ -11,7 +10,7 @@ import { ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const UPLOAD_FLAGS = { file: { @@ -80,7 +79,6 @@ export default defineCommand({ `Supported schemas: chatml, dpo, cpt, tts, image.`, ); } - const format = detectOutputFormat(settings.output); // Image schema allows larger ZIPs (1 GB vs 300 MB for text). const isMediaSchema = schema === "image"; @@ -129,7 +127,7 @@ export default defineCommand({ validate: !flags.noValidate, schema: schema ?? "auto", }, - format, + "json", ); return; } @@ -142,11 +140,8 @@ export default defineCommand({ if (settings.quiet) { emitBare(file.file_id); - } else if (format === "text") { - emitBare(`Uploaded ${file.name} → file_id=${file.file_id}`); - emitRequestId(request_id, settings.quiet); } else { - emitResult({ ...file, request_id }, format); + emitResult({ ...file, request_id }, "json"); } }, }); diff --git a/packages/commands/src/commands/dataset/validate.ts b/packages/commands/src/commands/dataset/validate.ts index 8bf0d29..142fbdf 100644 --- a/packages/commands/src/commands/dataset/validate.ts +++ b/packages/commands/src/commands/dataset/validate.ts @@ -1,26 +1,13 @@ import { defineCommand, - detectOutputFormat, validateDataset, parseDatasetSchemaFlag, - formatIssue, BailianError, ExitCode, - type ValidationResult, type FlagsDef, } from "bailian-cli-core"; import { emitResult, emitBare } from "bailian-cli-runtime"; -function formatStats(result: ValidationResult): string[] { - const out: string[] = []; - if (result.stats.totalRecords !== undefined) out.push(`records: ${result.stats.totalRecords}`); - if (result.stats.sampledRecords !== undefined) - out.push(`sampled: ${result.stats.sampledRecords}`); - if (result.stats.bytes !== undefined) out.push(`bytes: ${result.stats.bytes}`); - if (result.stats.durationMs !== undefined) out.push(`took: ${result.stats.durationMs}ms`); - return out; -} - const VALIDATE_FLAGS = { file: { type: "string", @@ -79,8 +66,6 @@ export default defineCommand({ `Supported schemas: chatml, dpo, cpt, tts, image.`, ); } - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { emitResult( { @@ -89,38 +74,17 @@ export default defineCommand({ full: flags.fullValidate, schema: schema ?? "auto", }, - format, + "json", ); return; } const result = await validateDataset(filePath, { fullValidate: flags.fullValidate, schema }); - if (format === "json") { - // For json output we always emit the structured result, exit code conveys validity. - emitResult(result, format); - } else if (settings.quiet) { + if (settings.quiet) { emitBare(result.valid ? "ok" : "fail"); } else { - const status = result.valid ? "PASSED" : "FAILED"; - emitBare(`Dataset validation ${status} for ${result.filePath}`); - const stats = formatStats(result); - if (stats.length) emitBare(` ${stats.join(" · ")}`); - - if (result.errors.length) { - emitBare(`Errors (${result.errors.length}):`); - for (const error of result.errors.slice(0, 20)) emitBare(formatIssue(error)); - if (result.errors.length > 20) { - emitBare(` … and ${result.errors.length - 20} more.`); - } - } - if (result.warnings.length) { - emitBare(`Warnings (${result.warnings.length}):`); - for (const warning of result.warnings.slice(0, 10)) emitBare(formatIssue(warning)); - if (result.warnings.length > 10) { - emitBare(` … and ${result.warnings.length - 10} more.`); - } - } + emitResult(result, "json"); } if (!result.valid) { diff --git a/packages/commands/src/commands/deploy/create.ts b/packages/commands/src/commands/deploy/create.ts index 96c75d9..15f6b2a 100644 --- a/packages/commands/src/commands/deploy/create.ts +++ b/packages/commands/src/commands/deploy/create.ts @@ -1,6 +1,5 @@ import { defineCommand, - detectOutputFormat, createDeployment, pickPlanStrategy, STRATEGIES, @@ -11,7 +10,7 @@ import { type CommandContext, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const CREATE_FLAGS = { model: { @@ -122,7 +121,6 @@ async function runCreate( const model = flags.model as string; const name = flags.name as string; const plan = (flags.plan as string | undefined) || defaultDeployPlan(modality); - const format = detectOutputFormat(settings.output); // Plan-specific behaviour is owned by core `plans.ts`. The strategy resolves // the plan-specific body fragment (mu may auto-pick a template from the @@ -146,7 +144,7 @@ async function runCreate( }; if (settings.dryRun) { - emitResult({ action: "deploy.create", body }, format); + emitResult({ action: "deploy.create", body }, "json"); return; } @@ -155,17 +153,8 @@ async function runCreate( if (settings.quiet) { emitBare(deployment?.deployed_model ?? ""); - } else if (format === "text") { - emitBare(`Created deployment.`); - if (deployment?.deployed_model) emitBare(` deployed_model: ${deployment.deployed_model}`); - if (deployment?.status) emitBare(` status: ${deployment.status}`); - if (deployment?.plan) emitBare(` plan: ${deployment.plan}`); - emitBare( - `\nNext: track readiness with: ${identity.binName} deploy get --deployed-model ${deployment?.deployed_model ?? ""}`, - ); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } } diff --git a/packages/commands/src/commands/deploy/delete.ts b/packages/commands/src/commands/deploy/delete.ts index bd6dd85..d3f386d 100644 --- a/packages/commands/src/commands/deploy/delete.ts +++ b/packages/commands/src/commands/deploy/delete.ts @@ -1,13 +1,12 @@ import { defineCommand, - detectOutputFormat, deleteDeployment, getDeployment, BailianError, ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { deployedModel: { @@ -38,10 +37,9 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const deployedModel = flags.deployedModel; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "deploy.delete", deployed_model: deployedModel }, format); + emitResult({ action: "deploy.delete", deployed_model: deployedModel }, "json"); return; } @@ -55,7 +53,8 @@ export default defineCommand({ if (status && status !== "STOPPED" && status !== "FAILED") { throw new BailianError( `Deployment ${deployedModel} is ${status}. Only STOPPED / FAILED deployments can be deleted. ` + - `Stop it first via the platform console, or pass --skip-precheck to attempt deletion anyway.`, + `Run \`bl deploy pause --deployed-model ${deployedModel}\` to pause it first, ` + + `or pass --skip-precheck to attempt deletion anyway.`, ExitCode.USAGE, ); } @@ -69,11 +68,8 @@ export default defineCommand({ if (settings.quiet) { emitBare(deployedModel); - } else if (format === "text") { - emitBare(`Deleted ${deployedModel}.`); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/deploy/get.ts b/packages/commands/src/commands/deploy/get.ts index 9c53e96..09bff85 100644 --- a/packages/commands/src/commands/deploy/get.ts +++ b/packages/commands/src/commands/deploy/get.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, getDeployment, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, getDeployment, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const GET_FLAGS = { deployedModel: { @@ -22,10 +22,9 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const deployedModel = flags.deployedModel; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "deploy.get", deployed_model: deployedModel }, format); + emitResult({ action: "deploy.get", deployed_model: deployedModel }, "json"); return; } @@ -33,7 +32,7 @@ export default defineCommand({ const deployment = response.output ?? response.data; if (!deployment) { - emitBare(`No data returned for ${deployedModel}`); + emitResult({ deployed_model: deployedModel, request_id: response.request_id }, "json"); return; } @@ -57,18 +56,6 @@ export default defineCommand({ if (deployment.gmt_create) item.created_at = deployment.gmt_create; if (deployment.gmt_modified) item.updated_at = deployment.gmt_modified; - if (format === "json") { - emitResult({ ...item, request_id: response.request_id }, format); - return; - } - - // text / quiet — fixed-width label column for alignment - const label = (key: string) => `${key}:`.padEnd(18); - for (const [key, value] of Object.entries(item)) { - if (value === "" || value === undefined) continue; - const display = typeof value === "string" ? value : JSON.stringify(value); - emitBare(`${label(key)}${display}`); - } - emitRequestId(response.request_id, settings.quiet); + emitResult({ ...item, request_id: response.request_id }, "json"); }, }); diff --git a/packages/commands/src/commands/deploy/list.ts b/packages/commands/src/commands/deploy/list.ts index e7c2917..944038a 100644 --- a/packages/commands/src/commands/deploy/list.ts +++ b/packages/commands/src/commands/deploy/list.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - listDeployments, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime"; +import { defineCommand, listDeployments, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const LIST_FLAGS = { page: { type: "number", valueHint: "", description: "Page number (default: 1)" }, @@ -28,13 +23,12 @@ export default defineCommand({ exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); const status = flags.status || undefined; if (settings.dryRun) { emitResult( { action: "deploy.list", page: flags.page, page_size: flags.pageSize, status }, - format, + "json", ); return; } @@ -57,27 +51,6 @@ export default defineCommand({ created_at: item.gmt_create ?? "", })); - if (format === "json") { - emitResult({ items, total, request_id: response.request_id }, format); - return; - } - - // text / quiet - if (items.length === 0) { - emitBare("No deployments found."); - return; - } - const headers = ["DEPLOYED_MODEL", "MODEL_NAME", "STATUS", "PLAN", "CAPACITY", "CREATED_AT"]; - const rows = items.map((item) => [ - item.deployed_model, - item.model_name, - item.status, - item.plan, - item.capacity, - item.created_at, - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - if (total !== undefined) emitBare(`\nTotal: ${total}`); - emitRequestId(response.request_id, settings.quiet); + emitResult({ items, total, request_id: response.request_id }, "json"); }, }); diff --git a/packages/commands/src/commands/deploy/models.ts b/packages/commands/src/commands/deploy/models.ts index d1ea972..b2c81d4 100644 --- a/packages/commands/src/commands/deploy/models.ts +++ b/packages/commands/src/commands/deploy/models.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - listDeployableModels, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime"; +import { defineCommand, listDeployableModels, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const MODELS_FLAGS = { page: { type: "number", valueHint: "", description: "Page number (default: 1)" }, @@ -39,7 +34,6 @@ export default defineCommand({ ], async run(ctx) { const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); // Default version to v1.0 — without it, the API returns the legacy catalog // (only old fine-tune outputs). Pass --catalog-version "" to opt out. const version = flags.catalogVersion === "" ? undefined : (flags.catalogVersion ?? "v1.0"); @@ -54,7 +48,7 @@ export default defineCommand({ version, model_source: modelSource, }, - format, + "json", ); return; } @@ -72,102 +66,55 @@ export default defineCommand({ // Two response shapes: // - custom (fine-tuned): top-level supported_plans: string[] // - base (catalog): plans: [{plan, templates?, cu_specs?}] - // For json: surface the deployment-relevant fields preserved as a tree, so + // Surface the deployment-relevant fields preserved as a tree, so // downstream tooling can drive `bl deploy create --deploy-spec <…>` - // without a second round-trip. For text: keep the compact one-line summary. - if (format === "json") { - const items = models.map((model) => { - const out: Record = { - model_name: model.model_name ?? "", - }; - if (model.base_model) out.base_model = model.base_model; - if (model.model_source) out.model_source = model.model_source; - if (model.supported_plans && model.supported_plans.length > 0) { - out.supported_plans = model.supported_plans; - } - if (model.plans && model.plans.length > 0) { - out.plans = model.plans.map((plan) => { - const planEntry: Record = { plan: plan.plan ?? "" }; - if (plan.cu_specs && plan.cu_specs.length > 0) { - planEntry.cu_specs = plan.cu_specs; - } - if (plan.templates && plan.templates.length > 0) { - // Pull the top 6 fields most useful for `bl deploy create`. - // Drop noisy/redundant: template_source, template_type, - // template_version, deploy_spec (typically == template_id). - planEntry.templates = plan.templates.map((template) => { - const tpl: Record = {}; - if (template.template_id) tpl.template_id = template.template_id; - if (template.template_name) tpl.template_name = template.template_name; - if (template.charge_type) tpl.charge_type = template.charge_type; - // Flatten roles.unified for the common COUPLED case. - const unified = template.roles?.unified; - if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec; - if (unified?.capacity_unit_per_instance !== undefined) - tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance; - // Preserve split-role configs (SEPERATED) as-is so callers - // can still drive prefill/decode sizing. - if (template.roles?.prefill || template.roles?.decode) { - tpl.roles = { - prefill: template.roles?.prefill, - decode: template.roles?.decode, - }; - } - if (template.template_desc) tpl.template_desc = template.template_desc; - return tpl; - }); - } - return planEntry; - }); - } - return out; - }); - emitResult({ items, total, request_id: response.request_id }, format); - return; - } - - // text / quiet — keep the compact single-line summary table. - const textItems = models.map((model) => { - let plansSummary = ""; - if (model.supported_plans && model.supported_plans.length > 0) { - plansSummary = model.supported_plans.join(","); - } else if (model.plans && model.plans.length > 0) { - plansSummary = model.plans - .map((plan) => { - const planName = plan.plan ?? "?"; - if (plan.templates && plan.templates.length > 0) { - return `${planName}(${plan.templates.length}t)`; - } - if (plan.cu_specs && plan.cu_specs.length > 0) { - return `${planName}(${plan.cu_specs.join("/")})`; - } - return planName; - }) - .join(","); - } else { - plansSummary = "-"; - } - return { + // without a second round-trip. + const items = models.map((model) => { + const out: Record = { model_name: model.model_name ?? "", - base_model: model.base_model ?? "", - source: model.model_source ?? "", - plans: plansSummary, }; + if (model.base_model) out.base_model = model.base_model; + if (model.model_source) out.model_source = model.model_source; + if (model.supported_plans && model.supported_plans.length > 0) { + out.supported_plans = model.supported_plans; + } + if (model.plans && model.plans.length > 0) { + out.plans = model.plans.map((plan) => { + const planEntry: Record = { plan: plan.plan ?? "" }; + if (plan.cu_specs && plan.cu_specs.length > 0) { + planEntry.cu_specs = plan.cu_specs; + } + if (plan.templates && plan.templates.length > 0) { + // Pull the top 6 fields most useful for `bl deploy create`. + // Drop noisy/redundant: template_source, template_type, + // template_version, deploy_spec (typically == template_id). + planEntry.templates = plan.templates.map((template) => { + const tpl: Record = {}; + if (template.template_id) tpl.template_id = template.template_id; + if (template.template_name) tpl.template_name = template.template_name; + if (template.charge_type) tpl.charge_type = template.charge_type; + // Flatten roles.unified for the common COUPLED case. + const unified = template.roles?.unified; + if (unified?.model_unit_spec) tpl.model_unit_spec = unified.model_unit_spec; + if (unified?.capacity_unit_per_instance !== undefined) + tpl.capacity_unit_per_instance = unified.capacity_unit_per_instance; + // Preserve split-role configs (SEPERATED) as-is so callers + // can still drive prefill/decode sizing. + if (template.roles?.prefill || template.roles?.decode) { + tpl.roles = { + prefill: template.roles?.prefill, + decode: template.roles?.decode, + }; + } + if (template.template_desc) tpl.template_desc = template.template_desc; + return tpl; + }); + } + return planEntry; + }); + } + return out; }); - - if (textItems.length === 0) { - emitBare("No deployable models found."); - return; - } - const headers = ["MODEL_NAME", "BASE_MODEL", "SOURCE", "PLANS"]; - const rows = textItems.map((item) => [ - item.model_name, - item.base_model, - item.source, - item.plans, - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - if (total !== undefined) emitBare(`\nTotal: ${total}`); - emitRequestId(response.request_id, settings.quiet); + emitResult({ items, total, request_id: response.request_id }, "json"); }, }); diff --git a/packages/commands/src/commands/deploy/pause.ts b/packages/commands/src/commands/deploy/pause.ts new file mode 100644 index 0000000..23d36c6 --- /dev/null +++ b/packages/commands/src/commands/deploy/pause.ts @@ -0,0 +1,85 @@ +import { + defineCommand, + stopModelService, + listIndependentDeployedModels, + findDeploymentEntry, + BailianError, + ExitCode, + type FlagsDef, +} from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; + +const PAUSE_FLAGS = { + deployedModel: { + type: "string", + valueHint: "", + description: "Deployed model identifier (required)", + required: true, + }, + skipPrecheck: { + type: "switch", + description: "Skip the local RUNNING/PENDING status precheck", + }, +} satisfies FlagsDef; + +/** + * `bl deploy pause` — pause a running deployment. + * + * Takes the model service offline so it no longer serves inference requests. + * For mu/ptu plans, billing stops while paused. + * Precheck: status must be RUNNING or PENDING. + */ +export default defineCommand({ + description: "Pause a running model deployment (stops billing for mu/ptu)", + auth: "console", + usageArgs: "--deployed-model [--skip-precheck]", + flags: PAUSE_FLAGS, + exampleArgs: [ + "--deployed-model dep-...", + "--deployed-model dep-... --skip-precheck", + "--deployed-model dep-... --dry-run", + ], + notes: [ + "While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove.", + "Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass.", + ], + async run(ctx) { + const { settings, flags } = ctx; + const deployedModel = flags.deployedModel; + + if (settings.dryRun) { + emitResult({ action: "deploy.pause", deployed_model: deployedModel }, "json"); + return; + } + + // Precheck: verify the deployment is in a pausable state. + if (!flags.skipPrecheck) { + try { + const entries = await listIndependentDeployedModels(ctx.client); + const entry = findDeploymentEntry(entries, deployedModel); + if (entry) { + const status = (entry.status ?? "").toUpperCase(); + if (status && status !== "RUNNING" && status !== "PENDING") { + throw new BailianError( + `Deployment ${deployedModel} is ${status}. Only RUNNING / PENDING deployments can be paused. ` + + `Pass --skip-precheck to attempt the pause anyway.`, + ExitCode.USAGE, + ); + } + } + // If entry not found in list, proceed — the server will surface the real error. + } catch (error) { + if (error instanceof BailianError) throw error; + // If the list call itself failed, proceed and let the API call surface the error. + } + } + + const response = await stopModelService(ctx.client, deployedModel); + + if (settings.quiet) { + emitBare(deployedModel); + } else { + emitResult({ deployed_model: deployedModel, action: "pause", ...response }, "json"); + } + }, +}); diff --git a/packages/commands/src/commands/deploy/resume.ts b/packages/commands/src/commands/deploy/resume.ts new file mode 100644 index 0000000..7e0cb28 --- /dev/null +++ b/packages/commands/src/commands/deploy/resume.ts @@ -0,0 +1,84 @@ +import { + defineCommand, + startModelService, + listIndependentDeployedModels, + findDeploymentEntry, + BailianError, + ExitCode, + type FlagsDef, +} from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; + +const RESUME_FLAGS = { + deployedModel: { + type: "string", + valueHint: "", + description: "Deployed model identifier (required)", + required: true, + }, + skipPrecheck: { + type: "switch", + description: "Skip the local STOPPED status precheck", + }, +} satisfies FlagsDef; + +/** + * `bl deploy resume` — resume a paused deployment. + * + * Brings the model service back online so it can serve inference requests. + * Precheck: status must be STOPPED. + */ +export default defineCommand({ + description: "Resume a paused model deployment (brings service back online)", + auth: "console", + usageArgs: "--deployed-model [--skip-precheck]", + flags: RESUME_FLAGS, + exampleArgs: [ + "--deployed-model dep-...", + "--deployed-model dep-... --skip-precheck", + "--deployed-model dep-... --dry-run", + ], + notes: [ + "Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass.", + "For mu/ptu plans, billing resumes once the service is back online.", + ], + async run(ctx) { + const { settings, flags } = ctx; + const deployedModel = flags.deployedModel; + + if (settings.dryRun) { + emitResult({ action: "deploy.resume", deployed_model: deployedModel }, "json"); + return; + } + + // Precheck: verify the deployment is in a resumable state. + if (!flags.skipPrecheck) { + try { + const entries = await listIndependentDeployedModels(ctx.client); + const entry = findDeploymentEntry(entries, deployedModel); + if (entry) { + const status = (entry.status ?? "").toUpperCase(); + if (status && status !== "STOPPED") { + throw new BailianError( + `Deployment ${deployedModel} is ${status}. Only STOPPED deployments can be resumed. ` + + `Pass --skip-precheck to attempt the resume anyway.`, + ExitCode.USAGE, + ); + } + } + // If entry not found in list, proceed — the server will surface the real error. + } catch (error) { + if (error instanceof BailianError) throw error; + // If the list call itself failed, proceed and let the API call surface the error. + } + } + + const response = await startModelService(ctx.client, deployedModel); + + if (settings.quiet) { + emitBare(deployedModel); + } else { + emitResult({ deployed_model: deployedModel, action: "resume", ...response }, "json"); + } + }, +}); diff --git a/packages/commands/src/commands/deploy/scale.ts b/packages/commands/src/commands/deploy/scale.ts index 805f9c5..c6400a4 100644 --- a/packages/commands/src/commands/deploy/scale.ts +++ b/packages/commands/src/commands/deploy/scale.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - scaleDeployment, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, scaleDeployment, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const SCALE_FLAGS = { deployedModel: { @@ -52,7 +47,6 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const deployedModel = flags.deployedModel; - const format = detectOutputFormat(settings.output); const body: Record = {}; if (flags.capacity !== undefined) body.capacity = flags.capacity; @@ -60,21 +54,16 @@ export default defineCommand({ if (flags.outputTpm !== undefined) body.output_tpm = flags.outputTpm; if (settings.dryRun) { - emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, format); + emitResult({ action: "deploy.scale", deployed_model: deployedModel, body }, "json"); return; } const response = await scaleDeployment(ctx.client, deployedModel, body); - const deployment = response.output ?? response.data; if (settings.quiet) { emitBare(deployedModel); - } else if (format === "text") { - const cap = deployment?.capacity !== undefined ? ` (capacity=${deployment.capacity})` : ""; - emitBare(`Scaled ${deployedModel}${cap}.`); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/deploy/update.ts b/packages/commands/src/commands/deploy/update.ts index 0bbb29d..6c3ab89 100644 --- a/packages/commands/src/commands/deploy/update.ts +++ b/packages/commands/src/commands/deploy/update.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - updateDeployment, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, updateDeployment, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const UPDATE_FLAGS = { deployedModel: { @@ -48,31 +43,22 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const deployedModel = flags.deployedModel; - const format = detectOutputFormat(settings.output); const body: Record = {}; if (flags.rpmLimit !== undefined) body.rpm_limit = flags.rpmLimit; if (flags.tpmLimit !== undefined) body.tpm_limit = flags.tpmLimit; if (settings.dryRun) { - emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, format); + emitResult({ action: "deploy.update", deployed_model: deployedModel, body }, "json"); return; } const response = await updateDeployment(ctx.client, deployedModel, body); - const deployment = response.output ?? response.data; if (settings.quiet) { emitBare(deployedModel); - } else if (format === "text") { - const parts: string[] = []; - if (deployment?.rpm_limit !== undefined) parts.push(`rpm_limit=${deployment.rpm_limit}`); - if (deployment?.tpm_limit !== undefined) parts.push(`tpm_limit=${deployment.tpm_limit}`); - const summary = parts.length ? ` (${parts.join(", ")})` : ""; - emitBare(`Updated ${deployedModel}${summary}.`); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/finetune/cancel.ts b/packages/commands/src/commands/finetune/cancel.ts index 789a3b0..966ce3e 100644 --- a/packages/commands/src/commands/finetune/cancel.ts +++ b/packages/commands/src/commands/finetune/cancel.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, cancelFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, cancelFineTune, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const CANCEL_FLAGS = { jobId: { @@ -23,24 +23,18 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const jobId = flags.jobId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "finetune.cancel", job_id: jobId }, format); + emitResult({ action: "finetune.cancel", job_id: jobId }, "json"); return; } const response = await cancelFineTune(ctx.client, jobId); - const job = response.output ?? response.data; if (settings.quiet) { emitBare(jobId); - } else if (format === "text") { - const status = job?.status ? ` (status=${job.status})` : ""; - emitBare(`Cancelled ${jobId}${status}.`); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/finetune/capability.ts b/packages/commands/src/commands/finetune/capability.ts index 1bf7203..e9091a9 100644 --- a/packages/commands/src/commands/finetune/capability.ts +++ b/packages/commands/src/commands/finetune/capability.ts @@ -1,6 +1,5 @@ import { defineCommand, - detectOutputFormat, fetchModelList, fetchModelCapability, listSupportedTrainingTypes, @@ -43,17 +42,6 @@ async function fetchAllFoundationModels(settings: Settings): Promise = { - full: "full-parameter", - lora: "LoRA", -}; - -function describeTrainingType(value: string): string { - if (!isTrainingTypeCli(value)) return value; - const { method, variant } = trainingTypeMethodVariant(value); - return `${VARIANT_LABEL[variant] ?? variant} ${method.toUpperCase()}`; -} - const CAPABILITY_FLAGS = { model: { type: "string", @@ -95,7 +83,6 @@ export default defineCommand({ const { settings, flags } = ctx; const model = flags.model || undefined; const trainingType = flags.trainingType || undefined; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { emitResult( @@ -104,7 +91,7 @@ export default defineCommand({ model, training_type: trainingType, }, - format, + "json", ); return; } @@ -113,7 +100,7 @@ export default defineCommand({ if (model) { const capability = await fetchModelCapability(settings, model); if (!capability) { - emitBare(`No foundation model found matching "${model}".`); + emitResult({ model, error: `No foundation model found matching "${model}".` }, "json"); return; } const supported = listSupportedTrainingTypes(capability); @@ -121,23 +108,15 @@ export default defineCommand({ for (const value of supported) emitBare(value); return; } - if (format !== "text") { - emitResult( - { - model: capability.model ?? model, - supported, - supports: capability.supports, - trainingTypes: capability.trainingTypes, - }, - format, - ); - return; - } - emitBare(`${capability.model ?? model}`); - emitBare(supported.length ? "Supported training types:" : "No supported training types."); - for (const value of supported) { - emitBare(` ${value.padEnd(10)} ${describeTrainingType(value)}`); - } + emitResult( + { + model: capability.model ?? model, + supported, + supports: capability.supports, + trainingTypes: capability.trainingTypes, + }, + "json", + ); return; } @@ -162,20 +141,15 @@ export default defineCommand({ for (const entry of matched) emitBare(entry.model); return; } - if (format !== "text") { - emitResult( - { - training_type: trainingType, - method, - variant, - count: matched.length, - models: matched, - }, - format, - ); - return; - } - emitBare(`Models supporting ${trainingType} (${method} / ${variant}): ${matched.length}`); - for (const entry of matched) emitBare(` ${entry.model}`); + emitResult( + { + training_type: trainingType, + method, + variant, + count: matched.length, + models: matched, + }, + "json", + ); }, }); diff --git a/packages/commands/src/commands/finetune/checkpoints.ts b/packages/commands/src/commands/finetune/checkpoints.ts index 7c05a36..769cf72 100644 --- a/packages/commands/src/commands/finetune/checkpoints.ts +++ b/packages/commands/src/commands/finetune/checkpoints.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - listCheckpoints, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime"; +import { defineCommand, listCheckpoints, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const CHECKPOINTS_FLAGS = { jobId: { @@ -15,6 +10,8 @@ const CHECKPOINTS_FLAGS = { }, } satisfies FlagsDef; +const EXPIRY_WARN_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours + export default defineCommand({ description: "List checkpoints produced by a fine-tune job", auth: "apiKey", @@ -22,16 +19,15 @@ export default defineCommand({ flags: CHECKPOINTS_FLAGS, exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"], notes: [ - "Use the returned `checkpoint` value with `finetune export` to publish", - "a deployable model.", + "`model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model`.", + "Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry.", ], async run(ctx) { const { settings, flags } = ctx; const jobId = flags.jobId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "finetune.checkpoints", job_id: jobId }, format); + emitResult({ action: "finetune.checkpoints", job_id: jobId }, "json"); return; } @@ -44,22 +40,26 @@ export default defineCommand({ checkpoint: item.checkpoint ?? item.checkpoint_id ?? "", step: item.step !== undefined ? String(item.step) : "", status: item.status ?? "", + model_name: item.model_name ?? "", + expire_time: item.expire_time ?? "", })); - if (format === "json") { - emitResult({ items, total, request_id: response.request_id }, format); - return; - } + emitResult({ items, total, request_id: response.request_id }, "json"); - // text / quiet - if (items.length === 0) { - emitBare("No checkpoints found."); - return; + // Near-expiry warning: check if any non-expired checkpoint is within 72h of expiry. + const now = Date.now(); + const expiringSoon = items.filter((item) => { + if (!item.expire_time) return false; + const deadline = new Date(item.expire_time).getTime(); + if (Number.isNaN(deadline)) return false; + const remaining = deadline - now; + return remaining > 0 && remaining < EXPIRY_WARN_THRESHOLD_MS; + }); + if (expiringSoon.length > 0) { + process.stderr.write( + `\n[warning] ${expiringSoon.length} checkpoint(s) will expire within 72 hours. ` + + "Export or deploy before expiry to avoid losing the model artifact.\n", + ); } - const headers = ["CHECKPOINT", "STEP", "STATUS"]; - const rows = items.map((i) => [i.checkpoint, i.step, i.status]); - for (const line of formatTable(headers, rows)) emitBare(line); - emitBare(`\nTotal: ${total}`); - emitRequestId(response.request_id, settings.quiet); }, }); diff --git a/packages/commands/src/commands/finetune/create.ts b/packages/commands/src/commands/finetune/create.ts index 6fbb54f..f9bbaf0 100644 --- a/packages/commands/src/commands/finetune/create.ts +++ b/packages/commands/src/commands/finetune/create.ts @@ -1,6 +1,5 @@ import { defineCommand, - detectOutputFormat, createFineTune, getDataset, uploadDataset, @@ -27,7 +26,7 @@ import { } from "bailian-cli-core"; import { existsSync, statSync } from "fs"; import { basename } from "path"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; /** * A `--datasets` / `--validations` token is treated as a local file to upload @@ -606,8 +605,6 @@ async function runCreate( if (modelName) body.model_name = modelName; if (suffix) body.finetuned_output_suffix = suffix; - const format = detectOutputFormat(settings.output); - if (settings.dryRun) { const pending = [ ...training.localPaths.map((path) => ({ field: "datasets", path })), @@ -617,7 +614,7 @@ async function runCreate( pending.length > 0 ? { action: "finetune.create", body, pending_uploads: pending } : { action: "finetune.create", body }, - format, + "json", ); return; } @@ -627,16 +624,8 @@ async function runCreate( if (settings.quiet) { if (job?.job_id) emitBare(job.job_id); - } else if (format === "text") { - if (job?.job_id) { - emitBare(`Created fine-tune job: ${job.job_id}`); - if (job.status) emitBare(`Status: ${job.status}`); - emitRequestId(response.request_id, settings.quiet); - } else { - emitResult(response, format); - } } else { - emitResult(response, format); + emitResult(response, "json"); } } diff --git a/packages/commands/src/commands/finetune/delete.ts b/packages/commands/src/commands/finetune/delete.ts index 102fda6..f18a13f 100644 --- a/packages/commands/src/commands/finetune/delete.ts +++ b/packages/commands/src/commands/finetune/delete.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, deleteFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const DELETE_FLAGS = { jobId: { @@ -23,10 +23,9 @@ export default defineCommand({ async run(ctx) { const { settings, flags } = ctx; const jobId = flags.jobId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "finetune.delete", job_id: jobId }, format); + emitResult({ action: "finetune.delete", job_id: jobId }, "json"); return; } @@ -34,11 +33,8 @@ export default defineCommand({ if (settings.quiet) { emitBare(jobId); - } else if (format === "text") { - emitBare(`Deleted ${jobId}.`); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/finetune/export.ts b/packages/commands/src/commands/finetune/export.ts index 7e52de0..97bb0aa 100644 --- a/packages/commands/src/commands/finetune/export.ts +++ b/packages/commands/src/commands/finetune/export.ts @@ -1,10 +1,5 @@ -import { - defineCommand, - detectOutputFormat, - exportCheckpoint, - type FlagsDef, -} from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, exportCheckpoint, type FlagsDef } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const EXPORT_FLAGS = { jobId: { @@ -39,11 +34,10 @@ export default defineCommand({ "explicit export is the canonical path for non-best checkpoints.", ], async run(ctx) { - const { identity, settings, flags } = ctx; + const { settings, flags } = ctx; const jobId = flags.jobId; const checkpoint = flags.checkpoint; const modelName = flags.modelName; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { emitResult( @@ -53,7 +47,7 @@ export default defineCommand({ checkpoint, model_name: modelName, }, - format, + "json", ); return; } @@ -64,14 +58,8 @@ export default defineCommand({ if (settings.quiet) { emitBare(exported); - } else if (format === "text") { - emitBare(`Exported ${jobId} / ${checkpoint} → model_name=${exported}`); - emitBare( - `Next: ${identity.binName} deploy text create --model ${exported} --name `, - ); - emitRequestId(response.request_id, settings.quiet); } else { - emitResult(response, format); + emitResult(response, "json"); } }, }); diff --git a/packages/commands/src/commands/finetune/get.ts b/packages/commands/src/commands/finetune/get.ts index 0ba0114..05d96fb 100644 --- a/packages/commands/src/commands/finetune/get.ts +++ b/packages/commands/src/commands/finetune/get.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, getFineTune, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const GET_FLAGS = { jobId: { @@ -17,12 +17,11 @@ export default defineCommand({ flags: GET_FLAGS, exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"], async run(ctx) { - const { identity, settings, flags } = ctx; + const { settings, flags } = ctx; const jobId = flags.jobId; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { - emitResult({ action: "finetune.get", job_id: jobId }, format); + emitResult({ action: "finetune.get", job_id: jobId }, "json"); return; } @@ -30,16 +29,20 @@ export default defineCommand({ const job = response.output ?? response.data; if (!job) { - emitBare(`No data returned for ${jobId}`); + emitResult({ job_id: jobId, error: "No data returned" }, "json"); return; } - const hp = job.hyper_parameters; + const hyperParameters = job.hyper_parameters; const hyperParts: string[] = []; - if (hp?.n_epochs !== undefined) hyperParts.push(`n_epochs=${hp.n_epochs}`); - if (hp?.batch_size !== undefined) hyperParts.push(`batch_size=${hp.batch_size}`); - if (hp?.learning_rate !== undefined) hyperParts.push(`learning_rate=${hp.learning_rate}`); - if (hp?.max_length !== undefined) hyperParts.push(`max_length=${hp.max_length}`); + if (hyperParameters?.n_epochs !== undefined) + hyperParts.push(`n_epochs=${hyperParameters.n_epochs}`); + if (hyperParameters?.batch_size !== undefined) + hyperParts.push(`batch_size=${hyperParameters.batch_size}`); + if (hyperParameters?.learning_rate !== undefined) + hyperParts.push(`learning_rate=${hyperParameters.learning_rate}`); + if (hyperParameters?.max_length !== undefined) + hyperParts.push(`max_length=${hyperParameters.max_length}`); const item = { job_id: job.job_id ?? jobId, @@ -53,29 +56,10 @@ export default defineCommand({ model_name: job.model_name ?? "", created_at: job.create_time ?? job.gmt_create ?? "", updated_at: job.end_time ?? job.gmt_modified ?? "", + usage: typeof job.usage === "number" ? String(job.usage) : "", + charge_type: typeof job.charge_type === "string" ? job.charge_type : "", }; - if (format === "json") { - emitResult({ ...item, request_id: response.request_id }, format); - return; - } - - // text / quiet - emitBare(`job_id: ${item.job_id}`); - if (item.base_model) emitBare(`base_model: ${item.base_model}`); - if (item.status) emitBare(`status: ${item.status}`); - if (item.training_type) emitBare(`training_type: ${item.training_type}`); - if (item.training_files.length) emitBare(`training_files: ${item.training_files.join(", ")}`); - if (item.validation_files.length) - emitBare(`validation_files: ${item.validation_files.join(", ")}`); - if (item.hyper_params) emitBare(`hyper_params: ${item.hyper_params}`); - if (item.output_model) - emitBare( - `output_model: ${item.output_model} (→ ${identity.binName} deploy text create --model)`, - ); - if (item.model_name) emitBare(`model_name: ${item.model_name}`); - if (item.created_at) emitBare(`created_at: ${item.created_at}`); - if (item.updated_at) emitBare(`updated_at: ${item.updated_at}`); - emitRequestId(response.request_id, settings.quiet); + emitResult({ ...item, request_id: response.request_id }, "json"); }, }); diff --git a/packages/commands/src/commands/finetune/list.ts b/packages/commands/src/commands/finetune/list.ts index ca4eddd..529a5fd 100644 --- a/packages/commands/src/commands/finetune/list.ts +++ b/packages/commands/src/commands/finetune/list.ts @@ -1,5 +1,5 @@ -import { defineCommand, detectOutputFormat, listFineTunes, type FlagsDef } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId, formatTable } from "bailian-cli-runtime"; +import { defineCommand, listFineTunes, type FlagsDef } from "bailian-cli-core"; +import { emitResult } from "bailian-cli-runtime"; const LIST_FLAGS = { page: { type: "number", valueHint: "", description: "Page number (default: 1)" }, @@ -22,14 +22,13 @@ export default defineCommand({ flags: LIST_FLAGS, exampleArgs: ["", "--status RUNNING", "--page-size 20 --output json"], async run(ctx) { - const { identity, settings, flags } = ctx; - const format = detectOutputFormat(settings.output); + const { settings, flags } = ctx; const pageNo = flags.page; const pageSize = flags.pageSize; const status = flags.status || undefined; if (settings.dryRun) { - emitResult({ action: "finetune.list", page: pageNo, page_size: pageSize, status }, format); + emitResult({ action: "finetune.list", page: pageNo, page_size: pageSize, status }, "json"); return; } @@ -38,46 +37,15 @@ export default defineCommand({ const jobs = payload?.jobs ?? []; const total = payload?.total; - const items = jobs.map((item) => ({ - job_id: item.job_id ?? "", - base_model: item.model ?? "", - status: item.status ?? "", - training_type: item.training_type ?? "", - output_model: item.finetuned_output ?? "", - created_at: item.create_time ?? item.gmt_create ?? "", + const items = jobs.map((job) => ({ + job_id: job.job_id ?? "", + base_model: job.model ?? "", + status: job.status ?? "", + training_type: job.training_type ?? "", + output_model: job.finetuned_output ?? "", + created_at: job.create_time ?? job.gmt_create ?? "", })); - if (format === "json") { - emitResult({ items, total, request_id: response.request_id }, format); - return; - } - - // text / quiet - if (items.length === 0) { - emitBare("No fine-tune jobs found."); - return; - } - const headers = [ - "JOB_ID", - "BASE_MODEL", - "STATUS", - "TRAINING_TYPE", - "OUTPUT_MODEL", - "CREATED_AT", - ]; - const rows = items.map((i) => [ - i.job_id, - i.base_model, - i.status, - i.training_type, - i.output_model, - i.created_at, - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - if (total !== undefined) emitBare(`\nTotal: ${total}`); - emitBare( - `Tip: OUTPUT_MODEL is the input for \`${identity.binName} deploy text create --model\``, - ); - emitRequestId(response.request_id, settings.quiet); + emitResult({ items, total, request_id: response.request_id }, "json"); }, }); diff --git a/packages/commands/src/commands/finetune/logs.ts b/packages/commands/src/commands/finetune/logs.ts index bd0802e..320c07c 100644 --- a/packages/commands/src/commands/finetune/logs.ts +++ b/packages/commands/src/commands/finetune/logs.ts @@ -1,25 +1,24 @@ import { defineCommand, - detectOutputFormat, getFineTuneLogs, type Client, type FineTuneLogEntry, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; /** - * Render a single log entry as a single line (mirrors the flatten logic used - * for non-search text output: prefer common fields, fall back to JSON). + * Render a single log entry as a single line (used for search matching: + * prefer common fields, fall back to JSON). */ function renderEntry(entry: FineTuneLogEntry | string): string { if (typeof entry === "string") return entry; const record = entry as Record; - const ts = (record.timestamp ?? record.time ?? record.create_time ?? "") as string; + const timestamp = (record.timestamp ?? record.time ?? record.create_time ?? "") as string; const level = (record.level ?? "") as string; - const msg = (record.message ?? record.msg ?? record.log ?? "") as string; - if (msg || ts || level) { - return [ts, level, msg].filter(Boolean).join("\t"); + const message = (record.message ?? record.msg ?? record.log ?? "") as string; + if (message || timestamp || level) { + return [timestamp, level, message].filter(Boolean).join("\t"); } return JSON.stringify(entry); } @@ -48,16 +47,16 @@ async function fetchAllLogs( let total = 0; // Hard cap to avoid an unbounded loop if the server misreports `total`. const maxPages = 200; - for (let i = 0; i < maxPages; i++) { + for (let page = 0; page < maxPages; page++) { const response = await getFineTuneLogs(client, jobId, { pageNo, pageSize }); const payload = response.output ?? response.data; - const page = payload?.logs ?? []; + const logs = payload?.logs ?? []; total = payload?.total ?? total; - if (page.length === 0) break; - entries.push(...page); + if (logs.length === 0) break; + entries.push(...logs); // Stop once we've collected everything the server claims exists. if (total && entries.length >= total) break; - if (page.length < pageSize) break; + if (logs.length < pageSize) break; pageNo++; } return { entries, total }; @@ -110,7 +109,6 @@ export default defineCommand({ const pageSize = flags.pageSize; const search = flags.search || undefined; const tail = flags.tail; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { emitResult( @@ -122,7 +120,7 @@ export default defineCommand({ search, tail, }, - format, + "json", ); return; } @@ -147,18 +145,6 @@ export default defineCommand({ const result = tailApplied !== undefined ? scanned.slice(scanned.length - tailApplied) : scanned; - if (settings.quiet || format === "text") { - if (result.length === 0) { - emitBare(search ? `No logs matched "${search}".` : "No logs returned."); - return; - } - for (const entry of result) emitBare(renderEntry(entry)); - const parts: string[] = [`${result.length} shown`]; - if (matched !== undefined) parts.push(`matched ${matched}`); - parts.push(`of ${entries.length}` + (total ? ` (total ${total})` : "")); - emitBare(`\n${parts.join(", ")}`); - return; - } emitResult( { ...(matched !== undefined ? { matched } : {}), @@ -168,28 +154,13 @@ export default defineCommand({ ...(tailApplied !== undefined ? { tail: tailApplied } : {}), logs: result, }, - format, + "json", ); return; } // Default: single page, verbatim response. const response = await getFineTuneLogs(ctx.client, jobId, { pageNo, pageSize }); - const payload = response.output ?? response.data; - const logs = payload?.logs ?? []; - - if (settings.quiet || format === "text") { - if (logs.length === 0) { - emitBare("No logs returned."); - return; - } - for (const entry of logs) { - emitBare(renderEntry(entry)); - } - if (payload?.total !== undefined) emitBare(`\nTotal: ${payload.total}`); - emitRequestId(response.request_id, settings.quiet); - } else { - emitResult(response, format); - } + emitResult(response, "json"); }, }); diff --git a/packages/commands/src/commands/finetune/watch.ts b/packages/commands/src/commands/finetune/watch.ts index c02b207..8baf738 100644 --- a/packages/commands/src/commands/finetune/watch.ts +++ b/packages/commands/src/commands/finetune/watch.ts @@ -1,12 +1,11 @@ import { defineCommand, - detectOutputFormat, getFineTune, BailianError, ExitCode, type FlagsDef, } from "bailian-cli-core"; -import { emitResult, emitBare, emitRequestId } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const DEFAULT_INTERVAL_SEC = 10; const MIN_INTERVAL_SEC = 1; @@ -103,7 +102,6 @@ export default defineCommand({ const follow = flags.follow; const intervalSec = Math.max(MIN_INTERVAL_SEC, flags.interval ?? DEFAULT_INTERVAL_SEC); const pollTimeoutSec = flags.pollTimeout; - const format = detectOutputFormat(settings.output); if (settings.dryRun) { emitResult( @@ -114,7 +112,7 @@ export default defineCommand({ interval: intervalSec, timeout: pollTimeoutSec, }, - format, + "json", ); return; } @@ -132,15 +130,10 @@ export default defineCommand({ if (settings.quiet) { // Just the status word — ideal for `status=$(... finetune watch ... --quiet)`. emitBare(status || "UNKNOWN"); - } else if (format === "text") { - emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`); - if (status === "SUCCEEDED") emitBare(`✓ ${jobId} ${status}`); - emitRequestId(response.request_id, settings.quiet); } else { - // json: a compact, purpose-built status probe. emitResult( { job_id: jobId, status: status || "UNKNOWN", terminal, request_id: response.request_id }, - format, + "json", ); } @@ -168,18 +161,17 @@ export default defineCommand({ const job = response.output ?? response.data; const status = String(job?.status ?? "").toUpperCase(); - if (format === "text" && !settings.quiet && status !== lastStatus) { - emitBare(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}`); + if (!settings.quiet && status !== lastStatus) { + process.stderr.write(`${nowStamp()} ${jobId} ${status || "UNKNOWN"}\n`); lastStatus = status; } if (TERMINAL_STATUSES.has(status)) { const elapsed = Date.now() - startedAt; - if (format !== "text" || settings.quiet) { - emitResult(response, format); - } else if (status === "SUCCEEDED") { - emitBare(`\n✓ ${jobId} ${status} (elapsed ${formatElapsed(elapsed)})`); - emitRequestId(response.request_id, settings.quiet); + if (settings.quiet) { + emitBare(status || "UNKNOWN"); + } else { + emitResult(response, "json"); } if (status !== "SUCCEEDED") { throw new BailianError( @@ -205,7 +197,7 @@ export default defineCommand({ // Any other error (including the BailianError thrown above) propagates to // the central handler. if (controller.signal.aborted) { - emitBare("\nInterrupted."); + process.stderr.write("\nInterrupted.\n"); return; } throw error; diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index cba8b32..63a1316 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -87,6 +87,8 @@ export { default as deployModels } from "./commands/deploy/models.ts"; export { default as deployScale } from "./commands/deploy/scale.ts"; export { default as deployUpdate } from "./commands/deploy/update.ts"; export { default as deployDelete } from "./commands/deploy/delete.ts"; +export { default as deployPause } from "./commands/deploy/pause.ts"; +export { default as deployResume } from "./commands/deploy/resume.ts"; export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.ts"; export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts"; export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts"; diff --git a/packages/core/src/deploy/index.ts b/packages/core/src/deploy/index.ts index e0515c1..ce3909a 100644 --- a/packages/core/src/deploy/index.ts +++ b/packages/core/src/deploy/index.ts @@ -2,3 +2,4 @@ export * from "./api.ts"; export * from "./types.ts"; export * from "./constants.ts"; export * from "./plans.ts"; +export * from "./lifecycle.ts"; diff --git a/packages/core/src/deploy/lifecycle.ts b/packages/core/src/deploy/lifecycle.ts new file mode 100644 index 0000000..9f647d5 --- /dev/null +++ b/packages/core/src/deploy/lifecycle.ts @@ -0,0 +1,99 @@ +/** + * Deployment lifecycle operations via the **console gateway**. + * + * Unlike the DashScope REST endpoints in `api.ts`, start/stop/list-independent + * are console-domain APIs (`zeldaEasy.broadscope-platform.modelInstance.*`). + * Commands using these must declare `auth: "console"`. + */ +import type { Client } from "../client/client.ts"; +import { unwrapResponse } from "../console/models.ts"; + +// --------------------------------------------------------------------------- +// API names +// --------------------------------------------------------------------------- + +export const DEPLOY_START_API = "zeldaEasy.broadscope-platform.modelInstance.startModelService"; +export const DEPLOY_STOP_API = "zeldaEasy.broadscope-platform.modelInstance.stopModelService"; +export const DEPLOY_LIST_INDEPENDENT_API = + "zeldaEasy.broadscope-platform.modelInstance.listIndependentDeployedModel"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface ModelServiceEntry { + modelServiceId?: string; + deployedModel?: string; + deployed_model?: string; + status?: string; + modelName?: string; + model_name?: string; + plan?: string; + [key: string]: unknown; +} + +// --------------------------------------------------------------------------- +// API wrappers +// --------------------------------------------------------------------------- + +/** Start (bring online) a stopped deployment. */ +export async function startModelService( + client: Client, + modelServiceId: string, +): Promise> { + const raw = await client.console>(DEPLOY_START_API, { + input: { modelServiceId }, + }); + return unwrapResponse(raw); +} + +/** Stop (take offline) a running deployment. Stops billing for mu/ptu plans. */ +export async function stopModelService( + client: Client, + modelServiceId: string, +): Promise> { + const raw = await client.console>(DEPLOY_STOP_API, { + input: { modelServiceId }, + }); + return unwrapResponse(raw); +} + +/** + * List independently deployed models (console domain). + * Used for precheck status verification and ID mapping. + * Paginates internally to return all entries. + */ +export async function listIndependentDeployedModels(client: Client): Promise { + const allEntries: ModelServiceEntry[] = []; + let page = 1; + + while (true) { + const raw = await client.console>(DEPLOY_LIST_INDEPENDENT_API, { + input: { pageNo: page, pageSize: 50 }, + }); + const resp = unwrapResponse(raw); + const records = (resp.records ?? []) as ModelServiceEntry[]; + allEntries.push(...records); + const pageCount = (resp.pageCount as number) ?? 1; + if (page >= pageCount || records.length === 0) break; + page++; + } + + return allEntries; +} + +/** + * Find a deployment entry by its identifier in the console-domain list. + * Matches against `modelServiceId`, `deployedModel`, or `deployed_model`. + */ +export function findDeploymentEntry( + entries: ModelServiceEntry[], + deployedModel: string, +): ModelServiceEntry | undefined { + return entries.find( + (entry) => + entry.modelServiceId === deployedModel || + entry.deployedModel === deployedModel || + entry.deployed_model === deployedModel, + ); +} diff --git a/skills/bailian-finetune/reference/deploy.md b/skills/bailian-finetune/reference/deploy.md index a33d2c5..8d6c777 100644 --- a/skills/bailian-finetune/reference/deploy.md +++ b/skills/bailian-finetune/reference/deploy.md @@ -7,17 +7,19 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Description | -| ------------------------ | --------------------------------------------------------- | -| `bl deploy audio create` | Create an audio (TTS) model deployment | -| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) | -| `bl deploy get` | Get details of a single model deployment | -| `bl deploy image create` | Create an image generation model deployment | -| `bl deploy list` | List model deployments | -| `bl deploy models` | List models available for deployment | -| `bl deploy scale` | Scale a deployment's capacity | -| `bl deploy text create` | Create a text model deployment | -| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) | +| Command | Description | +| ------------------------ | ------------------------------------------------------------- | +| `bl deploy audio create` | Create an audio (TTS) model deployment | +| `bl deploy delete` | Delete a model deployment (must be STOPPED or FAILED) | +| `bl deploy get` | Get details of a single model deployment | +| `bl deploy image create` | Create an image generation model deployment | +| `bl deploy list` | List model deployments | +| `bl deploy models` | List models available for deployment | +| `bl deploy pause` | Pause a running model deployment (stops billing for mu/ptu) | +| `bl deploy resume` | Resume a paused model deployment (brings service back online) | +| `bl deploy scale` | Scale a deployment's capacity | +| `bl deploy text create` | Create a text model deployment | +| `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) | ## Command details @@ -263,6 +265,82 @@ bl deploy models --source custom --page-size 50 bl deploy models --catalog-version v1.0 --output json ``` +### `bl deploy pause` + +| Field | Value | +| --------------- | ----------------------------------------------------------- | +| **Name** | `deploy pause` | +| **Description** | Pause a running model deployment (stops billing for mu/ptu) | +| **Usage** | `bl deploy pause --deployed-model [--skip-precheck]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--deployed-model ` | string | yes | Deployed model identifier (required) | +| `--skip-precheck` | switch | no | Skip the local RUNNING/PENDING status precheck | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- While paused, billing ceases for mu/ptu plans. Use `deploy resume` to bring it back online or `deploy delete` to remove. +- Precheck verifies status is RUNNING/PENDING before issuing the pause; pass --skip-precheck to bypass. + +#### Examples + +```bash +bl deploy pause --deployed-model dep-... +``` + +```bash +bl deploy pause --deployed-model dep-... --skip-precheck +``` + +```bash +bl deploy pause --deployed-model dep-... --dry-run +``` + +### `bl deploy resume` + +| Field | Value | +| --------------- | ------------------------------------------------------------- | +| **Name** | `deploy resume` | +| **Description** | Resume a paused model deployment (brings service back online) | +| **Usage** | `bl deploy resume --deployed-model [--skip-precheck]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--deployed-model ` | string | yes | Deployed model identifier (required) | +| `--skip-precheck` | switch | no | Skip the local STOPPED status precheck | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Notes + +- Precheck verifies status is STOPPED before issuing the resume; pass --skip-precheck to bypass. +- For mu/ptu plans, billing resumes once the service is back online. + +#### Examples + +```bash +bl deploy resume --deployed-model dep-... +``` + +```bash +bl deploy resume --deployed-model dep-... --skip-precheck +``` + +```bash +bl deploy resume --deployed-model dep-... --dry-run +``` + ### `bl deploy scale` | Field | Value | diff --git a/skills/bailian-finetune/reference/finetune.md b/skills/bailian-finetune/reference/finetune.md index d269dfc..0b05ca0 100644 --- a/skills/bailian-finetune/reference/finetune.md +++ b/skills/bailian-finetune/reference/finetune.md @@ -166,8 +166,8 @@ bl finetune capability --training-type sft --quiet #### Notes -- Use the returned `checkpoint` value with `finetune export` to publish -- a deployable model. +- `model_name` (shown for SUCCEEDED checkpoints) is the direct input for `deploy create --model`. +- Checkpoints expire ~15 days after creation; `expire_time` shows the deadline. Export or deploy before expiry. #### Examples diff --git a/skills/bailian-finetune/reference/index.md b/skills/bailian-finetune/reference/index.md index 4877e27..07cf63f 100644 --- a/skills/bailian-finetune/reference/index.md +++ b/skills/bailian-finetune/reference/index.md @@ -22,6 +22,8 @@ Use this index for the skill-scoped quick index and global flags. | `bl deploy image create` | Create an image generation model deployment | [deploy.md](deploy.md) | | `bl deploy list` | List model deployments | [deploy.md](deploy.md) | | `bl deploy models` | List models available for deployment | [deploy.md](deploy.md) | +| `bl deploy pause` | Pause a running model deployment (stops billing for mu/ptu) | [deploy.md](deploy.md) | +| `bl deploy resume` | Resume a paused model deployment (brings service back online) | [deploy.md](deploy.md) | | `bl deploy scale` | Scale a deployment's capacity | [deploy.md](deploy.md) | | `bl deploy text create` | Create a text model deployment | [deploy.md](deploy.md) | | `bl deploy update` | Update a deployment's rate limits (rpm_limit / tpm_limit) | [deploy.md](deploy.md) | @@ -43,7 +45,7 @@ Use this index for the skill-scoped quick index and global flags. | Group | Commands | Reference | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | | `dataset` | `delete`, `get`, `list`, `upload`, `validate` | [dataset.md](dataset.md) | -| `deploy` | `audio create`, `delete`, `get`, `image create`, `list`, `models`, `scale`, `text create`, `update` | [deploy.md](deploy.md) | +| `deploy` | `audio create`, `delete`, `get`, `image create`, `list`, `models`, `pause`, `resume`, `scale`, `text create`, `update` | [deploy.md](deploy.md) | | `finetune` | `audio create`, `cancel`, `capability`, `checkpoints`, `delete`, `export`, `get`, `image create`, `list`, `logs`, `text create`, `watch` | [finetune.md](finetune.md) | ## Global flags