feat: update monitor data & quato list

This commit is contained in:
故璃
2026-07-13 13:43:03 +08:00
parent 1a63fcdb5c
commit d37f4c07eb
3 changed files with 231 additions and 112 deletions
+66 -55
View File
@@ -4,8 +4,7 @@ import {
detectOutputFormat,
type Client,
} from "bailian-cli-core";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
import { ansi, emitResult, renderBoxTable } from "bailian-cli-runtime";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
const MONITOR_API = "zeldaEasy.bailian-telemetry.monitor.getMonitorData";
@@ -49,24 +48,6 @@ function calculateTPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): n
return Math.floor((item.usage_limit * 60) / period);
}
function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
function formatRatio(usage: number, limit: number): string {
if (limit <= 0) return "-";
const pct = Math.round((usage / limit) * 100);
return `${formatNumber(usage)}/${formatNumber(limit)} (${pct}%)`;
}
function getStatus(usage: number, limit: number): string {
if (limit <= 0) return "-";
const pct = (usage / limit) * 100;
if (pct >= 100) return "Rate Limited";
if (pct >= 80) return "Near limit";
return "Normal";
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
@@ -157,7 +138,11 @@ async function fetchMonitorData(
}
return { rpm, tpm };
} catch {
} catch (error) {
// Re-throw console authentication errors, but catch other errors
if (error instanceof Error && error.message?.includes("Console session")) {
throw error;
}
return { rpm: -1, tpm: -1 };
}
}
@@ -168,16 +153,24 @@ interface CheckRow {
rpmLimit: number;
tpmUsage: number;
tpmLimit: number;
rpmQuotaLeft: number | null;
tpmQuotaLeft: number | null;
rpmQuotaLabel: string | null;
tpmQuotaLabel: string | null;
}
function printTable(rows: CheckRow[]): void {
const color = ansi(process.stdout);
const headers = ["Model", "RPM Used/Limit", "TPM Used/Limit", "Status", "RPM Left", "TPM Left"];
const headers = ["Model", "RPM Usage/Limit", "TPM Usage/Limit", "Status"];
const rpmPercents = rows.map((r) => r.rpmQuotaLeft);
const rpmLabels = rows.map((r) => r.rpmQuotaLabel);
const tpmPercents = rows.map((r) => r.tpmQuotaLeft);
const tpmLabels = rows.map((r) => r.tpmQuotaLabel);
const tableRows = rows.map((r) => {
const rpmStr = r.rpmUsage < 0 ? "-" : formatRatio(r.rpmUsage, r.rpmLimit);
const tpmStr = r.tpmUsage < 0 ? "-" : formatRatio(r.tpmUsage, r.tpmLimit);
const rpmStr = r.rpmUsage < 0 ? "-" : `${r.rpmUsage}/${r.rpmLimit}`;
const tpmStr = r.tpmUsage < 0 ? "-" : `${r.tpmUsage}/${r.tpmLimit}`;
const maxPct = Math.max(
r.rpmLimit > 0 ? (r.rpmUsage / r.rpmLimit) * 100 : 0,
r.tpmLimit > 0 ? (r.tpmUsage / r.tpmLimit) * 100 : 0,
@@ -185,39 +178,34 @@ function printTable(rows: CheckRow[]): void {
const status =
r.rpmUsage < 0
? "-"
: getStatus(Math.max(r.rpmUsage, r.tpmUsage), Math.max(r.rpmLimit, r.tpmLimit));
return { cells: [r.model, rpmStr, tpmStr, status], maxPct };
: maxPct >= 100
? "Rate Limited"
: maxPct >= 80
? "Near limit"
: "Normal";
return [r.model, rpmStr, tpmStr, status, "", ""];
});
if (tableRows.length === 0) {
process.stdout.write("No models found.\n");
return;
}
const widths = headers.map((label, col) =>
Math.max(displayWidth(label), ...tableRows.map((r) => displayWidth(r.cells[col]))),
);
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => color.dim("─".repeat(w))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
const statusCol = 3;
for (const r of tableRows) {
const cells = r.cells.map((cell, col) => {
if (col === statusCol) {
if (cell === "Rate Limited") return color.red(padEnd(cell, widths[col]));
if (cell === "Near limit") return color.yellow(padEnd(cell, widths[col]));
if (cell === "Normal") return color.green(padEnd(cell, widths[col]));
const lines = renderBoxTable({
headers,
rows: tableRows,
align: ["left", "right", "right", "left", "left", "left"],
barColumns: [
{ index: 4, percents: rpmPercents, labels: rpmLabels, width: 15 },
{ index: 5, percents: tpmPercents, labels: tpmLabels, width: 15 },
],
cellColor: (rowIndex, colIndex, value) => {
if (colIndex === 3) {
// Status 列着色
if (value === "Rate Limited") return color.red(value);
if (value === "Near limit") return color.yellow(value);
if (value === "Normal") return color.green(value);
}
return padEnd(cell, widths[col]);
});
process.stdout.write(cells.join(" ") + "\n");
}
return undefined;
},
});
process.stdout.write(color.dim(`\nTotal: ${rows.length} models`) + "\n");
for (const line of lines) process.stdout.write(line + "\n");
}
export default defineCommand({
@@ -295,12 +283,35 @@ export default defineCommand({
const tpmLimit =
calculateTPM(userSpec, modelDefault?.usage_limit_period) || calculateTPM(modelDefault);
const rpmUsage = monitorResults[idx].rpm;
const tpmUsage = monitorResults[idx].tpm;
// RPM Quota Left = 1 - (rpmUsage / rpmLimit) in percentage
let rpmQuotaPercent: number | null = null;
let rpmQuotaLabel: string | null = null;
if (rpmUsage >= 0 && rpmLimit > 0) {
rpmQuotaPercent = Math.max(0, 100 - (rpmUsage / rpmLimit) * 100);
rpmQuotaLabel = rpmQuotaPercent.toFixed(1) + "%";
}
// TPM Quota Left = 1 - (tpmUsage / tpmLimit) in percentage
let tpmQuotaPercent: number | null = null;
let tpmQuotaLabel: string | null = null;
if (tpmUsage >= 0 && tpmLimit > 0) {
tpmQuotaPercent = Math.max(0, 100 - (tpmUsage / tpmLimit) * 100);
tpmQuotaLabel = tpmQuotaPercent.toFixed(1) + "%";
}
return {
model: m.model,
rpmUsage: monitorResults[idx].rpm,
rpmUsage,
rpmLimit,
tpmUsage: monitorResults[idx].tpm,
tpmUsage,
tpmLimit,
rpmQuotaLeft: rpmQuotaPercent,
tpmQuotaLeft: tpmQuotaPercent,
rpmQuotaLabel,
tpmQuotaLabel,
};
});
+147 -49
View File
@@ -1,8 +1,8 @@
import { defineCommand, BailianError, detectOutputFormat, type Client } from "bailian-cli-core";
import { ansi, emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
import { emitResult, renderBoxTable } from "bailian-cli-runtime";
const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
const MONITOR_API = "zeldaEasy.bailian-telemetry.monitor.getMonitorData";
interface QpmInfoItem {
count_limit: number;
@@ -18,6 +18,17 @@ interface ModelWithQpm {
qpmInfo?: Record<string, QpmInfoItem>;
}
interface MonitorPoint {
value: number;
timestamp: number;
}
interface MonitorMetric {
aggMethod: string;
metricName: string;
points: MonitorPoint[];
}
function calculateRPM(item: QpmInfoItem | undefined, fallbackPeriod?: number): number {
if (!item) return 0;
const period = item.count_limit_period || fallbackPeriod;
@@ -36,6 +47,58 @@ function formatNumber(num: number): string {
return num.toLocaleString("en-US");
}
async function fetchMonitorData(
client: Client,
modelName: string,
windowMinutes: number,
): Promise<{ rpm: number; tpm: number }> {
const now = Date.now();
const startTime = now - windowMinutes * 60 * 1000;
try {
const raw = await client.console(MONITOR_API, {
reqDTO: {
monitorType: "Advanced",
metricFilters: [
{ aggMethod: "sum_pm", metricName: "model_total_amount" },
{ aggMethod: "sum_pm", metricName: "model_call_count" },
],
labelFilters: {
resourceId: modelName,
resourceType: "model",
},
startTime,
endTime: now,
},
});
const resp = extractResponseData(raw as Record<string, unknown>);
const metrics = (resp.data ?? resp) as MonitorMetric[] | Record<string, unknown>;
if (!Array.isArray(metrics)) {
return { rpm: 0, tpm: 0 };
}
let rpm = 0;
let tpm = 0;
for (const metric of metrics) {
if (metric.aggMethod !== "sum_pm" || !metric.points?.length) continue;
const lastValue = metric.points[metric.points.length - 1].value ?? 0;
if (metric.metricName === "model_call_count") rpm = Math.round(lastValue);
if (metric.metricName === "model_total_amount") tpm = Math.round(lastValue);
}
return { rpm, tpm };
} catch (error) {
// Re-throw console authentication errors, but catch other errors
if (error instanceof Error && error.message?.includes("Console session")) {
throw error;
}
// Return -1 to indicate no data (same as check.ts for consistency)
return { rpm: -1, tpm: -1 };
}
}
function getNestedRecord(
obj: Record<string, unknown>,
key: string,
@@ -58,10 +121,7 @@ function extractResponseData(result: Record<string, unknown>): Record<string, un
return direct ?? data;
}
async function fetchAllModelsWithQpm(
client: Client,
onlySelfService: boolean,
): Promise<ModelWithQpm[]> {
async function fetchAllModelsWithQpm(client: Client): Promise<ModelWithQpm[]> {
const allModels: ModelWithQpm[] = [];
let pageNo = 1;
@@ -72,10 +132,8 @@ async function fetchAllModelsWithQpm(
group: false,
queryQpmInfo: true,
ignoreWorkspaceServiceSite: true,
supports: { selfServiceLimitIncrease: true },
};
if (onlySelfService) {
input.supports = { selfServiceLimitIncrease: true };
}
const raw = await client.console(MODEL_LIST_API, { input });
@@ -91,50 +149,38 @@ async function fetchAllModelsWithQpm(
return allModels;
}
function printTable(models: ModelWithQpm[]): void {
const color = ansi(process.stdout);
interface ListRow {
model: string;
rpm: string;
tpm: string;
maxTpm: string;
rpmQuotaLeft: number | null;
tpmQuotaLeft: number | null;
rpmQuotaLabel: string | null;
tpmQuotaLabel: string | null;
}
const headers = ["Model", "Req/min", "Token/min", "Max TPM"];
function printTable(rows: ListRow[]): void {
const headers = ["Model", "Req/min", "Token/min", "RPM Left", "TPM Left"];
const rows = models.map((m) => {
const qpm = m.qpmInfo;
const modelDefault = qpm?.["model-default"];
const userSpec = qpm?.["user-spec"];
const rpmPercents = rows.map((r) => r.rpmQuotaLeft);
const rpmLabels = rows.map((r) => r.rpmQuotaLabel);
const tpmPercents = rows.map((r) => r.tpmQuotaLeft);
const tpmLabels = rows.map((r) => r.tpmQuotaLabel);
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;
const tableRows = rows.map((r) => [r.model, r.rpm, r.tpm, "", ""]);
return [
m.model,
currentRPM > 0 ? formatNumber(currentRPM) : "-",
currentTPM > 0 ? formatNumber(currentTPM) : "-",
maxTPM > 0 ? formatNumber(maxTPM) : "-",
];
const lines = renderBoxTable({
headers,
rows: tableRows,
align: ["left", "right", "right", "left", "left"],
barColumns: [
{ index: 3, percents: rpmPercents, labels: rpmLabels, width: 15 },
{ index: 4, percents: tpmPercents, labels: tpmLabels, width: 15 },
],
});
if (rows.length === 0) {
process.stdout.write("No models found.\n");
return;
}
const widths = headers.map((label, col) =>
Math.max(displayWidth(label), ...rows.map((row) => displayWidth(row[col]))),
);
const headerLine = headers.map((label, col) => color.bold(padEnd(label, widths[col]))).join(" ");
const separator = widths.map((w) => color.dim("─".repeat(w))).join("──");
process.stdout.write(headerLine + "\n");
process.stdout.write(separator + "\n");
for (const row of rows) {
process.stdout.write(row.map((cell, col) => padEnd(cell, widths[col])).join(" ") + "\n");
}
process.stdout.write(color.dim(`\nTotal: ${models.length} models`) + "\n");
for (const line of lines) process.stdout.write(line + "\n");
}
export default defineCommand({
@@ -178,7 +224,7 @@ export default defineCommand({
return;
}
let models = await fetchAllModelsWithQpm(ctx.client, !showAll);
let models = await fetchAllModelsWithQpm(ctx.client);
if (modelFlag) {
const names = new Set(
@@ -216,6 +262,58 @@ export default defineCommand({
return;
}
printTable(models);
// For text output with gauges, we need monitor data
const monitorResults = await Promise.all(
models.map((m) => fetchMonitorData(ctx.client, m.model, 2)),
);
const rows: ListRow[] = models.map((m, idx) => {
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;
const rpmUsage = monitorResults[idx].rpm;
const tpmUsage = monitorResults[idx].tpm;
// RPM Quota Left = 1 - (rpmUsage / currentRPM) in percentage
let rpmQuotaPercent: number | null = null;
let rpmQuotaLabel: string | null = null;
if (rpmUsage >= 0 && currentRPM > 0) {
rpmQuotaPercent = Math.max(0, 100 - (rpmUsage / currentRPM) * 100);
rpmQuotaLabel = rpmQuotaPercent.toFixed(1) + "%";
}
// TPM Quota Left = 1 - (tpmUsage / currentTPM) in percentage
let tpmQuotaPercent: number | null = null;
let tpmQuotaLabel: string | null = null;
if (tpmUsage >= 0 && currentTPM > 0) {
tpmQuotaPercent = Math.max(0, 100 - (tpmUsage / currentTPM) * 100);
tpmQuotaLabel = tpmQuotaPercent.toFixed(1) + "%";
}
return {
model: m.model,
rpm: currentRPM > 0 ? formatNumber(currentRPM) : "-",
tpm: currentTPM > 0 ? formatNumber(currentTPM) : "-",
maxTpm: maxTPM > 0 ? formatNumber(maxTPM) : "-",
rpmQuotaLeft: rpmQuotaPercent,
tpmQuotaLeft: tpmQuotaPercent,
rpmQuotaLabel,
tpmQuotaLabel,
};
});
if (rows.length === 0) {
process.stdout.write("No models found.\n");
return;
}
printTable(rows);
},
});
+18 -8
View File
@@ -118,7 +118,10 @@ export interface BoxTableOptions {
rows: string[][];
/** Per-column alignment; defaults to left. */
align?: ("left" | "right")[];
/** Single gauge column (legacy; use barColumns for multiple). */
barColumn?: BarColumn;
/** Multiple gauge columns (overrides barColumn if provided). */
barColumns?: BarColumn[];
/** Return a colored variant of a plain cell value (must keep the same visible width). */
cellColor?: (rowIndex: number, colIndex: number, value: string) => string | undefined;
/** Stream used for color capability detection (default process.stdout). */
@@ -173,14 +176,19 @@ export function renderBoxTable(options: BoxTableOptions): string[] {
const out = options.out ?? process.stdout;
const level = colorLevel(out);
const align = options.align ?? [];
const barCol = options.barColumn;
const barWidth = barCol?.width ?? DEFAULT_BAR_WIDTH;
// Support both single barColumn (legacy) and multiple barColumns (new)
const barColumns = options.barColumns ?? (options.barColumn ? [options.barColumn] : []);
const columnCount = options.headers.length;
// Precompute bar cells so widths use their plain representation.
const barCells: RenderedCell[] = [];
if (barCol) {
// Precompute bar cells for all gauge columns
type BarCellsMap = Record<number, RenderedCell[]>;
const allBarCells: BarCellsMap = {};
for (const barCol of barColumns) {
const barWidth = barCol.width ?? DEFAULT_BAR_WIDTH;
const barCells: RenderedCell[] = [];
for (let rowIndex = 0; rowIndex < options.rows.length; rowIndex++) {
const percent = barCol.percents[rowIndex] ?? null;
const explicitLabel = barCol.labels?.[rowIndex];
@@ -192,10 +200,11 @@ export function renderBoxTable(options: BoxTableOptions): string[] {
: `${Number.isInteger(percent) ? percent : percent.toFixed(1)}%`;
barCells.push(buildBarCell(percent, label, barWidth, level));
}
allBarCells[barCol.index] = barCells;
}
const plainCell = (rowIndex: number, colIndex: number): string => {
if (barCol && colIndex === barCol.index) return barCells[rowIndex].plain;
if (allBarCells[colIndex]) return allBarCells[colIndex][rowIndex].plain;
return options.rows[rowIndex][colIndex] ?? "";
};
@@ -236,8 +245,9 @@ export function renderBoxTable(options: BoxTableOptions): string[] {
const cells: string[] = [];
for (let colIndex = 0; colIndex < columnCount; colIndex++) {
const width = widths[colIndex];
if (barCol && colIndex === barCol.index) {
const bar = barCells[rowIndex];
// Check if this column is a gauge column
if (allBarCells[colIndex]) {
const bar = allBarCells[colIndex][rowIndex];
const gap = width - displayWidth(bar.plain);
cells.push(gap > 0 ? bar.colored + " ".repeat(gap) : bar.colored);
continue;