From 42576cb53899e13acc02750d3ed3d14b1dbe8f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=95=85=E7=92=83?= Date: Tue, 10 Nov 2026 12:20:20 +0800 Subject: [PATCH 1/2] fix: finetune & dataset issue --- .../commands/src/commands/dataset/list.ts | 6 +- .../commands/src/commands/dataset/upload.ts | 5 +- .../commands/src/commands/deploy/delete.ts | 23 +- .../commands/src/commands/finetune/create.ts | 212 +++++++++----- packages/commands/src/commands/model/code.ts | 45 +-- packages/commands/src/commands/text/chat.ts | 24 +- .../tests/deploy-legacy-write.test.ts | 35 +++ .../commands/tests/e2e/finetune.e2e.test.ts | 265 +++++++++++++----- .../commands/tests/e2e/text-chat.e2e.test.ts | 159 ++++++++++- ...atalog-workspace-permission-output.test.ts | 78 +++++- .../output/fixtures/training-deployment.json | 51 +++- .../output/training-deployment-output.test.ts | 115 ++++++-- packages/core/src/dataset/api.ts | 2 +- packages/core/src/dataset/types.ts | 7 +- packages/core/src/finetune/preflight.ts | 150 +++++++++- packages/core/src/finetune/profiles/common.ts | 17 +- packages/core/src/types/command.ts | 1 + .../core/tests/finetune-preflight.test.ts | 89 +++++- packages/runtime/src/args.ts | 37 ++- packages/runtime/src/introspect.ts | 3 + packages/runtime/tests/args.test.ts | 42 +++ packages/runtime/tests/introspect.test.ts | 7 + skills/bailian-cli/reference/text.md | 44 +-- skills/bailian-finetune/reference/dataset.md | 22 +- skills/bailian-finetune/reference/finetune.md | 6 +- 25 files changed, 1153 insertions(+), 292 deletions(-) diff --git a/packages/commands/src/commands/dataset/list.ts b/packages/commands/src/commands/dataset/list.ts index 0a47afeb3..ca92e2a67 100644 --- a/packages/commands/src/commands/dataset/list.ts +++ b/packages/commands/src/commands/dataset/list.ts @@ -18,8 +18,8 @@ const LIST_FLAGS = { type: "string", valueHint: "", description: { - "en-US": 'Filter by purpose (e.g. "fine-tune", "evaluation"). Omit to list all.', - "zh-CN": '按用途筛选(例如 "fine-tune"、"evaluation")。省略时列出全部。', + "en-US": 'Filter by purpose (e.g. "fine-tune"). Omit to list all.', + "zh-CN": '按用途筛选(例如 "fine-tune")。省略时列出全部。', }, }, } satisfies FlagsDef; @@ -30,7 +30,7 @@ export default defineCommand({ auth: "apiKey", usageArgs: "[--page ] [--page-size ] [--purpose ]", flags: LIST_FLAGS, - exampleArgs: ["", "--purpose fine-tune", "--purpose evaluation --page-size 20", "--output json"], + exampleArgs: ["", "--purpose fine-tune", "--page-size 20", "--output json"], async run(ctx) { const { settings, flags } = ctx; diff --git a/packages/commands/src/commands/dataset/upload.ts b/packages/commands/src/commands/dataset/upload.ts index dc45dea60..3ba1f3a89 100644 --- a/packages/commands/src/commands/dataset/upload.ts +++ b/packages/commands/src/commands/dataset/upload.ts @@ -30,8 +30,8 @@ const UPLOAD_FLAGS = { type: "string", valueHint: "", description: { - "en-US": 'Dataset purpose tag (default: "fine-tune"; e.g. "evaluation")', - "zh-CN": '数据集用途标签(默认:"fine-tune";例如 "evaluation")', + "en-US": 'Dataset purpose tag (default: "fine-tune")', + "zh-CN": '数据集用途标签(默认:"fine-tune")', }, }, schema: { @@ -76,7 +76,6 @@ export default defineCommand({ "--file cpt.jsonl --schema cpt", "--file decision.jsonl --schema decision", "--file audio.zip --schema tts", - "--file eval.jsonl --purpose evaluation", "--file train.jsonl --full-validate", "--file train.jsonl --no-validate", ], diff --git a/packages/commands/src/commands/deploy/delete.ts b/packages/commands/src/commands/deploy/delete.ts index eb19ef40b..76a4940af 100644 --- a/packages/commands/src/commands/deploy/delete.ts +++ b/packages/commands/src/commands/deploy/delete.ts @@ -83,11 +83,24 @@ export default defineCommand({ const response = await getDeployment(ctx.client, deployedModel); const deployment = response.output ?? response.data; const status = (deployment?.status ?? "").toUpperCase(); - const isPtu = - deployment?.plan === "ptu" || - deployment?.plan === "ptu_v2" || - deployment?.ptu_capacity !== undefined || - deployment?.ptu_service_tier !== undefined; + const plan = typeof deployment?.plan === "string" ? deployment.plan.trim().toLowerCase() : ""; + const knownPtuPlan = plan === "ptu" || plan === "ptu_v2"; + const knownNonPtuPlan = plan === "mu" || plan === "cu" || plan === "lora"; + const hasMeaningfulPtuTier = + typeof deployment?.ptu_service_tier === "string" && + deployment.ptu_service_tier.trim().length > 0; + const hasMeaningfulPtuCapacity = + typeof deployment?.ptu_capacity === "object" && + deployment.ptu_capacity !== null && + !Array.isArray(deployment.ptu_capacity) && + Object.keys(deployment.ptu_capacity).length > 0; + const isPtu = knownPtuPlan || (!plan && (hasMeaningfulPtuTier || hasMeaningfulPtuCapacity)); + if (!isPtu && !knownNonPtuPlan) { + throw new BailianError( + "Cannot determine deployment plan safely; deletion was not submitted. / 无法可靠判断部署计划类型;未提交删除请求。", + ExitCode.USAGE, + ); + } if (isPtu) { if (status !== "STOPPED") { throw new BailianError( diff --git a/packages/commands/src/commands/finetune/create.ts b/packages/commands/src/commands/finetune/create.ts index 013b32fbc..38795c082 100644 --- a/packages/commands/src/commands/finetune/create.ts +++ b/packages/commands/src/commands/finetune/create.ts @@ -2,13 +2,15 @@ import { formatCommand, defineCommand, createFineTune, - getDataset, + getFineTuneParams, uploadDataset, detectModality, getProfile, fetchModelCapability, listSupportedTrainingTypes, preflightBatchSizeGate, + validateFineTuneHyperParameterOverrides, + getFineTuneHyperParameterDefaultNumber, isTrainingTypeCli, TRAINING_TYPES_CLI, DEFAULT_TRAINING_TYPE, @@ -19,6 +21,7 @@ import { type CommandContext, type CreateFineTuneRequest, type FineTuneHyperParameters, + type FineTuneHyperParameterIssue, type DatasetFile, type TrainingProfile, type DataModality, @@ -206,6 +209,86 @@ async function uploadResolvedLocal( /** The modality a `finetune create` subcommand is bound to. */ type CommandModality = "text" | "audio" | "image" | "video"; +function explicitHyperParameterOverrides( + commandModality: CommandModality, + flags: Record, +): Record { + const mappings: Record, Record> = { + text: { + nEpochs: "n_epochs", + batchSize: "batch_size", + learningRate: "learning_rate", + maxLength: "max_length", + evalSteps: "eval_steps", + loraAlpha: "lora_alpha", + loraDropout: "lora_dropout", + loraRank: "lora_rank", + lrSchedulerType: "lr_scheduler_type", + saveStrategy: "save_strategy", + saveTotalLimit: "save_total_limit", + saveSteps: "save_steps", + split: "split", + maxSplitValDatasetSample: "max_split_val_dataset_sample", + dataAugmentation: "data_augmentation", + }, + image: { learningRate: "learning_rate" }, + video: { + nEpochs: "n_epochs", + batchSize: "batch_size", + learningRate: "learning_rate", + }, + }; + if (commandModality === "audio") return {}; + + const overrides: Record = {}; + for (const [flagName, parameterName] of Object.entries(mappings[commandModality])) { + const value = flags[flagName]; + if (value !== undefined) { + overrides[parameterName] = flagName === "dataAugmentation" ? value === "true" : value; + } + } + return overrides; +} + +function formatHyperParameterIssue( + context: Pick, + issue: FineTuneHyperParameterIssue, +): string { + const value = JSON.stringify(issue.value); + switch (issue.code) { + case "unsupported": + return context.localize({ + "en-US": `${issue.parameter} is not supported for this model and training type.`, + "zh-CN": `当前模型和训练类型不支持参数 ${issue.parameter}。`, + }); + case "not_editable": + return context.localize({ + "en-US": `${issue.parameter} cannot be overridden for this model and training type.`, + "zh-CN": `当前模型和训练类型不允许覆盖参数 ${issue.parameter}。`, + }); + case "invalid_type": + return context.localize({ + "en-US": `${issue.parameter}=${value} must be ${issue.expected ?? "a scalar value"}.`, + "zh-CN": `${issue.parameter}=${value} 的类型不正确,应为 ${issue.expected ?? "标量值"}。`, + }); + case "not_supported": + return context.localize({ + "en-US": `${issue.parameter}=${value} must be one of: ${issue.expected}.`, + "zh-CN": `${issue.parameter}=${value} 必须是以下值之一:${issue.expected}。`, + }); + case "out_of_range": + return context.localize({ + "en-US": `${issue.parameter}=${value} must be within ${issue.expected}.`, + "zh-CN": `${issue.parameter}=${value} 必须位于区间 ${issue.expected}。`, + }); + case "invalid_step": + return context.localize({ + "en-US": `${issue.parameter}=${value} must follow ${issue.expected}.`, + "zh-CN": `${issue.parameter}=${value} 必须满足 ${issue.expected}。`, + }); + } +} + /** * Flags shared by every `finetune create` subcommand: what to train * (model), what data to train on (datasets / validations), and how to name the @@ -370,15 +453,17 @@ const TEXT_FLAGS = { nEpochs: { type: "number", valueHint: "", - description: { "en-US": "Number of epochs (default: 3)", "zh-CN": "训练轮数(默认:3)" }, + description: { + "en-US": "Number of epochs; valid values are checked against the selected model", + "zh-CN": "训练轮数;有效值按所选模型的权威配置检查", + }, }, batchSize: { type: "number", valueHint: "", description: { - "en-US": - "Per-device batch size (clamped to [8, 1024]). Auto-set to 8 for small datasets (<100KB)", - "zh-CN": "单设备 Batch Size(限制在 [8, 1024])。小数据集(<100KB)自动设为 8", + "en-US": "Per-device batch size; range and step are checked against the selected model", + "zh-CN": "单设备 Batch Size;范围和步长按所选模型的权威配置检查", }, }, learningRate: { @@ -511,8 +596,10 @@ const TEXT_NOTES = [ "提交任务前(非 dry-run),会通过 listFoundationModels 检查模型训练能力(无需登录控制台);如果训练类型不受支持,会立即失败并列出该模型实际支持的训练类型。", }, { - "en-US": "n_epochs defaults to 3. Other hyper-parameters are platform defaults unless set.", - "zh-CN": "n_epochs 默认为 3。其他超参数未设置时使用平台默认值。", + "en-US": + "Before upload, the command queries the selected model and training type's parameter schema. Explicit overrides must satisfy its editability, type, enum, range, and step constraints; omitted values use platform defaults.", + "zh-CN": + "上传前会查询所选模型和训练类型的参数配置。显式覆盖值必须满足可编辑性、类型、枚举、范围和步长约束;未指定项使用平台默认值。", }, { "en-US": "Learning rate is forwarded as a string to avoid JSON-number precision loss.", @@ -734,82 +821,59 @@ async function runCreate( } } - // Restore the batch-size clamping warning that was lost when the logic moved - // into profiles. The profile silently clamps to [8, 1024]; surface it here - // so the user has an audit trail. Skip modalities that bypass the batch_size - // gate (image): their batch_size is a fixed model-family default, not a - // clamp of the user's value, so the [8, 1024] "clamped" message would be - // self-contradictory and misleading. - if ( - flags.batchSize !== undefined && - hp.batch_size !== undefined && - !profile.shouldSkipGate("batch_size", modality) - ) { - const requested = flags.batchSize as number; - if (hp.batch_size !== requested) { - ctx.output.warning({ - code: "TRAINING_BATCH_SIZE_CLAMPED", - message: { - "en-US": `--batch-size ${requested} clamped to ${hp.batch_size} (server range [8, 1024] for the common training types).`, - "zh-CN": `--batch-size ${requested} 已调整为 ${hp.batch_size}(常用训练类型的服务端范围为 [8, 1024])。`, - }, - details: { requested, batch_size: hp.batch_size }, - }); - } - } - // For modalities that skip the batch_size gate, warn the user that their - // explicit --batch-size was discarded (model uses a fixed batch_size). - if (flags.batchSize !== undefined && profile.shouldSkipGate("batch_size", modality)) { - const requested = flags.batchSize as number; - if (hp.batch_size !== undefined && hp.batch_size !== requested) { - ctx.output.warning({ - code: "TRAINING_BATCH_SIZE_IGNORED", - message: { - "en-US": `--batch-size ${requested} ignored for ${modality} training (model uses a fixed batch_size of ${hp.batch_size}).`, - "zh-CN": `${modality} 训练忽略了 --batch-size ${requested}(模型使用固定 batch_size ${hp.batch_size})。`, - }, - details: { requested, batch_size: hp.batch_size, modality }, - }); - } + const parameterResponse = await getFineTuneParams(ctx.client, { + inputModel: model, + trainingType: profile.serverTrainingType, + paramType: "hyper", + workspaceId: settings.workspaceId, + }); + const parameterSchema = parameterResponse.output?.hyper_params; + if (!Array.isArray(parameterSchema) || parameterSchema.length === 0) { + throw new BailianError( + ctx.localize({ + "en-US": `No authoritative training parameter schema was returned for model "${model}" and training type "${trainingType}".`, + "zh-CN": `模型“${model}”和训练类型“${trainingType}”未返回权威训练参数配置。`, + }), + ExitCode.GENERAL, + ctx.localize({ + "en-US": "The dataset was not uploaded and the training job was not submitted.", + "zh-CN": "数据集未上传,训练任务未提交。", + }), + ); } - // Auto batch_size for small datasets — only for text data. Audio/image - // profiles already set their own batch parameters. - if (modality === "text" && hp.batch_size === undefined && !settings.dryRun) { - let sizeBytes = training.firstSize ?? 0; - if (sizeBytes === 0) { - try { - const fileInfo = await getDataset(ctx.client, trainingFileIds[0]); - sizeBytes = fileInfo.data?.size ?? 0; - } catch { - // If we can't fetch file info, skip auto-adjustment; platform will use default. - } - } - if (sizeBytes > 0 && sizeBytes < 100 * 1024) { - hp.batch_size = 8; - } + const overrideIssues = validateFineTuneHyperParameterOverrides( + parameterSchema, + explicitHyperParameterOverrides(commandModality, flags), + ); + if (overrideIssues.length > 0) { + throw new BailianError( + ctx.localize({ + "en-US": "One or more training hyper-parameter overrides are invalid.", + "zh-CN": "一个或多个训练超参数覆盖值无效。", + }), + ExitCode.USAGE, + overrideIssues.map((issue) => formatHyperParameterIssue(ctx, issue)).join("\n"), + ); } - // Pre-submit batch-size gate: the platform rejects a job whose number of - // training samples is not greater than batch_size, but only surfaces that - // ~10 minutes into the run (after data processing). Fail fast here, before - // burning quota. `recordCount` is only known when every --datasets token - // was a local file we validated; file-id tokens fall through to the - // platform rather than risk a false positive from an undercount. if ( !settings.dryRun && training.recordCount !== undefined && !profile.shouldSkipGate("batch_size", modality) ) { - // 16 is the platform default when neither the user nor the small-file - // auto-adjust set a batch_size (see the auto-adjust comment above). - const effectiveBatchSize = hp.batch_size ?? 16; - const gate = preflightBatchSizeGate({ - recordCount: training.recordCount, - batchSize: effectiveBatchSize, - }); - if (!gate.ok && gate.issue) { - throw new BailianError(gate.issue.message, ExitCode.GENERAL, gate.hint); + const effectiveBatchSize = + typeof hp.batch_size === "number" + ? hp.batch_size + : getFineTuneHyperParameterDefaultNumber(parameterSchema, "batch_size"); + if (effectiveBatchSize !== undefined) { + const gate = preflightBatchSizeGate({ + recordCount: training.recordCount, + batchSize: effectiveBatchSize, + }); + if (!gate.ok && gate.issue) { + throw new BailianError(gate.issue.message, ExitCode.GENERAL, gate.hint); + } } } diff --git a/packages/commands/src/commands/model/code.ts b/packages/commands/src/commands/model/code.ts index 94cd5d8fc..b3bb081e1 100644 --- a/packages/commands/src/commands/model/code.ts +++ b/packages/commands/src/commands/model/code.ts @@ -4,11 +4,11 @@ import { anonymousConsoleCall, defineCommand, fetchModelDetail, + formatCommand, type ModelGroupItem, type ModelSampleCodeV2, type ModelSampleSnippet, } from "bailian-cli-core"; -import { pickTrunkItems } from "./shared.ts"; /** `--api` values map onto the API-style keys used inside `sampleCodeV2`. */ const API_STYLE_KEYS: Record = { @@ -117,15 +117,9 @@ export function resolveSample( return { sdk, apiStyle, lang, code }; } -/** Prefer the exact model asked for, then any trunk item that carries samples. */ -function pickSampleBearingItem( - items: ModelGroupItem[], - modelKey: string, -): ModelGroupItem | undefined { - const exact = items.find((item) => item.model === modelKey); - if (exact?.sampleCodeV2) return exact; - const trunk = pickTrunkItems(items).find((item) => item.sampleCodeV2); - return trunk ?? items.find((item) => item.sampleCodeV2); +/** Select only the exact model requested; samples from sibling model IDs are not interchangeable. */ +function pickExactModelItem(items: ModelGroupItem[], modelKey: string): ModelGroupItem | undefined { + return items.find((item) => item.model === modelKey); } export default defineCommand({ @@ -217,20 +211,33 @@ export default defineCommand({ } const detail = await fetchModelDetail(call, flags.model); + const modelNotFoundMessage = ctx.localize({ + "en-US": `Model "${flags.model}" not found.`, + "zh-CN": `未找到模型“${flags.model}”。`, + }); + const browseCatalogHint = ctx.localize({ + "en-US": `Run "${formatCommand(ctx.identity, "model list")}" to browse the catalog.`, + "zh-CN": `运行“${formatCommand(ctx.identity, "model list")}”浏览模型目录。`, + }); if (!detail) { - throw new BailianError( - `Model "${flags.model}" not found.`, - ExitCode.GENERAL, - `Run "model list" to browse the catalog.`, - ); + throw new BailianError(modelNotFoundMessage, ExitCode.GENERAL, browseCatalogHint); } - const item = pickSampleBearingItem(detail.items ?? [], flags.model); - if (!item?.sampleCodeV2) { + const item = pickExactModelItem(detail.items ?? [], flags.model); + if (!item) { + throw new BailianError(modelNotFoundMessage, ExitCode.GENERAL, browseCatalogHint); + } + if (!item.sampleCodeV2) { throw new BailianError( - `Model "${flags.model}" publishes no SDK sample code.`, + ctx.localize({ + "en-US": `Model "${flags.model}" publishes no SDK sample code.`, + "zh-CN": `模型“${flags.model}”未发布 SDK 示例代码。`, + }), ExitCode.GENERAL, - "Speech, image and video models are called through their own command groups.", + ctx.localize({ + "en-US": "Speech, image and video models are called through their own command groups.", + "zh-CN": "语音、图像和视频模型需通过各自的命令组调用。", + }), ); } diff --git a/packages/commands/src/commands/text/chat.ts b/packages/commands/src/commands/text/chat.ts index 457e66180..fc52ac205 100644 --- a/packages/commands/src/commands/text/chat.ts +++ b/packages/commands/src/commands/text/chat.ts @@ -107,10 +107,13 @@ const CHAT_FLAGS = { }, }, enableThinking: { - type: "switch", + type: "boolean", + valueHint: "[true|false]", + implicitValue: true, description: { - "en-US": "Enable thinking/reasoning mode (for qwen3/qwq models)", - "zh-CN": "启用思考/推理模式(适用于 qwen3/qwq 模型)", + "en-US": + "Set thinking/reasoning mode; bare flag means true (model support is service-defined)", + "zh-CN": "设置思考/推理模式;裸参数表示 true(是否支持由服务端模型能力决定)", }, }, thinkingBudget: { @@ -214,6 +217,10 @@ export default defineCommand({ "en-US": '--model qwq-plus --message "Solve 1+1" --enable-thinking', "zh-CN": '--model qwq-plus --message "计算 1+1" --enable-thinking', }, + { + "en-US": '--model qwen-plus --message "Answer directly" --enable-thinking false', + "zh-CN": '--model qwen-plus --message "直接回答" --enable-thinking false', + }, { "en-US": '--enable-search true --message "Latest news about Alibaba Cloud"', "zh-CN": '--enable-search true --message "阿里云最新新闻"', @@ -313,9 +320,14 @@ export default defineCommand({ if (flags.temperature !== undefined) body.temperature = flags.temperature; if (flags.topP !== undefined) body.top_p = flags.topP; - if (flags.enableThinking) { - body.enable_thinking = true; - if (api === "chat" && "messages" in body && flags.thinkingBudget !== undefined) { + if (flags.enableThinking !== undefined) { + body.enable_thinking = flags.enableThinking; + if ( + flags.enableThinking && + api === "chat" && + "messages" in body && + flags.thinkingBudget !== undefined + ) { body.thinking_budget = flags.thinkingBudget; } } diff --git a/packages/commands/tests/deploy-legacy-write.test.ts b/packages/commands/tests/deploy-legacy-write.test.ts index 067397704..f50341d5b 100644 --- a/packages/commands/tests/deploy-legacy-write.test.ts +++ b/packages/commands/tests/deploy-legacy-write.test.ts @@ -631,6 +631,41 @@ describe("delete safety", () => { expect(requests(mock)).toHaveLength(1); }); + test.each([ + { plan: "mu", ptu_service_tier: "ptu_fast" }, + { plan: "cu", ptu_capacity: { input_tpm: 10, output_tpm: 1 } }, + { plan: "lora", ptu_service_tier: "ptu_fast", ptu_capacity: { input_tpm: 10 } }, + { plan: "mu", ptu_service_tier: "" }, + { plan: "mu", ptu_service_tier: null }, + { plan: "mu", ptu_capacity: null }, + ])("explicit non-PTU plan overrides PTU-looking fields: %j", async (deployment) => { + const mock = mockClient( + { output: { ...deployment, status: "FAILED" } }, + { output: { deleted: true } }, + ); + await run(deployDelete, { deployedModel: MODEL_CODE }, mock.client); + expect(requests(mock).map(({ method }) => method)).toEqual(["GET", "DELETE"]); + }); + + test.each([ + {}, + { ptu_service_tier: "" }, + { ptu_service_tier: null }, + { ptu_capacity: null }, + { ptu_capacity: {} }, + { ptu_capacity: [] }, + { plan: "future" }, + ])("fails closed when deployment type is not trustworthy: %j", async (deployment) => { + const mock = mockClient({ output: { ...deployment, status: "FAILED" } }); + await expect( + run(deployDelete, { deployedModel: MODEL_CODE }, mock.client), + ).rejects.toMatchObject({ + exitCode: ExitCode.USAGE, + message: expect.stringContaining("Cannot determine deployment plan"), + }); + expect(requests(mock)).toHaveLength(1); + }); + test.each([ { records: [INSTANCE], items: 1 }, { records: [{ ...INSTANCE, status: "STOPPED" }], items: 1 }, diff --git a/packages/commands/tests/e2e/finetune.e2e.test.ts b/packages/commands/tests/e2e/finetune.e2e.test.ts index 673c917eb..c4f950cd1 100644 --- a/packages/commands/tests/e2e/finetune.e2e.test.ts +++ b/packages/commands/tests/e2e/finetune.e2e.test.ts @@ -1,27 +1,176 @@ -import { describe, expect, test } from "vite-plus/test"; +import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test"; +import { createServer, type Server } from "node:http"; import { join } from "path"; import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e, + runCommandInProcess, e2eFixturesDir, } from "./helpers.ts"; import { FINETUNE_ROUTES } from "./topic-routes.ts"; +let schemaServer: Server; +let schemaOrigin = ""; + +function parameterSchema(inputModel: string): Array> { + const videoModel = inputModel.startsWith("wan"); + return [ + { + name: "batch_size", + type: "number", + data_range: videoModel ? ["1", "8192"] : ["8", "8192"], + step: videoModel ? 1 : 8, + editable: true, + defaultValue: videoModel ? "1" : "8", + }, + { + name: "n_epochs", + type: "number", + data_range: ["1", "200"], + step: 1, + editable: true, + }, + { name: "learning_rate", type: "input", editable: true }, + { + name: "max_length", + type: "intInputNumber", + data_range: ["500", "65536"], + editable: true, + }, + { + name: "eval_steps", + type: "intInputNumber", + data_range: ["1", "2147483647"], + editable: true, + }, + { + name: "lora_alpha", + type: "select", + support_values: ["16", "32", "64", "128"], + editable: true, + }, + { + name: "lora_dropout", + type: "floatInputNumber", + data_range: ["0", "0.2"], + left_range: "exclude", + right_range: "exclude", + editable: true, + }, + { + name: "lora_rank", + type: "select", + support_values: ["8", "16", "32", "64"], + editable: true, + }, + { + name: "lr_scheduler_type", + type: "select", + support_values: ["linear", "cosine"], + editable: true, + }, + { + name: "save_strategy", + type: "select", + support_values: ["steps", "epoch"], + editable: true, + }, + { + name: "save_total_limit", + type: "number", + data_range: ["1", "10"], + step: 1, + editable: true, + }, + { + name: "save_steps", + type: "intInputNumber", + data_range: ["1", "2147483647"], + editable: true, + }, + { + name: "split", + type: "floatInputNumber", + data_range: ["0", "1"], + left_range: "exclude", + right_range: "exclude", + editable: true, + }, + { + name: "max_split_val_dataset_sample", + type: "intInputNumber", + data_range: ["1", "2147483647"], + editable: true, + }, + { + name: "data_augmentation", + type: "select", + support_values: ["true", "false"], + editable: true, + }, + ]; +} + +beforeAll(async () => { + schemaServer = createServer((request, response) => { + const url = new URL(request.url ?? "/", "http://127.0.0.1"); + response.setHeader("content-type", "application/json"); + if (url.pathname !== "/api/v1/fine-tunes/fine-tunes-params") { + response.statusCode = 404; + response.end('{"message":"unexpected path"}'); + return; + } + response.end( + JSON.stringify({ + output: { + hyper_params: parameterSchema(url.searchParams.get("input_model") ?? ""), + }, + }), + ); + }); + await new Promise((resolve, reject) => { + schemaServer.once("error", reject); + schemaServer.listen(0, "127.0.0.1", resolve); + }); + const address = schemaServer.address(); + if (!address || typeof address === "string") throw new Error("Expected a local TCP server."); + schemaOrigin = `http://127.0.0.1:${address.port}`; +}); + +afterAll(async () => { + schemaServer.closeAllConnections(); + await new Promise((resolve) => schemaServer.close(() => resolve())); +}); + +function runSchemaCommand(args: string[]) { + return runCommandInProcess(FINETUNE_ROUTES, args, { + DASHSCOPE_API_KEY: "offline-test-only", + DASHSCOPE_BASE_URL: schemaOrigin, + BAILIAN_WORKSPACE_ID: "", + NODE_OPTIONS: "", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + NO_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + no_proxy: "", + NODE_USE_ENV_PROXY: "", + }); +} + /** * Fine-tune E2E. * * The suite exercises command discovery, help text, and the `--dry-run` - * structured-output path (arg parsing + body construction) with no network - * dependency. Because `ensureApiKey` runs before every command (see main.ts), - * these cases are gated by isDashScopeE2EReady() — they are skipped when no - * DashScope credential is present (e.g. on CI) and run offline when one is. - * The remote list test is also gated and tolerates both empty accounts and - * auth/permission failures (see the test comment). + * structured-output path. Fine-tune create previews query a local model-specific + * parameter schema but never upload data or submit a job. */ -describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { +describe("e2e: finetune (schema-gated)", () => { test("finetune 列出子命令", async () => { const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, ["finetune", "--help"]); expect(exitCode, stderr).toBe(0); @@ -42,7 +191,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 构造 SFT 默认请求体", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -64,7 +213,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { training_file_ids: string[]; validation_file_ids: string[]; training_type: string; - hyper_parameters: { n_epochs: number }; + hyper_parameters: Record; }; }>(stdout); expect(data.action).toBe("finetune.create"); @@ -72,11 +221,11 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { expect(data.body.training_file_ids).toEqual(["file-aaa", "file-bbb"]); expect(data.body.validation_file_ids).toEqual(["file-ccc"]); expect(data.body.training_type).toBe("efficient_sft"); - expect(data.body.hyper_parameters.n_epochs).toBe(3); + expect(data.body.hyper_parameters).toEqual({}); }); test("finetune create --dry-run 转发训练类型与超参", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -89,7 +238,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { "--n-epochs", "5", "--batch-size", - "16", + "32", "--learning-rate", "1.6e-5", "--max-length", @@ -120,10 +269,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { expect(data.body.training_type).toBe("efficient_sft"); expect(data.body.model_name).toBe("my-qwen-sft"); expect(data.body.finetuned_output_suffix).toBe("v1"); - // batch_size is forwarded verbatim when within the [8, 1024] server range. expect(data.body.hyper_parameters).toEqual({ n_epochs: 5, - batch_size: 16, + batch_size: 32, learning_rate: "1.6e-5", max_length: 4096, }); @@ -138,7 +286,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ])( "finetune create --training-type %s 经 profile 映射为 server 类型 %s", async (cliType, serverType) => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -160,7 +308,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ); test("finetune create --training-type 拒绝不支持的训练类型值", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -177,9 +325,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { expect(exitCode, stdout + stderr).not.toBe(0); }); - test("finetune create --dry-run 把本地路径标记为 pending 上传且不发起网络请求", async () => { + test("finetune create --dry-run 把本地路径标记为 pending 且不上传数据", async () => { const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -210,7 +358,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --datasets 为空时拒绝", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -225,13 +373,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { expect(exitCode, stdout + stderr).not.toBe(0); }); - test("finetune create 样本数 <= batch_size 时提交前快速失败且不上传", async () => { - // The fixture has 3 records; the small-file auto-adjust sets batch_size=8, - // so 3 <= 8 trips the pre-submit gate. The gate fires before any upload, - // so this is fully offline (no key, no network) — the proof is that the - // error is the gate message AND no "Uploaded …" line ever appears. + test("finetune create 样本数 <= 有效 batch_size 时提交前失败且不上传", async () => { const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -239,21 +383,20 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { "qwen3-8b", "--datasets", localPath, + "--batch-size", + "32", "--output", "json", ]); expect(exitCode, stdout + stderr).not.toBe(0); const combined = `${stdout}\n${stderr}`; expect(combined).toMatch(/not greater than batch_size/i); - // Crucially, no upload happened — the gate must fire before the upload step. expect(combined).not.toMatch(/Uploaded .* → file-/); }); - test("finetune create --batch-size 过小仍按 8 下限比较(不绕过卡口)", async () => { - // Even with --batch-size 1 (server clamps to 8), 3 samples <= 8 still trips - // the gate — confirms the gate uses the clamped/effective batch, not the raw. + test("finetune create 按权威配置拒绝过小 batch_size 且不上传", async () => { const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -267,7 +410,9 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { "json", ]); expect(exitCode, stdout + stderr).not.toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/batch_size \(8\)/); + const combined = `${stdout}\n${stderr}`; + expect(combined).toMatch(/batch_size/i); + expect(combined).not.toMatch(/Uploaded .* → file-/); }); test.each([ @@ -283,7 +428,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ["params", ["--base-model", "qwen3-8b"]], ["metrics", ["--job-id", "ft-xxx"]], ])("finetune %s --dry-run 发出结构化动作", async (sub, extra) => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", sub, ...extra, @@ -297,11 +442,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune delete --help 展示 --yes", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ - "finetune", - "delete", - "--help", - ]); + const { stderr, exitCode } = await runSchemaCommand(["finetune", "delete", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--yes/i); }); @@ -309,7 +450,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune params --dry-run 映射训练类型并合并 modality/reasoning-mode", async () => { // sft-lora → efficient_sft at the interface boundary; --modality and // --reasoning-mode merge into one hyper_parameters JSON query string. - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "params", "--base-model", @@ -348,7 +489,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune params --dry-run 显式训练类型按映射表转换", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "params", "--base-model", @@ -365,7 +506,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune params 非法训练类型退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stderr, exitCode } = await runSchemaCommand([ "finetune", "params", "--base-model", @@ -379,7 +520,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune params 非法 modality 枚举值退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stderr, exitCode } = await runSchemaCommand([ "finetune", "params", "--base-model", @@ -393,11 +534,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune params 缺少 --base-model 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ - "finetune", - "params", - "--quiet", - ]); + const { stderr, exitCode } = await runSchemaCommand(["finetune", "params", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--base-model|Missing required argument/i); }); @@ -405,7 +542,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune metrics --dry-run 默认 page_no=1 且显式 page_size=100", async () => { // The OpenAPI default is 1000 per page; the CLI keeps the legacy 100-page // shape by sending page_size explicitly unless the user overrides it. - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "metrics", "--job-id", @@ -428,7 +565,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune metrics --dry-run 转发 page/page-size/workspace 覆盖值", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "metrics", "--job-id", @@ -455,17 +592,13 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune metrics 缺少 --job-id 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ - "finetune", - "metrics", - "--quiet", - ]); + const { stderr, exitCode } = await runSchemaCommand(["finetune", "metrics", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--job-id|Missing required argument/i); }); test("finetune create --dry-run 解析多 datasets 中的空白", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "text", "create", @@ -487,7 +620,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune audio create --dry-run 用 sft-lora 默认 + audio 超参", async () => { // Audio has no --training-type flag; the command fixes modality=audio and // defaults to sft-lora (efficient_sft) with the fixed CosyVoice hyper-params. - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "audio", "create", @@ -525,7 +658,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune image create --dry-run 用 sft-lora 默认", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "image", "create", @@ -561,7 +694,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune video create --datasets 缺失时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stderr, exitCode } = await runSchemaCommand([ "finetune", "video", "create", @@ -581,7 +714,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ])( "finetune video create --dry-run %s 解析 batch_size=%i / max_pixels=%i", async (baseModel, batchSize, maxPixels) => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "video", "create", @@ -613,7 +746,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ); test("finetune video create --dry-run 转发超参覆盖且不做 clamp", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stdout, stderr, exitCode } = await runSchemaCommand([ "finetune", "video", "create", @@ -644,17 +777,13 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { describe("e2e: finetune high-risk confirmation", () => { test("finetune delete --help 展示 runtime 注入的 --yes", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ - "finetune", - "delete", - "--help", - ]); + const { stderr, exitCode } = await runSchemaCommand(["finetune", "delete", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--yes/i); }); test("finetune delete 无 --yes 返回确认请求 (7)", async () => { - const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + const { stderr, exitCode } = await runSchemaCommand([ "finetune", "delete", "--job-id", @@ -748,7 +877,7 @@ describe("finetune complete recipe (offline)", () => { "json", ]; test("preserves names, priority and every explicit hyperparameter", async () => { - const result = await runCommandE2e(FINETUNE_ROUTES, recipeArgs); + const result = await runSchemaCommand(recipeArgs); expect(result.exitCode, result.stderr).toBe(0); const response = parseStdoutJson<{ body: Record }>(result.stdout); expect(response.body).toMatchObject({ @@ -781,7 +910,7 @@ describe("finetune complete recipe (offline)", () => { ])("rejects invalid %s before submission", async (flagName, invalidValue) => { const invalidArgs = [...recipeArgs]; invalidArgs[invalidArgs.indexOf(flagName) + 1] = invalidValue; - const result = await runCommandE2e(FINETUNE_ROUTES, invalidArgs); + const result = await runSchemaCommand(invalidArgs); expect(result.exitCode).toBe(2); }); }); diff --git a/packages/commands/tests/e2e/text-chat.e2e.test.ts b/packages/commands/tests/e2e/text-chat.e2e.test.ts index f90504135..d25e863c6 100644 --- a/packages/commands/tests/e2e/text-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/text-chat.e2e.test.ts @@ -1,4 +1,5 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; @@ -12,13 +13,22 @@ import { import { TEXT_CHAT_ROUTES } from "./topic-routes.ts"; let isolatedDirectory: string; +const servers: Server[] = []; beforeEach(() => { isolatedDirectory = mkdtempSync(join(tmpdir(), "bl-text-prime-e2e-")); writeFileSync(join(isolatedDirectory, "config.json"), "{}"); }); -afterEach(() => { +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); rmSync(isolatedDirectory, { recursive: true, force: true }); }); @@ -37,6 +47,20 @@ const EMPTY_MODEL_ENV = { BAILIAN_WORKSPACE_ID: "", }; +async function startChatServer( + handler: (request: IncomingMessage, response: ServerResponse) => void, +): Promise<{ origin: string }> { + const server = createServer(handler); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local TCP server."); + return { origin: `http://127.0.0.1:${address.port}` }; +} + /** * Text chat:help / 分组不依赖密钥;对话需 DashScope。 */ @@ -47,6 +71,9 @@ describe("e2e: text chat", () => { expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api\s+/i); expect(stderr).toMatch(/--enable-search/i); + expect(stderr).toMatch(/--enable-thinking\s+\[true\|false\]/i); + expect(stderr).toMatch(/bare flag means true|裸参数表示 true/i); + expect(stderr).toMatch(/--enable-thinking false/i); expect(stderr).toMatch(/--prime/i); expect(stderr).toMatch(/--workspace-id/i); expect(stderr).toMatch(/chat|--message|model|stream/i); @@ -139,6 +166,136 @@ describe("e2e: text chat", () => { expect(disabled.request?.enable_search).toBe(false); }); + test.each([ + { name: "omitted", thinkingArgs: [], expected: undefined, expectedBudget: undefined }, + { name: "bare", thinkingArgs: ["--enable-thinking"], expected: true, expectedBudget: 32 }, + { + name: "explicit true", + thinkingArgs: ["--enable-thinking=true"], + expected: true, + expectedBudget: 32, + }, + { + name: "explicit false", + thinkingArgs: ["--enable-thinking", "false"], + expected: false, + expectedBudget: undefined, + }, + ])( + "text chat --dry-run keeps enable_thinking tri-state: $name", + async ({ thinkingArgs, expected, expectedBudget }) => { + const { stdout, stderr, exitCode } = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--dry-run", + "--model", + "qwen-plus", + "--message", + "hello", + ...thinkingArgs, + "--thinking-budget", + "32", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + request?: { enable_thinking?: boolean; thinking_budget?: number }; + }>(stdout); + expect(data.request?.enable_thinking).toBe(expected); + expect(data.request?.thinking_budget).toBe(expectedBudget); + }, + ); + + test("text chat actual local request preserves thinking tri-state", async () => { + const requestBodies: Array> = []; + const { origin } = await startChatServer((request, response) => { + let rawBody = ""; + request.setEncoding("utf8"); + request.on("data", (chunk: string) => { + rawBody += chunk; + }); + request.on("end", () => { + requestBodies.push(JSON.parse(rawBody) as Record); + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ choices: [{ message: { content: "ok" } }] })); + }); + }); + + for (const thinkingArgs of [[], ["--enable-thinking"], ["--enable-thinking=false"]]) { + const result = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--base-url", + origin, + "--api-key", + "sk-local-test", + "--model", + "fixture-model", + "--message", + "hello", + ...thinkingArgs, + "--thinking-budget", + "32", + "--output", + "json", + ], + {}, + ); + expect(result.exitCode, result.stderr).toBe(0); + } + + expect(requestBodies).toHaveLength(3); + expect(requestBodies[0]).not.toHaveProperty("enable_thinking"); + expect(requestBodies[0]).not.toHaveProperty("thinking_budget"); + expect(requestBodies[1]).toMatchObject({ enable_thinking: true, thinking_budget: 32 }); + expect(requestBodies[2]).toMatchObject({ enable_thinking: false }); + expect(requestBodies[2]).not.toHaveProperty("thinking_budget"); + }); + + test("text chat leaves unsupported-model thinking errors unchanged", async () => { + const { origin } = await startChatServer((request, response) => { + request.resume(); + response.writeHead(400, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + code: "InvalidParameter", + message: "fixture model does not support thinking", + request_id: "request-thinking-test", + }), + ); + }); + const result = await runCommandInProcess( + TEXT_CHAT_ROUTES, + [ + "text", + "chat", + "--base-url", + origin, + "--api-key", + "sk-local-test", + "--model", + "fixture-model", + "--message", + "hello", + "--enable-thinking", + "--output", + "json", + ], + {}, + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("fixture model does not support thinking"); + expect(result.stderr).toContain("InvalidParameter"); + expect(result.stderr).toContain("request-thinking-test"); + }); + test("text chat --dry-run 未传 --enable-search 时请求体不含 enable_search", async () => { const { stdout, stderr, exitCode } = await runCommandInProcess( TEXT_CHAT_ROUTES, diff --git a/packages/commands/tests/output/catalog-workspace-permission-output.test.ts b/packages/commands/tests/output/catalog-workspace-permission-output.test.ts index 5ac2ac3fb..2b77134e3 100644 --- a/packages/commands/tests/output/catalog-workspace-permission-output.test.ts +++ b/packages/commands/tests/output/catalog-workspace-permission-output.test.ts @@ -4,9 +4,11 @@ import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; import { buildSettings, Client, + ExitCode, httpTransport, type AnyCommand, type LocalizedText, + type ModelGroup, } from "bailian-cli-core"; import { createOutputReporter } from "bailian-cli-runtime"; import * as commands from "../../src/index.ts"; @@ -168,6 +170,7 @@ let missing: boolean, neverReady: boolean; let failing: string | undefined; let catalogTotal: number; +let modelFamily: ModelGroup; let permissionResponse: unknown; beforeEach(() => { calls = []; @@ -179,6 +182,7 @@ beforeEach(() => { neverReady = false; failing = undefined; catalogTotal = 2; + modelFamily = family; permissionResponse = permission; vi.stubEnv("NO_COLOR", "1"); vi.spyOn(httpTransport, "fetch").mockImplementation(async (input, init) => { @@ -211,7 +215,7 @@ beforeEach(() => { const request = params.Data.input as Record; payload = { total: catalogTotal, - list: missing ? [] : request.queryGroupByModel ? [family] : [family, deadFamily], + list: missing ? [] : request.queryGroupByModel ? [modelFamily] : [family, deadFamily], }; } else if (params.Api === schemaApi) payload = { predictConfig: schema }; else if (params.Api === workspaceApi) payload = missing ? [] : workspace; @@ -366,6 +370,78 @@ const diagnostics = (stderr: string) => .split("\n") .map((line) => JSON.parse(line)) : []; + +test("model code uses the exact requested model sample", async () => { + modelFamily = { + ...family, + items: [ + item, + { + ...item, + model: "qwen-fixture-2024-09-19", + category: "snapshot", + sampleCodeV2: { + openai: { completionsAPI: { python: { code: 'print("snapshot")\n' } } }, + }, + }, + ], + }; + const result = await invoke( + cases.find(({ name }) => name === "model code")!, + { + format: "text", + quiet: true, + }, + { model: "qwen-fixture-2024-09-19" }, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe('print("snapshot")\n\n'); +}); + +test("model code rejects an unlisted concrete model instead of using a sibling sample", async () => { + const result = await invoke( + cases.find(({ name }) => name === "model code")!, + { + format: "json", + quiet: true, + }, + { model: "qwen-fixture-missing" }, + ); + expect(result.exitCode).toBe(1); + expect(diagnostics(result.stderr)[0]).toMatchObject({ + error: { + code: ExitCode.GENERAL, + message: expect.stringContaining('Model "qwen-fixture-missing" not found.'), + }, + }); + expect(result.stdout).toBe(""); +}); + +test("model code rejects an exact model with no sample instead of using a sibling sample", async () => { + modelFamily = { + ...family, + items: [...family.items, { ...item, model: "qwen-fixture-no-sample", sampleCodeV2: undefined }], + }; + const result = await invoke( + cases.find(({ name }) => name === "model code")!, + { + format: "json", + quiet: true, + }, + { model: "qwen-fixture-no-sample" }, + ); + expect(result.exitCode).toBe(1); + expect(diagnostics(result.stderr)[0]).toMatchObject({ + error: { + code: ExitCode.GENERAL, + message: expect.stringContaining( + 'Model "qwen-fixture-no-sample" publishes no SDK sample code.', + ), + }, + }); + expect(result.stdout).toBe(""); +}); + const modes = ["json", "text"].flatMap((format) => [false, true].flatMap((quiet) => [false, true].map((verbose) => ({ format: format as "text" | "json", quiet, verbose })), diff --git a/packages/commands/tests/output/fixtures/training-deployment.json b/packages/commands/tests/output/fixtures/training-deployment.json index d477cb2c5..855f08d48 100644 --- a/packages/commands/tests/output/fixtures/training-deployment.json +++ b/packages/commands/tests/output/fixtures/training-deployment.json @@ -744,7 +744,7 @@ "requests": [] }, "text training create preview": { - "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"qwen2.5-7b-instruct\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"efficient_sft\",\n \"hyper_parameters\": {\n \"n_epochs\": 3,\n \"batch_size\": 8\n }\n }\n}\n", + "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"qwen2.5-7b-instruct\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"efficient_sft\",\n \"hyper_parameters\": {\n \"batch_size\": 4\n }\n }\n}\n", "data": { "action": "finetune.create", "body": { @@ -752,28 +752,45 @@ "training_file_ids": ["file-fixture"], "training_type": "efficient_sft", "hyper_parameters": { - "n_epochs": 3, - "batch_size": 8 + "batch_size": 4 } } }, - "requests": [] + "requests": [ + { + "url": "https://fixture.invalid/api/v1/fine-tunes/fine-tunes-params?input_model=qwen2.5-7b-instruct&training_type=efficient_sft¶m_type=hyper", + "method": "GET", + "body": null + } + ] }, "audio training create preview": { - "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"cosyvoice-v3.5\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"sft\",\n \"hyper_parameters\": {\n \"n_epochs\": 3,\n \"batch_size\": 8\n }\n }\n}\n", + "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"cosyvoice-v3.5\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"efficient_sft\",\n \"hyper_parameters\": {\n \"lm_max_epoch\": 60,\n \"lm_step\": 5,\n \"lm_num\": 3,\n \"lm_batch_size\": 1000,\n \"fm_max_epoch\": 100,\n \"fm_step\": 10,\n \"fm_num\": 3,\n \"fm_batch_size\": 2000\n }\n }\n}\n", "data": { "action": "finetune.create", "body": { "model": "cosyvoice-v3.5", "training_file_ids": ["file-fixture"], - "training_type": "sft", + "training_type": "efficient_sft", "hyper_parameters": { - "n_epochs": 3, - "batch_size": 8 + "lm_max_epoch": 60, + "lm_step": 5, + "lm_num": 3, + "lm_batch_size": 1000, + "fm_max_epoch": 100, + "fm_step": 10, + "fm_num": 3, + "fm_batch_size": 2000 } } }, - "requests": [] + "requests": [ + { + "url": "https://fixture.invalid/api/v1/fine-tunes/fine-tunes-params?input_model=cosyvoice-v3.5&training_type=efficient_sft¶m_type=hyper", + "method": "GET", + "body": null + } + ] }, "image training create preview": { "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"wan2.7\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"efficient_sft\",\n \"hyper_parameters\": {\n \"learning_rate\": \"3e-5\",\n \"max_steps\": 800,\n \"eval_steps\": 200,\n \"max_token_length\": \"1k\",\n \"gradient_clip\": 0.5,\n \"weight_decay\": 0.02,\n \"max_pixels\": \"2k\",\n \"val_img_size\": \"2k\",\n \"generation_type\": \"t2i\",\n \"lora_rank\": 32,\n \"save_total_limit\": 10,\n \"split\": 0.9\n }\n }\n}\n", @@ -799,7 +816,13 @@ } } }, - "requests": [] + "requests": [ + { + "url": "https://fixture.invalid/api/v1/fine-tunes/fine-tunes-params?input_model=wan2.7&training_type=efficient_sft¶m_type=hyper", + "method": "GET", + "body": null + } + ] }, "video training create preview": { "stdout": "{\n \"action\": \"finetune.create\",\n \"body\": {\n \"model\": \"wan2.6-i2v\",\n \"training_file_ids\": [\n \"file-fixture\"\n ],\n \"training_type\": \"efficient_sft\",\n \"hyper_parameters\": {\n \"n_epochs\": 50,\n \"learning_rate\": \"2e-5\",\n \"split\": 0.5,\n \"max_split_val_dataset_sample\": 5,\n \"eval_epochs\": 20,\n \"save_total_limit\": 10,\n \"lora_rank\": 32,\n \"lora_alpha\": 32,\n \"batch_size\": 4,\n \"max_pixels\": 262144\n }\n }\n}\n", @@ -823,6 +846,12 @@ } } }, - "requests": [] + "requests": [ + { + "url": "https://fixture.invalid/api/v1/fine-tunes/fine-tunes-params?input_model=wan2.6-i2v&training_type=efficient_sft¶m_type=hyper", + "method": "GET", + "body": null + } + ] } } diff --git a/packages/commands/tests/output/training-deployment-output.test.ts b/packages/commands/tests/output/training-deployment-output.test.ts index bc3bc604e..1e5d096b2 100644 --- a/packages/commands/tests/output/training-deployment-output.test.ts +++ b/packages/commands/tests/output/training-deployment-output.test.ts @@ -53,6 +53,33 @@ const deployment = { ...retained, }; const response = { request_id: "request-fixture", output: job, ...retained }; +const trainingParameters = { + output: { + hyper_params: [ + { + name: "batch_size", + type: "number", + data_range: ["1", "8192"], + left_range: "include", + right_range: "include", + step: 1, + editable: true, + defaultValue: "32", + }, + { + name: "n_epochs", + type: "number", + data_range: ["1", "200"], + left_range: "include", + right_range: "include", + step: 1, + editable: true, + defaultValue: "3", + }, + { name: "learning_rate", type: "input", editable: true, defaultValue: "2e-5" }, + ], + }, +}; const scenarios: Scenario[] = [ { name: "dataset list", @@ -357,43 +384,28 @@ const scenarios: Scenario[] = [ trainingType: "sft-lora", batchSize: 4, }, - response: {}, + response: trainingParameters, dryRun: true, }, { name: "audio training create preview", command: commands.finetuneAudioCreate, - flags: { - baseModel: "cosyvoice-v3.5", - datasets: "file-fixture", - trainingType: "sft", - batchSize: 4, - }, - response: {}, + flags: { baseModel: "cosyvoice-v3.5", datasets: "file-fixture" }, + response: trainingParameters, dryRun: true, }, { name: "image training create preview", command: commands.finetuneImageCreate, - flags: { - baseModel: "wan2.7", - datasets: "file-fixture", - trainingType: "sft-lora", - batchSize: 4, - }, - response: {}, + flags: { baseModel: "wan2.7", datasets: "file-fixture" }, + response: trainingParameters, dryRun: true, }, { name: "video training create preview", command: commands.finetuneVideoCreate, - flags: { - baseModel: "wan2.6-i2v", - datasets: "file-fixture", - trainingType: "sft-lora", - batchSize: 4, - }, - response: {}, + flags: { baseModel: "wan2.6-i2v", datasets: "file-fixture", batchSize: 4 }, + response: trainingParameters, dryRun: true, }, ]; @@ -532,6 +544,65 @@ async function withDatasetFixture(contents: string, run: (file: string) => Promi await rm(directory, { recursive: true, force: true }); } } + +test("invalid authoritative training override stops before upload and submission", async () => { + const contents = '{"messages":[{"role":"assistant","content":"fixture response"}]}\n'; + await withDatasetFixture(contents, async (file) => { + const result = await invoke( + { + name: "invalid training override", + command: commands.finetuneTextCreate, + flags: { + baseModel: "qwen-fixture", + datasets: file, + trainingType: "sft-lora", + batchSize: 4, + }, + response: { + output: { + hyper_params: [ + { + name: "batch_size", + type: "number", + data_range: ["32", "8192"], + left_range: "include", + right_range: "include", + step: 8, + editable: true, + }, + ], + }, + }, + }, + { format: "json", quiet: false, verbose: false }, + ); + expect(result.exitCode).toBe(2); + expect(result.requests).toEqual([expect.objectContaining({ method: "GET", body: null })]); + expect(result.stderr).toContain("training hyper-parameter overrides are invalid"); + expect(result.stderr).not.toContain("Uploaded"); + }); +}); + +test("training parameter lookup failure stops before upload and submission", async () => { + const contents = '{"messages":[{"role":"assistant","content":"fixture response"}]}\n'; + await withDatasetFixture(contents, async (file) => { + const result = await invoke( + { + name: "failed training parameter lookup", + command: commands.finetuneTextCreate, + flags: { baseModel: "qwen-fixture", datasets: file, trainingType: "sft-lora" }, + response: {}, + }, + { format: "json", quiet: false, verbose: false }, + true, + ); + expect(result.exitCode).toBe(1); + expect(result.requests).toEqual([expect.objectContaining({ method: "GET", body: null })]); + expect(result.stderr).toContain("original server failure"); + expect(result.stderr).not.toContain("Uploaded"); + }); +}); + test.each(displays)( "dataset missing data returns JSON null or the old text: %j", async (display) => { diff --git a/packages/core/src/dataset/api.ts b/packages/core/src/dataset/api.ts index eaeafec22..df21054de 100644 --- a/packages/core/src/dataset/api.ts +++ b/packages/core/src/dataset/api.ts @@ -27,7 +27,7 @@ export interface DatasetUploadParams { /** * Purpose tag forwarded to the platform. Defaults to "fine-tune" because * the API requires the field, but callers should set this explicitly when - * uploading evaluation or other dataset kinds. + * uploading other dataset kinds. */ purpose?: string; /** 已由调用方校验的不可变字节;提供时不再读取 filePath。 */ diff --git a/packages/core/src/dataset/types.ts b/packages/core/src/dataset/types.ts index 713178303..e1d72873f 100644 --- a/packages/core/src/dataset/types.ts +++ b/packages/core/src/dataset/types.ts @@ -2,9 +2,8 @@ * Dataset API types. * * Maps DashScope `/api/v1/files` responses. The same endpoint backs every - * dataset purpose the platform supports today (fine-tune training, - * evaluation, etc.) — these types are deliberately purpose-agnostic so new - * purposes can be plugged in without schema changes. + * dataset purpose the platform supports today — these types are deliberately + * purpose-agnostic so new purposes can be plugged in without schema changes. */ /** A single uploaded dataset file as returned by the platform. */ @@ -17,7 +16,7 @@ export interface DatasetFile { size?: number; /** Content hash (server-computed). */ md5?: string; - /** Free-form purpose tag, e.g. "fine-tune", "evaluation". */ + /** Free-form purpose tag, e.g. "fine-tune". */ purpose?: string; /** Optional internal/external URL (kept for parity with the API). */ url?: string; diff --git a/packages/core/src/finetune/preflight.ts b/packages/core/src/finetune/preflight.ts index ac4836059..5ab435d53 100644 --- a/packages/core/src/finetune/preflight.ts +++ b/packages/core/src/finetune/preflight.ts @@ -15,6 +15,7 @@ * `finetune create` (the only call site today) — that's the job-level boundary. */ import type { ValidationIssue } from "../dataset/validate/types.ts"; +import type { FineTuneParamHyperParam } from "./types.ts"; /** Stable issue code for "too few training samples for the batch size". */ export const INSUFFICIENT_SAMPLES_CODE = "INSUFFICIENT_SAMPLES"; @@ -28,11 +29,7 @@ export interface BatchSizeGateInput { * platform. */ recordCount: number; - /** - * Effective batch_size the job will run with — after the CLI's clamp - * ([8, 1024]) and small-file auto-adjust, or the platform default (16) when - * neither the user nor auto-adjust set one. - */ + /** Effective batch_size from the user override or the model-specific service default. */ batchSize: number; } @@ -73,7 +70,148 @@ export function preflightBatchSizeGate(input: BatchSizeGateInput): BatchSizeGate "Options:", " • add more data (recommended: comfortably more than batch_size, since the", " platform also holds back a default 0.9 train split),", - " • lower --batch-size (server clamps to a minimum of 8).", + " • lower --batch-size within the range published for this model and training type.", ].join("\n"), }; } + +export type FineTuneHyperParameterIssueCode = + | "unsupported" + | "not_editable" + | "invalid_type" + | "not_supported" + | "out_of_range" + | "invalid_step"; + +export interface FineTuneHyperParameterIssue { + parameter: string; + value: unknown; + code: FineTuneHyperParameterIssueCode; + expected?: string; +} + +function numericValue(value: unknown): number | undefined { + if (typeof value === "number") return Number.isFinite(value) ? value : undefined; + if (typeof value !== "string" || value.trim() === "") return undefined; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function isStepAligned(value: number, origin: number, step: number): boolean { + const quotient = (value - origin) / step; + return Math.abs(quotient - Math.round(quotient)) < 1e-9; +} + +export function validateFineTuneHyperParameterOverrides( + schema: FineTuneParamHyperParam[], + overrides: Record, +): FineTuneHyperParameterIssue[] { + const definitions = new Map( + schema.flatMap((parameter) => (parameter.name ? [[parameter.name, parameter] as const] : [])), + ); + const issues: FineTuneHyperParameterIssue[] = []; + + for (const [parameter, value] of Object.entries(overrides)) { + const definition = definitions.get(parameter); + if (!definition) { + issues.push({ parameter, value, code: "unsupported" }); + continue; + } + if (definition.editable === false) { + issues.push({ parameter, value, code: "not_editable" }); + continue; + } + + const serialized = String(value); + const supportedValues = definition.support_values ?? []; + if (supportedValues.length > 0 && !supportedValues.includes(serialized)) { + issues.push({ + parameter, + value, + code: "not_supported", + expected: supportedValues.join(", "), + }); + continue; + } + + const range = definition.data_range ?? []; + const numericType = + definition.type === "number" || + definition.type === "intInputNumber" || + definition.type === "floatInputNumber" || + range.length === 2 || + definition.step !== undefined; + if (numericType) { + const numeric = numericValue(value); + if ( + numeric === undefined || + (definition.type === "intInputNumber" && !Number.isInteger(numeric)) + ) { + issues.push({ + parameter, + value, + code: "invalid_type", + expected: definition.type === "intInputNumber" ? "integer" : "number", + }); + continue; + } + + const minimum = numericValue(range[0]); + const maximum = numericValue(range[1]); + const belowMinimum = + minimum !== undefined && + (definition.left_range === "exclude" ? numeric <= minimum : numeric < minimum); + const aboveMaximum = + maximum !== undefined && + (definition.right_range === "exclude" ? numeric >= maximum : numeric > maximum); + if (belowMinimum || aboveMaximum) { + const left = definition.left_range === "exclude" ? "(" : "["; + const right = definition.right_range === "exclude" ? ")" : "]"; + issues.push({ + parameter, + value, + code: "out_of_range", + expected: `${left}${range[0]}, ${range[1]}${right}`, + }); + continue; + } + + if ( + definition.step !== undefined && + definition.step > 0 && + !isStepAligned(numeric, minimum ?? 0, definition.step) + ) { + issues.push({ + parameter, + value, + code: "invalid_step", + expected: `step ${definition.step} from ${minimum ?? 0}`, + }); + } + continue; + } + + if ( + value === null || + typeof value === "object" || + (definition.type === "input" && serialized.trim() === "") + ) { + issues.push({ + parameter, + value, + code: "invalid_type", + expected: definition.type ?? "scalar", + }); + } + } + + return issues; +} + +export function getFineTuneHyperParameterDefaultNumber( + schema: FineTuneParamHyperParam[], + parameter: string, +): number | undefined { + const definition = schema.find((candidate) => candidate.name === parameter); + return numericValue(definition?.defaultValue ?? definition?.recommend_value); +} diff --git a/packages/core/src/finetune/profiles/common.ts b/packages/core/src/finetune/profiles/common.ts index 142e0bbbe..f28df4452 100644 --- a/packages/core/src/finetune/profiles/common.ts +++ b/packages/core/src/finetune/profiles/common.ts @@ -2,10 +2,9 @@ * Shared helpers for training profiles. * * Most text-based training types (sft, dpo, cpt, and their -lora variants) - * share the same hyper-parameter resolution logic: n_epochs defaults to 3, - * learning_rate and max_length are set only when explicitly provided, and - * batch_size is clamped to the server's [8, 1024] range. Extracting this - * into a single helper prevents drift when defaults or bounds change. + * share the same hyper-parameter resolution logic: only explicitly supplied + * values are forwarded. Defaults and valid ranges come from the model-specific + * training parameter schema returned by the service. */ import type { TrainingProfile, DataModality } from "./types.ts"; import type { ValidateOpts, ValidationResult } from "../../dataset/validate/types.ts"; @@ -22,16 +21,10 @@ export function resolveTextHyperParameters( flags: Record, ): Record { const hp: Record = {}; - hp.n_epochs = flags.nEpochs !== undefined ? (flags.nEpochs as number) : 3; + if (flags.nEpochs !== undefined) hp.n_epochs = flags.nEpochs as number; if (flags.learningRate !== undefined) hp.learning_rate = flags.learningRate as string; if (flags.maxLength !== undefined) hp.max_length = flags.maxLength as number; - if (flags.batchSize !== undefined) { - const requested = flags.batchSize as number; - let batchSize = requested; - if (batchSize < 8) batchSize = 8; - if (batchSize > 1024) batchSize = 1024; - hp.batch_size = batchSize; - } + if (flags.batchSize !== undefined) hp.batch_size = flags.batchSize as number; return hp; } diff --git a/packages/core/src/types/command.ts b/packages/core/src/types/command.ts index 4844c1e7c..6902a0588 100644 --- a/packages/core/src/types/command.ts +++ b/packages/core/src/types/command.ts @@ -23,6 +23,7 @@ export interface ValueFlag { description: LocalizedText; valueHint: string; required?: boolean; + implicitValue?: boolean; /** * Restrict to a fixed set of values. The parser rejects anything else and the * parsed type narrows to the union. Declare with `as const` so the literals diff --git a/packages/core/tests/finetune-preflight.test.ts b/packages/core/tests/finetune-preflight.test.ts index 1bc2552d3..dd1580b17 100644 --- a/packages/core/tests/finetune-preflight.test.ts +++ b/packages/core/tests/finetune-preflight.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from "vite-plus/test"; -import { preflightBatchSizeGate, INSUFFICIENT_SAMPLES_CODE } from "../src/index.ts"; +import { + getFineTuneHyperParameterDefaultNumber, + INSUFFICIENT_SAMPLES_CODE, + preflightBatchSizeGate, + validateFineTuneHyperParameterOverrides, + type FineTuneParamHyperParam, +} from "../src/index.ts"; describe("preflightBatchSizeGate", () => { test("passes when recordCount exceeds batch_size", () => { @@ -38,10 +44,81 @@ describe("preflightBatchSizeGate", () => { expect(r.hint).toMatch(/0\.9 train split/); }); - test("honors the effective (clamped) batch size, not a raw sub-minimum", () => { - // The CLI clamps --batch-size 1 up to 8 before calling; 3 <= 8 still fails. - const r = preflightBatchSizeGate({ recordCount: 3, batchSize: 8 }); - expect(r.ok).toBe(false); - expect(r.issue!.message).toMatch(/batch_size \(8\)/); + test("honors the effective model-specific batch size", () => { + const result = preflightBatchSizeGate({ recordCount: 31, batchSize: 32 }); + expect(result.ok).toBe(false); + expect(result.issue!.message).toMatch(/batch_size \(32\)/); + expect(result.hint).toMatch(/range published for this model/); + }); +}); + +const schema: FineTuneParamHyperParam[] = [ + { + name: "batch_size", + type: "number", + data_range: ["32", "8192"], + left_range: "include", + right_range: "include", + step: 8, + editable: true, + defaultValue: "32", + }, + { + name: "lora_dropout", + type: "floatInputNumber", + data_range: ["0", "0.2"], + left_range: "exclude", + right_range: "exclude", + editable: true, + }, + { + name: "lora_rank", + type: "select", + support_values: ["8", "16", "32", "64"], + editable: true, + }, + { + name: "eval_steps", + type: "intInputNumber", + data_range: ["1", "2147483647"], + left_range: "include", + right_range: "include", + editable: true, + }, + { name: "fixed", type: "number", editable: false }, + { name: "fallback_default", type: "number", recommend_value: "7" }, +]; + +describe("validateFineTuneHyperParameterOverrides", () => { + test("accepts values that satisfy enum, range, type, and step constraints", () => { + expect( + validateFineTuneHyperParameterOverrides(schema, { + batch_size: 40, + lora_dropout: 0.1, + lora_rank: 16, + eval_steps: 50, + }), + ).toEqual([]); + }); + + test.each([ + [{ unknown: 1 }, "unsupported"], + [{ fixed: 1 }, "not_editable"], + [{ eval_steps: 1.5 }, "invalid_type"], + [{ lora_rank: 4 }, "not_supported"], + [{ batch_size: 16 }, "out_of_range"], + [{ batch_size: 34 }, "invalid_step"], + [{ lora_dropout: 0 }, "out_of_range"], + [{ lora_dropout: 0.2 }, "out_of_range"], + ] as const)("rejects %j as %s", (overrides, code) => { + expect(validateFineTuneHyperParameterOverrides(schema, overrides)).toEqual([ + expect.objectContaining({ code }), + ]); + }); + + test("uses defaultValue before recommend_value for numeric service defaults", () => { + expect(getFineTuneHyperParameterDefaultNumber(schema, "batch_size")).toBe(32); + expect(getFineTuneHyperParameterDefaultNumber(schema, "fallback_default")).toBe(7); + expect(getFineTuneHyperParameterDefaultNumber(schema, "missing")).toBeUndefined(); }); }); diff --git a/packages/runtime/src/args.ts b/packages/runtime/src/args.ts index 9f4eeea1e..483779271 100644 --- a/packages/runtime/src/args.ts +++ b/packages/runtime/src/args.ts @@ -185,17 +185,33 @@ export function parseFlags( if (value === undefined) { const next = rest[index + 1]; if (next === undefined || next.startsWith("--")) { - throw usageError(localize, { - "en-US": `Flag --${rawKey} requires a value.`, - "zh-CN": `参数 --${rawKey} 需要一个值。`, - }); + if (definition.type === "boolean" && definition.implicitValue !== undefined) { + flags[key] = definition.implicitValue; + index += 1; + } else { + throw usageError(localize, { + "en-US": `Flag --${rawKey} requires a value.`, + "zh-CN": `参数 --${rawKey} 需要一个值。`, + }); + } + } else { + value = next; + index += 2; } - value = next; - index += 2; } else { index += 1; } + if (seen.has(key)) { + throw usageError(localize, { + "en-US": `Flag --${rawKey} given more than once.`, + "zh-CN": `参数 --${rawKey} 不能重复指定。`, + }); + } + seen.add(key); + + if (value === undefined) continue; + if (definition.choices && !definition.choices.includes(value)) { throw usageError(localize, { "en-US": `Flag --${rawKey} must be one of: ${definition.choices.join(", ")}.`, @@ -207,17 +223,10 @@ export function parseFlags( const values = flags[key] as string[] | undefined; if (values) values.push(value); else flags[key] = [value]; + seen.delete(key); continue; } - if (seen.has(key)) { - throw usageError(localize, { - "en-US": `Flag --${rawKey} given more than once.`, - "zh-CN": `参数 --${rawKey} 不能重复指定。`, - }); - } - seen.add(key); - if (definition.type === "number") { const numberValue = Number(value); if (!Number.isFinite(numberValue)) { diff --git a/packages/runtime/src/introspect.ts b/packages/runtime/src/introspect.ts index 6caa27e95..f0f7ff62b 100644 --- a/packages/runtime/src/introspect.ts +++ b/packages/runtime/src/introspect.ts @@ -68,6 +68,9 @@ function flagsSchema(definitions: FlagsDef, identity: Identity): Record", description: "Image URL (repeatable)" }, n: { type: "number", valueHint: "", description: "Number of images" }, watermark: { type: "boolean", valueHint: "", description: "Watermark" }, + thinking: { + type: "boolean", + valueHint: "[true|false]", + implicitValue: true, + description: "Thinking", + }, async: { type: "switch", description: "Return immediately" }, } satisfies FlagsDef; const OPTS = { ...GLOBAL_FLAGS, ...IMAGE_GENERATE_FLAGS }; @@ -138,6 +144,42 @@ test("parseFlags coerces boolean flags to real booleans", () => { expect(parseFlags(["--prompt", "x", "--watermark=true"], OPTS).watermark).toBe(true); }); +test("parseFlags keeps omitted opt-in booleans undefined", () => { + expect(parseFlags(["--prompt", "x"], OPTS).thinking).toBeUndefined(); +}); + +test.each([ + { argumentsList: ["--prompt", "x", "--thinking"], expected: true }, + { argumentsList: ["--prompt", "x", "--thinking", "true"], expected: true }, + { argumentsList: ["--prompt", "x", "--thinking=true"], expected: true }, + { argumentsList: ["--prompt", "x", "--thinking", "false"], expected: false }, + { argumentsList: ["--prompt", "x", "--thinking=false"], expected: false }, + { argumentsList: ["--prompt", "x", "--thinking", "--async"], expected: true }, +])("parseFlags parses opt-in boolean form $argumentsList", ({ argumentsList, expected }) => { + const flags = parseFlags(argumentsList, OPTS); + expect(flags.thinking).toBe(expected); + expect(flags.async).toBe(argumentsList.includes("--async")); +}); + +test("parseFlags rejects invalid and repeated opt-in booleans", () => { + expect(() => parseFlags(["--prompt", "x", "--thinking", "yes"], OPTS)).toThrowError( + expect.objectContaining({ message: expect.stringContaining("true or false") }), + ); + expect(() => parseFlags(["--prompt", "x", "--thinking", "--thinking=false"], OPTS)).toThrowError( + expect.objectContaining({ message: expect.stringContaining("more than once") }), + ); +}); + +test("parseFlags keeps ordinary booleans value-required and switches unchanged", () => { + expect(() => parseFlags(["--prompt", "x", "--watermark"], OPTS)).toThrowError( + expect.objectContaining({ message: expect.stringContaining("requires a value") }), + ); + expect(parseFlags(["--prompt", "x", "--async"], OPTS).async).toBe(true); + expect(() => parseFlags(["--prompt", "x", "--async=false"], OPTS)).toThrowError( + expect.objectContaining({ message: expect.stringContaining("takes no value") }), + ); +}); + test("parseFlags collects repeated array flags", () => { expect(parseFlags(["--prompt", "x", "--image", "a", "--image", "b"], OPTS).image).toEqual([ "a", diff --git a/packages/runtime/tests/introspect.test.ts b/packages/runtime/tests/introspect.test.ts index a3dca2f0f..576d45383 100644 --- a/packages/runtime/tests/introspect.test.ts +++ b/packages/runtime/tests/introspect.test.ts @@ -19,6 +19,12 @@ const command = defineCommand({ choices: ["append", "replace"], description: "Mode", }, + thinking: { + type: "boolean", + valueHint: "[true|false]", + implicitValue: true, + description: "Thinking", + }, }, usageArgs: "--index-id ", exampleArgs: ["--index-id example", { "en-US": "# confirm first", "zh-CN": "# 先确认" }], @@ -129,6 +135,7 @@ test("schema contains actual flags, bilingual metadata, credential definitions a flags: { "--index-id": { required: true, type: "string" }, "--mode": { choices: ["append", "replace"] }, + "--thinking": { type: "boolean", implicitValue: true, valueHint: "[true|false]" }, "--yes": { type: "switch" }, }, notes: command.notes, diff --git a/skills/bailian-cli/reference/text.md b/skills/bailian-cli/reference/text.md index 87231cf2d..24c651e72 100644 --- a/skills/bailian-cli/reference/text.md +++ b/skills/bailian-cli/reference/text.md @@ -24,26 +24,26 @@ Index: [index.md](index.md) #### Flags -| Flag | Type | Required | Description | -| ------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------- | -| `--api ` | string | no | API to call (default: chat) | -| `--model ` | string | no | Model ID (default: qwen3.8-max) | -| `--message ` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file | -| `--messages-file ` | string | no | Messages array for one request (use - for stdin) | -| `--system ` | string | no | System prompt | -| `--max-tokens ` | number | no | Maximum tokens to generate (default: 4096) | -| `--temperature ` | number | no | Sampling temperature (0.0, 2.0] | -| `--top-p ` | number | no | Nucleus sampling threshold | -| `--enable-search ` | boolean | no | Enable built-in web search (Chat Completions API only; default: true for qwen3.8-max) | -| `--stream` | switch | no | Stream response tokens (default: on in TTY) | -| `--tool ` | array | no | Tool definition as JSON or file path (repeatable) | -| `--enable-thinking` | switch | no | Enable thinking/reasoning mode (for qwen3/qwq models) | -| `--thinking-budget ` | number | no | Max tokens for thinking (default: 4096) | -| `--batch` | switch | no | Single synchronous Batch Chat request (non-streaming) | -| `--prime` | switch | no | Use Prime mode with a workspace-scoped endpoint | -| `--workspace-id ` | string | no | Workspace ID for the default Prime endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------- | +| `--api ` | string | no | API to call (default: chat) | +| `--model ` | string | no | Model ID (default: qwen3.8-max) | +| `--message ` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file | +| `--messages-file ` | string | no | Messages array for one request (use - for stdin) | +| `--system ` | string | no | System prompt | +| `--max-tokens ` | number | no | Maximum tokens to generate (default: 4096) | +| `--temperature ` | number | no | Sampling temperature (0.0, 2.0] | +| `--top-p ` | number | no | Nucleus sampling threshold | +| `--enable-search ` | boolean | no | Enable built-in web search (Chat Completions API only; default: true for qwen3.8-max) | +| `--stream` | switch | no | Stream response tokens (default: on in TTY) | +| `--tool ` | array | no | Tool definition as JSON or file path (repeatable) | +| `--enable-thinking [true\|false]` | boolean | no | Set thinking/reasoning mode; bare flag means true (model support is service-defined) | +| `--thinking-budget ` | number | no | Max tokens for thinking (default: 4096) | +| `--batch` | switch | no | Single synchronous Batch Chat request (non-streaming) | +| `--prime` | switch | no | Use Prime mode with a workspace-scoped endpoint | +| `--workspace-id ` | string | no | Workspace ID for the default Prime endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Notes @@ -91,6 +91,10 @@ bl text chat --message "Hello" --output json bl text chat --model qwq-plus --message "Solve 1+1" --enable-thinking ``` +```bash +bl text chat --model qwen-plus --message "Answer directly" --enable-thinking false +``` + ```bash bl text chat --enable-search true --message "Latest news about Alibaba Cloud" ``` diff --git a/skills/bailian-finetune/reference/dataset.md b/skills/bailian-finetune/reference/dataset.md index 672708a4f..c52cedfeb 100644 --- a/skills/bailian-finetune/reference/dataset.md +++ b/skills/bailian-finetune/reference/dataset.md @@ -92,13 +92,13 @@ bl dataset get --file-id file-xxx --output json #### Flags -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | --------------------------------------------------------------------- | -| `--page ` | number | no | Page number (default: 1) | -| `--page-size ` | number | no | Results per page (default: 10, max 100) | -| `--purpose ` | string | no | Filter by purpose (e.g. "fine-tune", "evaluation"). Omit to list all. | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | ------------------------------------------------------- | +| `--page ` | number | no | Page number (default: 1) | +| `--page-size ` | number | no | Results per page (default: 10, max 100) | +| `--purpose ` | string | no | Filter by purpose (e.g. "fine-tune"). Omit to list all. | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Examples @@ -111,7 +111,7 @@ bl dataset list --purpose fine-tune ``` ```bash -bl dataset list --purpose evaluation --page-size 20 +bl dataset list --page-size 20 ``` ```bash @@ -132,7 +132,7 @@ bl dataset list --output json | Flag | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--file ` | string | yes | Local dataset file (.jsonl or .zip; ≤200MB SFT/DPO, ≤300MB CPT/decision, ≤2GB media zip) | -| `--purpose ` | string | no | Dataset purpose tag (default: "fine-tune"; e.g. "evaluation") | +| `--purpose ` | string | no | Dataset purpose tag (default: "fine-tune") | | `--schema ` | string | no | Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "decision" (state/questions), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record. | | `--no-validate` | switch | no | Skip the local JSONL pre-flight check (not recommended) | | `--full-validate` | switch | no | JSON.parse every line instead of sampling (slower) | @@ -167,10 +167,6 @@ bl dataset upload --file decision.jsonl --schema decision bl dataset upload --file audio.zip --schema tts ``` -```bash -bl dataset upload --file eval.jsonl --purpose evaluation -``` - ```bash bl dataset upload --file train.jsonl --full-validate ``` diff --git a/skills/bailian-finetune/reference/finetune.md b/skills/bailian-finetune/reference/finetune.md index 81234fadc..afef8bcd0 100644 --- a/skills/bailian-finetune/reference/finetune.md +++ b/skills/bailian-finetune/reference/finetune.md @@ -891,8 +891,8 @@ bl finetune price --base-model qwen3-8b --datasets file-ft-xxx --training-type c | `--max-split-val-dataset-sample ` | number | no | Maximum automatically split validation samples | | `--data-augmentation ` | string | no | Mix platform training data (true or false) | | `--training-type ` | string | no | Training type: sft \| sft-lora \| dpo \| dpo-lora \| cpt (default: sft-lora). Mapping to the server happens at the interface boundary (e.g. sft-lora -> efficient_sft, dpo -> dpo_full). | -| `--n-epochs ` | number | no | Number of epochs (default: 3) | -| `--batch-size ` | number | no | Per-device batch size (clamped to [8, 1024]). Auto-set to 8 for small datasets (<100KB) | +| `--n-epochs ` | number | no | Number of epochs; valid values are checked against the selected model | +| `--batch-size ` | number | no | Per-device batch size; range and step are checked against the selected model | | `--learning-rate ` | string | no | Learning rate as a string to preserve precision (e.g. "1.6e-5") | | `--max-length ` | number | no | Max sequence length | | `--api-key ` | string | no | API key | @@ -905,7 +905,7 @@ bl finetune price --base-model qwen3-8b --datasets file-ft-xxx --training-type c - --datasets / --validations accept either file-ids (from `dataset upload`) or local paths. Local paths are validated and uploaded first, then their file-ids are submitted — a one-step upload-and-train. - Training-type values use the `` / `-lora` convention: sft (full) | sft-lora (LoRA) | dpo (full) | dpo-lora (LoRA) | cpt. These map to the server's training_type at the interface boundary, so the rest of the CLI never sees the raw server strings. - Before submitting (non dry-run) the job, the model's training capability is checked via listFoundationModels (no console login required); an unsupported training type fails fast with the list the model actually supports. -- n_epochs defaults to 3. Other hyper-parameters are platform defaults unless set. +- Before upload, the command queries the selected model and training type's parameter schema. Explicit overrides must satisfy its editability, type, enum, range, and step constraints; omitted values use platform defaults. - Learning rate is forwarded as a string to avoid JSON-number precision loss. - Pre-submit gate: if the training dataset's sample count is not greater than batch_size, the job is rejected before upload or quota consumption (the platform would otherwise fail ~10 min in, after data processing). From 87d78316add9a10f102adc1b8d078b25534b4fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=95=85=E7=92=83?= Date: Tue, 10 Nov 2026 12:27:01 +0800 Subject: [PATCH 2/2] chore(release): prepare 2.8.5 --- CHANGELOG.md | 10 ++++++++++ CHANGELOG.zh.md | 10 ++++++++++ packages/bailian-kb-dsh/package.json | 2 +- packages/bailian-memo-dsh/package.json | 2 +- packages/cli/package.json | 2 +- packages/commands/package.json | 2 +- packages/core/package.json | 2 +- packages/kscli/package.json | 2 +- packages/runtime/package.json | 2 +- skills/bailian-cli/SKILL.md | 2 +- skills/bailian-finetune/SKILL.md | 2 +- skills/bailian-gen/SKILL.md | 2 +- skills/bailian-knowledge/SKILL.md | 2 +- skills/bailian-managed-agent/SKILL.md | 2 +- skills/bailian-memory/SKILL.md | 2 +- skills/bailian-protocol/SKILL.md | 2 +- skills/bailian-sandbox/SKILL.md | 2 +- skills/bailian-web-search/SKILL.md | 2 +- 18 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e8baa15..978e42d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [2.8.5] - 2026-11-10 + +### Fixed + +- **Fine-tune creation**: Query the authoritative hyper-parameter schema before uploads, validate only explicit constrained overrides, and stop before uploading or submitting when the schema lookup or validation fails. Unspecified parameters are no longer filled or clamped locally. +- **Deployment deletion**: Prefer the explicit deployment plan over stale PTU-shaped fields and refuse deletion when the plan cannot be determined safely. +- **Thinking control**: Allow `bl text chat --enable-thinking=false` while preserving the existing omitted and bare-flag behavior; a disabled request no longer sends `thinking_budget`. +- **Model sample code**: Require an exact model match instead of returning sample code from another model in the same family. +- **Dataset upload help**: Remove the unsupported `evaluation` purpose example while keeping `fine-tune` as the default. + ## [2.8.4] - 2026-11-06 ### Added diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 044122e24..a88dec8ef 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,16 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [2.8.5] - 2026-11-10 + +### 修复 + +- **精调任务创建**:上传前查询权威超参配置,只校验用户显式覆盖且有明确约束的参数;查询或校验失败时不上传数据、不提交任务,未指定参数不再由本地填充或裁剪。 +- **部署删除**:显式部署计划优先于残留的 PTU 特征字段;无法可靠判断计划类型时拒绝提交删除请求。 +- **思考模式控制**:`bl text chat` 支持 `--enable-thinking=false`,同时保持省略参数和裸参数的原有行为;关闭思考时不再发送 `thinking_budget`。 +- **模型调用示例**:仅返回精确匹配型号的示例代码,不再回退到同系列其他型号。 +- **数据集上传帮助**:删除不受支持的 `evaluation` purpose 示例,默认值仍为 `fine-tune`。 + ## [2.8.4] - 2026-11-06 ### 新增 diff --git a/packages/bailian-kb-dsh/package.json b/packages/bailian-kb-dsh/package.json index 8b36b17fc..915a8bad8 100644 --- a/packages/bailian-kb-dsh/package.json +++ b/packages/bailian-kb-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-kb-dsh", - "version": "2.8.4", + "version": "2.8.5", "description": "Bailian knowledge-base tools for DeepSeek Harness: kb_search and kb_chat over the DashScope RAG API, plus the bl CLI management skill.", "keywords": [ "alibaba-cloud", diff --git a/packages/bailian-memo-dsh/package.json b/packages/bailian-memo-dsh/package.json index 649e8b35c..29e62c52f 100644 --- a/packages/bailian-memo-dsh/package.json +++ b/packages/bailian-memo-dsh/package.json @@ -1,6 +1,6 @@ { "name": "bailian-memo-dsh", - "version": "2.8.4", + "version": "2.8.5", "description": "Bailian personal memory for DeepSeek Harness: automatic recall, silent curation, and user-controlled remember/forget.", "keywords": [ "alibaba-cloud", diff --git a/packages/cli/package.json b/packages/cli/package.json index 31df47954..747805895 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "2.8.4", + "version": "2.8.5", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/commands/package.json b/packages/commands/package.json index 71b0ad52b..1c25f0649 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "2.8.4", + "version": "2.8.5", "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/core/package.json b/packages/core/package.json index 8acdc7a9d..e5350fc9d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "2.8.4", + "version": "2.8.5", "description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 9fac71f1e..137da2e0c 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "2.8.4", + "version": "2.8.5", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index fd5c58310..7238cfda2 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "2.8.4", + "version": "2.8.5", "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 2f0e49cff..4f94912b7 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index 3d4f1f39d..e19677715 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 64f55592c..957058a0e 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-knowledge/SKILL.md b/skills/bailian-knowledge/SKILL.md index 09093c130..34682beec 100644 --- a/skills/bailian-knowledge/SKILL.md +++ b/skills/bailian-knowledge/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-knowledge metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 3082558fe..9cbab96bf 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-memory/SKILL.md b/skills/bailian-memory/SKILL.md index 65c11181a..b5839cde9 100644 --- a/skills/bailian-memory/SKILL.md +++ b/skills/bailian-memory/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-memory metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index 1ea125e44..7292b5412 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index fc2bd3f69..0d23fd1df 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-sandbox metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 38c72d384..1da6ce5ea 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-web-search metadata: - version: "2.8.4" + version: "2.8.5" requires: bins: ["bl"] description: >-