Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,41 @@ function formatContextWindow(tokens: number): string {
}

const MODALITY_LABELS: Record<string, string> = {
Text: "文本",
Image: "图片",
Video: "视频",
Audio: "音频",
Text: "Text",
Image: "Image",
Video: "Video",
Audio: "Audio",
};
const CAPABILITY_LABELS: Record<string, string> = {
TG: "文本生成",
VU: "视觉理解",
IG: "图像生成",
VG: "视频生成",
TTS: "语音合成",
ASR: "语音识别",
Reasoning: "推理",
TG: "Text Gen",
VU: "Vision",
IG: "Image Gen",
VG: "Video Gen",
TTS: "Text-to-Speech",
ASR: "Speech-to-Text",
Reasoning: "Reasoning",
};
const BUDGET_LABELS: Record<string, string> = {
low: "低成本优先",
medium: "适中",
high: "高投入",
low: "Cost-Effective",
medium: "Balanced",
high: "High Investment",
};
const QUALITY_LABELS: Record<string, string> = {
flagship: "旗舰优先",
balanced: "均衡",
"cost-optimized": "性价比优先",
flagship: "Flagship",
balanced: "Balanced",
"cost-optimized": "Value",
};
const PREFERENCE_MODE_LABELS: Record<string, string> = {
scoped: "限定范围",
comparison: "对比评估",
alternative: "替代推荐",
scoped: "Scoped",
comparison: "Comparison",
alternative: "Alternative",
};

function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;

const lines: string[] = [];
lines.push(colorize.cyan.bold("需求理解"));
lines.push(colorize.cyan.bold("Intent Analysis"));

if (intent.taskSummary) {
lines.push("");
Expand All @@ -72,48 +72,48 @@ function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {

if (intent.scenarioHints.length) {
lines.push("");
lines.push(`${colorize.dim("场景特征")} ${intent.scenarioHints.join(" · ")}`);
lines.push(`${colorize.dim("Scenario")} ${intent.scenarioHints.join(" · ")}`);
}

const inputLabels = intent.inputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
const outputLabels = intent.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod);
if (inputLabels.length || outputLabels.length) {
lines.push("");
const parts: string[] = [];
if (inputLabels.length) parts.push(`${colorize.dim("输入")} ${inputLabels.join(", ")}`);
if (outputLabels.length) parts.push(`${colorize.dim("输出")} ${outputLabels.join(", ")}`);
if (inputLabels.length) parts.push(`${colorize.dim("Input")} ${inputLabels.join(", ")}`);
if (outputLabels.length) parts.push(`${colorize.dim("Output")} ${outputLabels.join(", ")}`);
lines.push(parts.join(" "));
}

const capLabels = intent.requiredCapabilities.map((cap) => CAPABILITY_LABELS[cap] ?? cap);
if (capLabels.length) {
lines.push(`${colorize.dim("所需能力")} ${capLabels.join(", ")}`);
lines.push(`${colorize.dim("Capabilities")} ${capLabels.join(", ")}`);
}

const budgetLabel = BUDGET_LABELS[intent.budget] ?? intent.budget;
const qualityLabel = QUALITY_LABELS[intent.qualityPreference] ?? intent.qualityPreference;
lines.push("");
lines.push(
`${colorize.dim("预算倾向")} ${budgetLabel} ${colorize.dim("质量偏好")} ${qualityLabel}`,
`${colorize.dim("Budget")} ${budgetLabel} ${colorize.dim("Quality")} ${qualityLabel}`,
);

const preference = intent.modelPreference;
if (preference && preference.mode !== "unconstrained") {
lines.push("");
const modeLabel = PREFERENCE_MODE_LABELS[preference.mode] ?? preference.mode;
const prefParts = [colorize.dim("推荐模式") + ` ${colorize.yellow(modeLabel)}`];
const prefParts = [colorize.dim("Mode") + ` ${colorize.yellow(modeLabel)}`];
if (preference.targets?.length) {
prefParts.push(colorize.dim("目标") + ` ${preference.targets.join(", ")}`);
prefParts.push(colorize.dim("Targets") + ` ${preference.targets.join(", ")}`);
}
if (preference.excludes?.length) {
prefParts.push(colorize.dim("排除") + ` ${preference.excludes.join(", ")}`);
prefParts.push(colorize.dim("Excludes") + ` ${preference.excludes.join(", ")}`);
}
lines.push(prefParts.join(" "));
}

if (intent.segments?.length) {
lines.push("");
lines.push(colorize.dim("任务拆解"));
lines.push(colorize.dim("Pipeline"));
for (const [idx, segment] of intent.segments.entries()) {
const outMods = segment.outputModality.map((mod) => MODALITY_LABELS[mod] ?? mod).join(", ");
lines.push(
Expand All @@ -131,19 +131,19 @@ function formatIntentSummary(intent: IntentProfile, noColor: boolean): string {
});
}

const RECOMMEND_LABELS = ["最佳推荐", "次优选择", "备选参考"];
const RECOMMEND_LABELS = ["Best Pick", "Runner-Up", "Alternative"];

function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstance): string {
const labelColors = [colorize.green.bold, colorize.blue.bold, colorize.magenta.bold];
const colorFn = labelColors[index] ?? colorize.white.bold;
const label = RECOMMEND_LABELS[index] ?? `推荐 #${index + 1}`;
const label = RECOMMEND_LABELS[index] ?? `#${index + 1}`;

const lines: string[] = [];
lines.push(colorFn(`⬢ 推荐 #${index + 1} — ${label}`));
lines.push(colorFn(`⬢ #${index + 1} — ${label}`));
lines.push("");
lines.push(`${colorize.bold(rec.name)} ${colorize.dim(`(${rec.model})`)}`);
lines.push("");
lines.push(`${colorize.cyan("推荐理由")} ${rec.reason}`);
lines.push(`${colorize.cyan("Why")} ${rec.reason}`);

if (rec.highlights.length) {
lines.push("");
Expand All @@ -153,8 +153,8 @@ function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstanc
}

const meta: string[] = [];
if (rec.contextWindow) meta.push(`上下文 ${formatContextWindow(rec.contextWindow)}`);
if (rec.maxOutputTokens) meta.push(`最大输出 ${formatContextWindow(rec.maxOutputTokens)}`);
if (rec.contextWindow) meta.push(`Context ${formatContextWindow(rec.contextWindow)}`);
if (rec.maxOutputTokens) meta.push(`Max Output ${formatContextWindow(rec.maxOutputTokens)}`);
if (meta.length) {
lines.push("");
lines.push(colorize.dim(meta.join(" · ")));
Expand All @@ -163,7 +163,7 @@ function renderCard(rec: RecommendedModel, index: number, colorize: ChalkInstanc
const docLink = buildDocLink(rec.docUrl);
if (docLink) {
lines.push("");
lines.push(colorize.dim(`文档 ${docLink}`));
lines.push(colorize.dim(`Docs ${docLink}`));
}

return boxen(lines.join("\n"), {
Expand All @@ -183,7 +183,7 @@ function formatSingleResult(results: RecommendedModel[], noColor: boolean): stri
function formatPipelineResult(summary: string, steps: PipelineStep[], noColor: boolean): string {
const colorize = noColor ? new Chalk({ level: 0 }) : chalk;
const lines: string[] = [];
lines.push(` ${colorize.yellow.bold("⚡ 组合方案")} ${summary}`);
lines.push(` ${colorize.yellow.bold("⚡ Pipeline")} ${summary}`);

for (const [stepIdx, { step, recommendations, warnings }] of steps.entries()) {
lines.push("");
Expand Down Expand Up @@ -247,31 +247,31 @@ export default defineCommand({

if (!userInput.trim()) {
if (isInteractive({ nonInteractive: config.nonInteractive })) {
const hint = await promptText({ message: "描述你的需求:" });
const hint = await promptText({ message: "Describe your requirement:" });
if (!hint) {
process.stderr.write("已取消。\n");
process.stderr.write("Cancelled.\n");
process.exit(1);
}
userInput = hint;
} else {
failIfMissing("message", 'bl advisor recommend "你的需求"');
failIfMissing("message", 'bl advisor recommend "your requirement"');
}
}

const top = 3;
const format = detectOutputFormat(config.output);

const modelsOptions: GetModelsOptions = {
onPrepareStart: () => process.stderr.write("初始化中...\n"),
onPrepareStart: () => process.stderr.write("Initializing model data...\n"),
};
process.stderr.write("正在分析需求...\n");
process.stderr.write("Analyzing your request...\n");
const [allModels, intent] = await Promise.all([
getModels(config, modelsOptions),
analyzeIntent(config, userInput),
]);

if (intent.confidence === 0) {
process.stderr.write("需求分析超时,使用默认参数继续...\n");
process.stderr.write("Intent analysis timed out, using defaults...\n");
} else {
process.stderr.write("\n");
}
Expand All @@ -297,20 +297,39 @@ export default defineCommand({
}

// Stage 3: LLM Ranking
const spinner = createSpinner("正在推荐最佳模型...");
const spinner = createSpinner("Recommending best models...");
spinner.start();

const result = await rankModels(config, candidates, intent, userInput, top);

spinner.stop();

if (isEmptyResult(result)) {
emitBare("暂无满足该需求的模型。");
emitBare("No suitable models found for this request.");
return;
}

if (format !== "text") {
emitResult(result, format);
emitResult(
{
intent: {
taskSummary: intent.taskSummary,
scenarioHints: intent.scenarioHints,
complexity: intent.complexity,
inputModality: intent.inputModality,
outputModality: intent.outputModality,
requiredCapabilities: intent.requiredCapabilities,
budget: intent.budget,
qualityPreference: intent.qualityPreference,
modelPreference:
intent.modelPreference?.mode !== "unconstrained" ? intent.modelPreference : undefined,
segments: intent.segments,
},
result,
candidates: candidates.length,
},
format,
);
return;
}

Expand Down
29 changes: 11 additions & 18 deletions packages/cli/src/commands/quota/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ function formatRatio(usage: number, limit: number): string {
function getStatus(usage: number, limit: number): string {
if (limit <= 0) return "-";
const pct = (usage / limit) * 100;
if (pct >= 100) return "已限流";
if (pct >= 80) return "接近限流";
return "正常";
if (pct >= 100) return "Throttled";
if (pct >= 80) return "Near Limit";
return "Normal";
}

function getNestedRecord(
Expand Down Expand Up @@ -193,7 +193,6 @@ function printTable(rows: CheckRow[], noColor: boolean): void {
const yellow = noColor ? (t: string) => t : (t: string) => `\x1b[33m${t}\x1b[0m`;
const red = noColor ? (t: string) => t : (t: string) => `\x1b[31m${t}\x1b[0m`;

const headersCn = ["模型", "RPM 用量/限额", "TPM 用量/限额", "状态"];
const headersEn = ["Model", "RPM Usage/Limit", "TPM Usage/Limit", "Status"];

const tableRows = rows.map((r) => {
Expand All @@ -215,36 +214,30 @@ function printTable(rows: CheckRow[], noColor: boolean): void {
return;
}

const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...tableRows.map((r) => displayWidth(r.cells[col])),
),
const widths = headersEn.map((label, col) =>
Math.max(displayWidth(label), ...tableRows.map((r) => displayWidth(r.cells[col]))),
);

const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const headerLine = headersEn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");

process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");

const statusCol = 3;
for (const r of tableRows) {
const cells = r.cells.map((cell, col) => {
if (col === statusCol) {
if (cell === "已限流") return red(padEnd(cell, widths[col]));
if (cell === "接近限流") return yellow(padEnd(cell, widths[col]));
if (cell === "正常") return green(padEnd(cell, widths[col]));
if (cell === "Throttled") return red(padEnd(cell, widths[col]));
if (cell === "Near Limit") return yellow(padEnd(cell, widths[col]));
if (cell === "Normal") return green(padEnd(cell, widths[col]));
}
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}

process.stdout.write(dim(`\n共 ${rows.length} 个模型 (Total: ${rows.length})`) + "\n");
process.stdout.write(dim(`\nTotal: ${rows.length} models`) + "\n");
}

export default defineCommand({
Expand Down
32 changes: 15 additions & 17 deletions packages/cli/src/commands/quota/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ function printTable(records: LimitApplicationItem[], noColor: boolean, total: nu
const bold = noColor ? (t: string) => t : (t: string) => `\x1b[1m${t}\x1b[0m`;
const dim = noColor ? (t: string) => t : (t: string) => `\x1b[2m${t}\x1b[0m`;

const headersCn = ["模型", "Token 账号限流", "申请时间"];
const headersEn = ["Model", "Token Limit", "Applied At"];

const rows = records.map((r) => [
Expand All @@ -74,27 +73,21 @@ function printTable(records: LimitApplicationItem[], noColor: boolean, total: nu
formatDateTime(r.gmtCreate),
]);

const widths = headersCn.map((label, col) =>
Math.max(
displayWidth(label),
displayWidth(headersEn[col]),
...rows.map((row) => displayWidth(row[col])),
),
const widths = headersEn.map((label, col) =>
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);

const cnLine = headersCn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const enLine = headersEn.map((label, col) => dim(padEnd(label, widths[col]))).join(" ");
const headerLine = headersEn.map((label, col) => bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => dim("─".repeat(w))).join("──");

process.stdout.write(cnLine + "\n");
process.stdout.write(enLine + "\n");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");

for (const row of rows) {
process.stdout.write(row.map((cell, col) => padEnd(cell, widths[col])).join(" ") + "\n");
}

process.stdout.write(dim(`\n共 ${total} 条记录 (Total: ${total})`) + "\n");
process.stdout.write(dim(`\nTotal: ${total} records`) + "\n");
}

export default defineCommand({
Expand Down Expand Up @@ -161,11 +154,6 @@ export default defineCommand({
throw err;
}

if (format === "json") {
emitResult(result, format);
return;
}

const resp = extractResponseData(result as Record<string, unknown>);
let records = (resp.records as LimitApplicationItem[]) ?? [];
const total = (resp.items as number) ?? records.length;
Expand All @@ -174,6 +162,16 @@ export default defineCommand({
records = records.filter((r) => r.deployedModel === modelFilter);
}

if (format === "json") {
const items = records.map((r) => ({
model: r.deployedModel,
tokenLimit: r.usageLimit,
appliedAt: formatDateTime(r.gmtCreate),
}));
emitResult({ records: items, total: modelFilter ? records.length : total }, format);
return;
}

if (records.length === 0) {
process.stdout.write("No quota change history found.\n");
return;
Expand Down
Loading