@@ -2,33 +2,23 @@ import {
22 defineCommand ,
33 detectOutputFormat ,
44 createDeployment ,
5- listDeployableModels ,
65 BailianError ,
76 ExitCode ,
87 type Config ,
98 type GlobalFlags ,
109} from "bailian-cli-core" ;
1110import { failIfMissing , promptConfirm } from "../../output/prompt.ts" ;
1211import { emitResult , emitBare } from "../../output/output.ts" ;
12+ import { pickPlanStrategy } from "./plans.ts" ;
1313
1414/**
1515 * `bl deploy create` — create a model deployment.
1616 *
17- * Plan handling:
18- * - lora (default): Token-billed; `capacity` is required by API but ignored.
19- * - ptu: Token-billed (provisioned throughput); requires
20- * `ptu_capacity` {input_tpm, output_tpm}. The doc says
21- * these default to 10000/1000 when omitted, but the platform
22- * currently rejects creation without them ("Miss ptu capacity
23- * info"), so the CLI requires --input-tpm/--output-tpm for ptu.
24- * - mu: Unit-based; requires `capacity`, `billing_method` and a
25- * `template_id`. `billing_method` defaults to "POST_PAY"
26- * (the only value the platform currently supports). If
27- * --template-id is omitted, the CLI auto-picks the template
28- * returned by GET /deployments/models whose charge_type
29- * matches billing_method; --capacity defaults to that
30- * template's `capacity_unit_per_instance` (the smallest
31- * valid multiple of base_capacity).
17+ * Plan-specific behaviour (required flags / body assembly / confirm rows /
18+ * auto-pick) lives in `plans.ts` (`PlanStrategy` + `STRATEGIES`). This file
19+ * only handles the shared envelope: argument parsing, dispatch, dry-run,
20+ * confirmation prompt, and result formatting. Adding a new plan = one entry
21+ * in the strategy table; nothing here changes.
3222 *
3323 * `--model` (model identifier) and `--name` (console display name) are required.
3424 */
@@ -116,119 +106,22 @@ export default defineCommand({
116106 if ( ! name ) failIfMissing ( "name" , "bl deploy create --model <model_name> --name <display_name>" ) ;
117107
118108 const plan = ( flags . plan as string | undefined ) || "lora" ;
119- let templateId = flags . templateId as string | undefined ;
120- const inputTpm = flags . inputTpm as number | undefined ;
121- const outputTpm = flags . outputTpm as number | undefined ;
122- const thinkingOutputTpm = flags . thinkingOutputTpm as number | undefined ;
123- // mu-only: capacity (resource units) and billing_method (default POST_PAY,
124- // the only value the platform currently supports per the deploy doc).
125- let capacity = flags . capacity as number | undefined ;
126- const billingMethod = ( flags . billingMethod as string | undefined ) || "POST_PAY" ;
127-
128109 const format = detectOutputFormat ( config . output ) ;
129110
130- // Validate plan. The catalog lists plan names like `ptu_v2`, but the create
131- // endpoint only accepts `ptu` — so reject anything outside the supported set
132- // with a clear message instead of letting the API fail with a vague error.
133- const SUPPORTED_PLANS = [ "lora" , "ptu" , "mu" ] as const ;
134- if ( ! ( SUPPORTED_PLANS as readonly string [ ] ) . includes ( plan ) ) {
135- throw new BailianError (
136- `Unsupported plan "${ plan } ". Supported plans: ${ SUPPORTED_PLANS . join ( ", " ) } .` ,
137- ExitCode . USAGE ,
138- ) ;
139- }
140-
141- // For plan=ptu, require throughput limits. The platform rejects creation
142- // without an explicit ptu_capacity ("Miss ptu capacity info") even though
143- // the doc lists 10000/1000 defaults.
144- if ( plan === "ptu" ) {
145- if ( inputTpm === undefined )
146- failIfMissing (
147- "input-tpm" ,
148- "bl deploy create --plan ptu --model <m> --name <n> --input-tpm <n> --output-tpm <n>" ,
149- ) ;
150- if ( outputTpm === undefined )
151- failIfMissing (
152- "output-tpm" ,
153- "bl deploy create --plan ptu --model <m> --name <n> --input-tpm <n> --output-tpm <n>" ,
154- ) ;
155- }
156-
157- // For plan=mu, auto-pick the template (preferring the one whose charge_type
158- // matches billing_method) and default capacity to the template's unit.
159- // Skip the catalog lookup when the user supplies --template-id explicitly —
160- // the model may be a fine-tuned custom model not present in the base
161- // catalog, and the lookup would otherwise throw a spurious error.
162- let autoPickedTemplate = false ;
163- if ( plan === "mu" && ! config . dryRun && ! templateId ) {
164- try {
165- const resp = await listDeployableModels ( config , {
166- modelSource : "base" ,
167- pageSize : 100 ,
168- version : "v1.0" ,
169- } ) ;
170- const payload = resp . output ?? resp . data ;
171- const target = ( payload ?. models ?? [ ] ) . find ( ( m ) => m . model_name === model ) ;
172- const muPlan = target ?. plans ?. find ( ( p ) => p . plan === "mu" ) ;
173- const templates = muPlan ?. templates ?? [ ] ;
174- if ( templates . length === 0 ) {
175- throw new BailianError (
176- `No mu-plan template found for model "${ model } ". ` +
177- `Run \`bl deploy models --source base\` to inspect available models, ` +
178- `or pass --template-id explicitly.` ,
179- ExitCode . USAGE ,
180- ) ;
181- }
182- // POST_PAY → post_paid template; fall back to the first available.
183- const wantChargeType = billingMethod === "POST_PAY" ? "post_paid" : "pre_paid" ;
184- const picked = templates . find ( ( t ) => t . charge_type === wantChargeType ) ?? templates [ 0 ] ;
185- if ( ! picked ?. template_id ) {
186- throw new BailianError (
187- `No mu-plan template found for model "${ model } ". ` +
188- `Run \`bl deploy models --source base\` to inspect available models, ` +
189- `or pass --template-id explicitly.` ,
190- ExitCode . USAGE ,
191- ) ;
192- }
193- templateId = picked . template_id ;
194- autoPickedTemplate = true ;
195- // capacity must be a multiple of base_capacity; default to the template's
196- // unit (capacity_unit_per_instance) which is the smallest valid value.
197- if ( capacity === undefined ) {
198- capacity = picked . roles ?. unified ?. capacity_unit_per_instance ?? 1 ;
199- }
200- } catch ( e ) {
201- if ( e instanceof BailianError ) throw e ;
202- throw new BailianError (
203- `Failed to auto-pick template for plan=mu: ${ ( e as Error ) . message } . ` +
204- `Pass --template-id explicitly.` ,
205- ExitCode . USAGE ,
206- ) ;
207- }
208- }
209-
111+ // Plan-specific behaviour is owned by `plans.ts`. The strategy:
112+ // 1. Validates required flags (USAGE error if missing).
113+ // 2. Resolves the body fragment + confirm rows (mu may auto-pick a
114+ // template from the deployable-models catalog).
115+ // Anything outside the strategy table is rejected with a USAGE error.
116+ const strategy = pickPlanStrategy ( plan ) ;
117+ strategy . validateFlags ( flags ) ;
118+ const resolved = await strategy . resolve ( { config, flags, model : model ! , name : name ! } ) ;
210119 const body : Record < string , unknown > = {
211120 model_name : model ! ,
212121 name : name ! ,
213122 plan,
123+ ...resolved . body ,
214124 } ;
215- if ( plan === "ptu" ) {
216- const ptuCapacity : Record < string , number > = {
217- input_tpm : inputTpm ! ,
218- output_tpm : outputTpm ! ,
219- } ;
220- if ( thinkingOutputTpm !== undefined ) ptuCapacity . thinking_output_tpm = thinkingOutputTpm ;
221- body . ptu_capacity = ptuCapacity ;
222- } else if ( plan === "mu" ) {
223- // mu requires capacity, billing_method and template_id (auto-picked above
224- // if --template-id was not supplied).
225- body . capacity = capacity ?? 1 ;
226- body . billing_method = billingMethod ;
227- if ( templateId ) body . template_id = templateId ;
228- } else {
229- // lora: capacity required by API but ignored (per the working example).
230- body . capacity = 1 ;
231- }
232125
233126 if ( config . dryRun ) {
234127 emitResult ( { action : "deploy.create" , body } , format ) ;
@@ -240,22 +133,9 @@ export default defineCommand({
240133 "Create deployment:" ,
241134 ` model: ${ model } ` ,
242135 ` name: ${ name } ` ,
243- ` plan: ${ plan } ${ plan === "lora" ? " (Token-billed)" : plan === "ptu" ? " (Token-billed, provisioned throughput)" : "" } ` ,
136+ ` plan: ${ plan } ${ resolved . planLabelSuffix ?? "" } ` ,
137+ ...resolved . confirmRows ,
244138 ] ;
245- if ( templateId ) {
246- const hint = autoPickedTemplate ? " (auto-picked)" : "" ;
247- lines . push ( ` template_id: ${ templateId } ${ hint } ` ) ;
248- }
249- if ( plan === "mu" ) {
250- lines . push ( ` capacity: ${ capacity ?? 1 } ` ) ;
251- lines . push ( ` billing_method: ${ billingMethod } ` ) ;
252- }
253- if ( plan === "ptu" ) {
254- lines . push ( ` input_tpm: ${ inputTpm } ` ) ;
255- lines . push ( ` output_tpm: ${ outputTpm } ` ) ;
256- if ( thinkingOutputTpm !== undefined )
257- lines . push ( ` thinking_output_tpm: ${ thinkingOutputTpm } ` ) ;
258- }
259139 process . stderr . write ( lines . join ( "\n" ) + "\n" ) ;
260140 const ok = await promptConfirm ( { message : "Proceed?" , initialValue : true } ) ;
261141 if ( ! ok ) {
0 commit comments