Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: change output & table name into English by gujieye · Pull Request #59 · modelstudioai/cli · GitHub
Skip to content
Merged
438 changes: 438 additions & 0 deletions docs/plans/finetune-deploy-mvp.md

Large diffs are not rendered by default.

113 changes: 66 additions & 47 deletions packages/cli/src/commands/advisor/recommend.ts
Original file line numberDiff line numberDiff 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 DownExpand 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
15 changes: 10 additions & 5 deletions packages/cli/src/commands/quota/history.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,11 +159,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@@ -172,6 +167,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
20 changes: 19 additions & 1 deletion packages/cli/src/commands/quota/list.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -218,7 +218,25 @@ export default defineCommand({
}

if (format === "json") {
emitResult(models, format);
const items = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];

const defaultRPM = calculateRPM(modelDefault);
const defaultTPM = calculateTPM(modelDefault);
const currentRPM = calculateRPM(userSpec, modelDefault?.count_limit_period) || defaultRPM;
const currentTPM = calculateTPM(userSpec, modelDefault?.usage_limit_period) || defaultTPM;
const maxTPM = defaultTPM * 2;

return {
model: m.model,
rpm: currentRPM > 0 ? currentRPM : null,
tpm: currentTPM > 0 ? currentTPM : null,
maxTPM: maxTPM > 0 ? maxTPM : null,
};
});
emitResult(items, format);
return;
}

Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/commands/usage/free.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,11 +297,6 @@ export default defineCommand({
}),
]);

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

const allQuotas = extractQuotas(quotaResult);
let quotas = modelFlag
? allQuotas
Expand All@@ -322,14 +317,44 @@ export default defineCommand({
quotas.sort((a, b) => (a.quotaValidityPeriod ?? 0) - (b.quotaValidityPeriod ?? 0));
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

if (format === "json") {
const items = quotas.map((quota) => {
const hasQuota = quota.quotaInitTotal != null && quota.quotaTotal != null;
const used = hasQuota ? quota.quotaInitTotal - quota.quotaTotal : 0;
const stopStatus = stopMap.get(quota.model);
const autoStop =
quota.quotaStatus === "UNKNOWN"
? "unsupported"
: stopStatus === true
? true
: stopStatus === false
? false
: null;
return {
model: quota.model,
type: typeMap.get(quota.model) || null,
remaining: hasQuota ? quota.quotaTotal : null,
total: hasQuota ? quota.quotaInitTotal : null,
usagePercent:
hasQuota && quota.quotaInitTotal > 0
? Math.round((used / quota.quotaInitTotal) * 1000) / 10
: null,
expires: quota.quotaValidityPeriod ? formatDate(quota.quotaValidityPeriod) : null,
autoStop,
};
});
emitResult(items, format);
return;
}

if (quotas.length === 0) {
process.stdout.write("No free-tier quota found.\n");
return;
}

const stopStatuses = extractFreeTierOnlyStatuses(stopResult);
const stopMap = new Map(stopStatuses.map((status) => [status.model, status.freeTierOnly]));

printTable(quotas, stopMap, typeMap, config.noColor);
},
});
Loading