Skip to content

Commit 78e6993

Browse files
authored
Merge pull request #162 from modelstudioai/feat/model-command-update
feat: update model quota limit & add model permission command
2 parents f7d3250 + 79a0d2d commit 78e6993

33 files changed

Lines changed: 1458 additions & 693 deletions

File tree

packages/cli/src/commands.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ import {
5353
modelList,
5454
workspaceList,
5555
quotaList,
56-
quotaRequest,
56+
quotaUpdate,
5757
quotaHistory,
5858
quotaCheck,
59+
permissionList,
60+
permissionGrant,
61+
permissionRevoke,
5962
datasetUpload,
6063
datasetList,
6164
datasetGet,
@@ -174,9 +177,12 @@ export const commands: Record<string, AnyCommand> = {
174177
"model list": modelList,
175178
"workspace list": workspaceList,
176179
"quota list": quotaList,
177-
"quota request": quotaRequest,
180+
"quota update": quotaUpdate,
178181
"quota history": quotaHistory,
179182
"quota check": quotaCheck,
183+
"permission list": permissionList,
184+
"permission grant": permissionGrant,
185+
"permission revoke": permissionRevoke,
180186
"dataset upload": datasetUpload,
181187
"dataset list": datasetList,
182188
"dataset get": datasetGet,
@@ -235,3 +241,13 @@ export const commands: Record<string, AnyCommand> = {
235241
"managed-agent session events": managedAgentSessionEvents,
236242
"managed-agent skill-list": managedAgentSkillList,
237243
};
244+
245+
/**
246+
* Runtime-only aliases for renamed commands: dispatched by the CLI (merged in
247+
* main.ts) but kept out of the canonical map so generate-reference.ts only
248+
* documents the canonical path.
249+
*/
250+
export const commandAliases: Record<string, AnyCommand> = {
251+
// Pre-migration name of "quota update".
252+
"quota request": quotaUpdate,
253+
};

packages/cli/src/main.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createCli } from "bailian-cli-runtime";
2-
import { commands } from "./commands.ts";
2+
import { commandAliases, commands } from "./commands.ts";
33
import { commandPackPolicy } from "./command-pack-policy.ts";
44
import pkg from "../package.json" with { type: "json" };
55

@@ -10,11 +10,14 @@ const quickStartTasks = [
1010
"Help me analyze this video and write a Xiaohongshu-style post",
1111
] as const;
1212

13-
void createCli(commands, {
14-
binName: "bl",
15-
version: pkg.version,
16-
clientName: "bailian-cli",
17-
npmPackage: "bailian-cli",
18-
quickStartTasks,
19-
commandPacks: commandPackPolicy,
20-
}).run();
13+
void createCli(
14+
{ ...commands, ...commandAliases },
15+
{
16+
binName: "bl",
17+
version: pkg.version,
18+
clientName: "bailian-cli",
19+
npmPackage: "bailian-cli",
20+
quickStartTasks,
21+
commandPacks: commandPackPolicy,
22+
},
23+
).run();

packages/commands/src/commands/finetune/capability.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,29 @@
11
import {
22
defineCommand,
33
detectOutputFormat,
4-
fetchModelList,
4+
fetchModelListAll,
55
fetchModelCapability,
66
listSupportedTrainingTypes,
77
modelSupportsTrainingType,
88
isTrainingTypeCli,
99
trainingTypeMethodVariant,
1010
TRAINING_TYPES_CLI,
11-
callConsoleGateway,
12-
effectiveConsoleGatewayConfig,
11+
anonymousConsoleCall,
1312
UsageError,
1413
type Settings,
1514
type ModelCapability,
1615
type FlagsDef,
1716
} from "bailian-cli-core";
1817
import { emitResult, emitBare } from "bailian-cli-runtime";
1918

20-
const PAGE_SIZE = 50;
21-
2219
/**
2320
* Page through every foundation-model page (listFoundationModels, public — no
2421
* console login needed, so the gateway is called anonymously). Returns raw
2522
* records so capability fields (`supports` / `trainingTypes`) are preserved
2623
* for filtering.
2724
*/
2825
async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapability[]> {
29-
const eff = effectiveConsoleGatewayConfig(settings);
30-
const call = (api: string, data: Record<string, unknown>) =>
31-
callConsoleGateway(
32-
{ region: eff.consoleRegion, site: eff.consoleSite, switchAgent: eff.consoleSwitchAgent },
33-
settings.timeout,
34-
{ api, data },
35-
);
36-
const first = await fetchModelList(call, { pageNo: 1, pageSize: PAGE_SIZE });
37-
const all = [...first.models];
38-
const totalPages = Math.ceil(first.total / PAGE_SIZE);
39-
for (let pageNo = 2; pageNo <= totalPages; pageNo++) {
40-
const result = await fetchModelList(call, { pageNo, pageSize: PAGE_SIZE });
41-
all.push(...result.models);
42-
}
26+
const all = await fetchModelListAll(anonymousConsoleCall(settings));
4327
return all as ModelCapability[];
4428
}
4529

packages/commands/src/commands/model/list.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
anonymousConsoleCall,
23
defineCommand,
34
detectOutputFormat,
45
fetchModelDetail,
@@ -290,7 +291,7 @@ function printPredictConfigTable(entries: PredictConfigEntry[]): void {
290291

291292
export default defineCommand({
292293
description: "Browse model families or show detailed model info in the Bailian model marketplace",
293-
auth: "console",
294+
auth: "none",
294295
usageArgs:
295296
"[--model <model>] [--page <n>] [--page-size <n>] [--provider <p>] [--capability <c>] [--feature <f>] [--enrich]",
296297
flags: LIST_FLAGS,
@@ -302,10 +303,14 @@ export default defineCommand({
302303
"--model qwen-max --enrich --output json",
303304
"--feature function-calling --output json",
304305
],
306+
notes: [
307+
"Both the catalog and --enrich parameter-schema endpoints are public — no console login needed.",
308+
],
305309
async run(ctx) {
306310
const { settings, flags } = ctx;
307311
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
308312
const modelKey = flags.model;
313+
const call = anonymousConsoleCall(settings);
309314

310315
// ── Detail mode ──
311316
if (modelKey) {
@@ -316,7 +321,7 @@ export default defineCommand({
316321
return;
317322
}
318323

319-
const detail = await fetchModelDetail(ctx.client.console.bind(ctx.client), modelKey);
324+
const detail = await fetchModelDetail(call, modelKey);
320325

321326
if (!detail) {
322327
emitBare(`Model "${modelKey}" not found.`);
@@ -328,10 +333,7 @@ export default defineCommand({
328333
await Promise.all(
329334
trunkItems.map(async (item) => {
330335
if (!item.model) return;
331-
const config = await fetchPredictConfig(
332-
ctx.client.console.bind(ctx.client),
333-
item.model,
334-
);
336+
const config = await fetchPredictConfig(call, item.model);
335337
if (config) item.predictConfig = config;
336338
}),
337339
);
@@ -361,7 +363,7 @@ export default defineCommand({
361363
return;
362364
}
363365

364-
const { total, groups } = await fetchModelGroups(ctx.client.console.bind(ctx.client), params);
366+
const { total, groups } = await fetchModelGroups(call, params);
365367

366368
if (format === "json") {
367369
emitResult(formatBrowseJson(groups, total), format);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { defineCommand } from "bailian-cli-core";
2+
import { runPermissionChange, validatePermissionChange } from "./shared.ts";
3+
4+
export default defineCommand({
5+
description: "Grant model permissions (inference / finetune / deploy)",
6+
auth: "apiKey",
7+
usageArgs: "--model <models> [--action <actions>] | --all",
8+
flags: {
9+
model: {
10+
type: "string",
11+
valueHint: "<models>",
12+
description: "Model ID(s), comma-separated (max 20)",
13+
},
14+
action: {
15+
type: "string",
16+
valueHint: "<actions>",
17+
description:
18+
"Permission action(s), comma-separated: inference, finetune, deploy (default: inference)",
19+
},
20+
all: {
21+
type: "switch",
22+
description:
23+
"One-key grant inference for all models in the workspace (including future ones)",
24+
},
25+
},
26+
exampleArgs: [
27+
"--model qwen-plus",
28+
"--model qwen-plus,qwen3-max --action inference,finetune",
29+
"--all",
30+
"--model qwen-plus --dry-run --output json",
31+
],
32+
notes: [
33+
"Grants apply to the business workspace your API key belongs to.",
34+
"--all maps to the server one-key switch (access_all_entities: OPEN) and only covers inference.",
35+
"Actions you omit keep their current grants (server-side tri-state patch).",
36+
],
37+
validate: (flags) => validatePermissionChange(flags),
38+
async run(ctx) {
39+
await runPermissionChange(ctx, ctx.flags, true);
40+
},
41+
});
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { defineCommand, detectOutputFormat, modelsPermissionsPath } from "bailian-cli-core";
2+
import { emitResult, renderBoxTable } from "bailian-cli-runtime";
3+
import { buildQuery } from "../shared/params.ts";
4+
5+
// ---------------------------------------------------------------------------
6+
// Types — mirror GET /api/v1/models/permissions
7+
// ---------------------------------------------------------------------------
8+
9+
interface PermissionDetail {
10+
inference?: boolean | null;
11+
fine_tune?: boolean | null;
12+
deploy?: boolean | null;
13+
}
14+
15+
interface ModelPermission {
16+
model: string;
17+
name?: string;
18+
permissions?: PermissionDetail;
19+
}
20+
21+
interface PermissionsResponse {
22+
output?: {
23+
total?: number;
24+
page_no?: number;
25+
page_size?: number;
26+
permissions?: ModelPermission[];
27+
};
28+
request_id?: string;
29+
}
30+
31+
// ---------------------------------------------------------------------------
32+
// Formatters
33+
// ---------------------------------------------------------------------------
34+
35+
/** Tri-state permission cell: true → yes, false → no, null/undefined → "-". */
36+
function formatGrant(granted: boolean | null | undefined): string {
37+
if (granted == null) return "-";
38+
return granted ? "yes" : "no";
39+
}
40+
41+
function printTable(permissions: ModelPermission[], total: number, emptyHint: string): void {
42+
if (permissions.length === 0) {
43+
process.stdout.write(`No model permissions found.\n${emptyHint}\n`);
44+
return;
45+
}
46+
const headers = ["Model", "Name", "Inference", "Fine-tune", "Deploy"];
47+
const rows = permissions.map((entry) => [
48+
entry.model,
49+
entry.name ?? "-",
50+
formatGrant(entry.permissions?.inference),
51+
formatGrant(entry.permissions?.fine_tune),
52+
formatGrant(entry.permissions?.deploy),
53+
]);
54+
const lines = renderBoxTable({
55+
headers,
56+
rows,
57+
align: ["left", "left", "right", "right", "right"],
58+
});
59+
for (const line of lines) process.stdout.write(line + "\n");
60+
process.stdout.write(`\nTotal: ${total}\n`);
61+
}
62+
63+
// ---------------------------------------------------------------------------
64+
// Command
65+
// ---------------------------------------------------------------------------
66+
67+
export default defineCommand({
68+
description: "List model permissions (inference / fine-tune / deploy) in the workspace",
69+
auth: "apiKey",
70+
usageArgs: "[--scope <scope>] [--model <model>] [--name <name>] [--page <n>] [--page-size <n>]",
71+
flags: {
72+
scope: {
73+
type: "string",
74+
valueHint: "<scope>",
75+
choices: ["authorized", "authorizable"] as const,
76+
description: "Authorization scope: authorizable (default, full catalog), authorized",
77+
},
78+
model: {
79+
type: "string",
80+
valueHint: "<model>",
81+
description: "Model ID (exact match)",
82+
},
83+
name: {
84+
type: "string",
85+
valueHint: "<name>",
86+
description: "Fuzzy search by model name or ID",
87+
},
88+
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
89+
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 20)" },
90+
},
91+
exampleArgs: [
92+
"",
93+
"--model qwen-plus",
94+
"--scope authorized",
95+
"--name qwen --page-size 50",
96+
"--output text",
97+
],
98+
notes: [
99+
"Default scope is `authorizable` (the full grantable catalog); use `--scope authorized` to see only models already granted.",
100+
"Output defaults to JSON; pass `--output text` for a table. Permission values are tri-state: true / false / null (never set).",
101+
"Values mirror the server's grant records as-is for the workspace bound to your API key. A model reporting false/null can still be callable (access may come from other channels); see the Model Studio authorization docs for the exact semantics.",
102+
],
103+
async run(ctx) {
104+
const { settings, flags } = ctx;
105+
const format = settings.outputExplicit ? detectOutputFormat(settings.output) : "json";
106+
const scope = flags.scope ?? "authorizable";
107+
108+
const query = {
109+
authorization_scope: scope.toUpperCase(),
110+
model: flags.model || undefined,
111+
name: flags.name || undefined,
112+
page_no: flags.page || 1,
113+
page_size: flags.pageSize || 20,
114+
};
115+
116+
if (settings.dryRun) {
117+
emitResult(
118+
{ endpoint: ctx.client.url(modelsPermissionsPath()), method: "GET", query },
119+
format,
120+
);
121+
return;
122+
}
123+
124+
const resp = await ctx.client.requestJson<PermissionsResponse>({
125+
path: modelsPermissionsPath() + buildQuery(query),
126+
});
127+
const permissions = resp.output?.permissions ?? [];
128+
const total = resp.output?.total ?? permissions.length;
129+
130+
if (format === "json") {
131+
emitResult({ items: permissions, total }, format);
132+
return;
133+
}
134+
135+
// The default authorized view is empty until something is granted — point
136+
// at the authorizable catalog instead of ending with a bare "nothing".
137+
const binName = ctx.identity.binName;
138+
const emptyHint =
139+
scope === "authorized"
140+
? `Nothing granted yet in this workspace. Browse grantable models with \`${binName} permission list --scope authorizable\`, then grant with \`${binName} permission grant --model <model>\`.`
141+
: `Adjust --name/--model filters, or check pagination with --page/--page-size.`;
142+
143+
printTable(permissions, total, emptyHint);
144+
},
145+
});

0 commit comments

Comments
 (0)