Merge pull request #1347 from Runfusion/gsxdsm/activitydebug

fix(dashboard): show missing Minimax usage rows and weekly windows
This commit is contained in:
gsxdsm
2026-06-03 08:09:32 -07:00
committed by GitHub
3 changed files with 173 additions and 38 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
Fix missing model rows in the Minimax provider usage panel. The primary `general` model meters quota purely via `current_interval_remaining_percent` (its count fields are `0`), so the previous count-based visibility filter dropped it entirely.
Minimax usage now prefers the authoritative `*_remaining_percent` field (with a count-based fallback) and renders a window only when a model exposes any quota signal. Each model's separate weekly quota window (`current_weekly_remaining_percent`, `weekly_*` timing) is now surfaced as its own indicator alongside the interval window.

View File

@@ -2607,6 +2607,95 @@ describe("usage", () => {
expect(speechWindow).toBeDefined();
});
it("shows percent-only models and weekly windows from real coding_plan response", async () => {
// Mirrors a real coding_plan/remains response: the primary "general"
// model meters quota purely via *_remaining_percent (its count fields are
// 0), and every model also carries a separate weekly quota window.
const now = Date.now();
const mockResponse = {
model_remains: [
{
model_name: "general",
current_interval_total_count: 0,
current_interval_usage_count: 0,
current_interval_remaining_percent: 91,
remains_time: 2_039_915,
start_time: now - 3 * 60 * 60 * 1000,
end_time: now + 2 * 60 * 60 * 1000,
current_weekly_total_count: 0,
current_weekly_usage_count: 0,
current_weekly_remaining_percent: 100,
weekly_remains_time: 380_039_915,
weekly_start_time: now - 1 * 60 * 60 * 1000,
weekly_end_time: now + 6 * 24 * 60 * 60 * 1000,
},
{
model_name: "video",
current_interval_total_count: 3,
current_interval_usage_count: 3,
current_interval_remaining_percent: 100,
remains_time: 34_439_915,
start_time: now - 1 * 60 * 60 * 1000,
end_time: now + 23 * 60 * 60 * 1000,
current_weekly_total_count: 21,
current_weekly_usage_count: 21,
current_weekly_remaining_percent: 100,
weekly_remains_time: 380_039_915,
weekly_start_time: now - 1 * 60 * 60 * 1000,
weekly_end_time: now + 6 * 24 * 60 * 60 * 1000,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
// 2 models × (interval + weekly) = 4 windows
expect(minimax.windows).toHaveLength(4);
// "general" interval row must appear even though its count fields are 0 —
// remaining-percent is the source of truth.
const generalInterval = minimax.windows.find((w) => w.label === "general")!;
expect(generalInterval).toBeDefined();
expect(generalInterval.percentLeft).toBeCloseTo(91, 0);
expect(generalInterval.percentUsed).toBeCloseTo(9, 0);
// Weekly window is surfaced as its own indicator.
const generalWeekly = minimax.windows.find((w) => w.label === "general (weekly)")!;
expect(generalWeekly).toBeDefined();
expect(generalWeekly.percentLeft).toBeCloseTo(100, 0);
expect(minimax.windows.find((w) => w.label === "video")).toBeDefined();
expect(minimax.windows.find((w) => w.label === "video (weekly)")).toBeDefined();
});
it("skips models with zero quota", async () => {
const mockResponse = {
model_remains: [

View File

@@ -1458,50 +1458,89 @@ async function fetchMinimaxUsage(authStorage?: AuthStorageLike): Promise<Provide
const data = JSON.parse(res.body);
usage.status = "ok";
// Parse model_remains array — group by model family
// Each model in model_remains can expose two quota windows: a rolling
// interval window and a weekly window. Minimax reports remaining quota two
// ways — an authoritative *_remaining_percent field, and count fields that
// are frequently 0 for percent-metered models (e.g. the primary "general"
// model). Prefer the percent field; fall back to counts only when it's
// absent. Keying solely off the count field hides whole model rows.
const buildWindow = (
label: string,
totalCount: number,
// Note: Minimax's *_usage_count is actually REMAINING, not used
// (known API quirk per https://github.com/MiniMax-AI/MiniMax-M2/issues/99)
remainingCount: number,
remainingPercent: number | undefined,
remainsTime: number | undefined,
startTime: number | undefined,
endTime: number | undefined,
): UsageWindow | null => {
let percentLeft: number;
if (typeof remainingPercent === "number" && Number.isFinite(remainingPercent)) {
percentLeft = remainingPercent;
} else if (totalCount > 0) {
const used = Math.max(0, totalCount - remainingCount);
percentLeft = 100 - (used / totalCount) * 100;
} else {
// No quota signal at all — skip (unused model type / window).
return null;
}
percentLeft = Math.min(100, Math.max(0, percentLeft));
const percentUsed = Math.min(100, Math.max(0, 100 - percentLeft));
let resetText: string | null = null;
let resetMs: number | undefined;
let resetAt: string | undefined;
if (remainsTime && remainsTime > 0) {
resetMs = remainsTime;
resetText = `resets in ${formatDuration(remainsTime)}`;
resetAt = new Date(Date.now() + remainsTime).toISOString();
}
let windowDurationMs: number | undefined;
if (startTime && endTime) {
windowDurationMs = endTime - startTime;
}
return {
label,
percentUsed,
percentLeft,
resetText,
resetMs,
resetAt,
windowDurationMs,
};
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const modelRemains: any[] = data?.model_remains || [];
if (Array.isArray(modelRemains) && modelRemains.length > 0) {
if (Array.isArray(modelRemains)) {
for (const model of modelRemains) {
const modelName: string = model.model_name || "Unknown";
const total: number = model.current_interval_total_count ?? 0;
// Note: Minimax's current_interval_usage_count is actually REMAINING, not used
// (known API quirk per https://github.com/MiniMax-AI/MiniMax-M2/issues/99)
const remaining: number = model.current_interval_usage_count ?? 0;
const used: number = Math.max(0, total - remaining);
const percentUsed = total > 0 ? (used / total) * 100 : 0;
const interval = buildWindow(
modelName,
model.current_interval_total_count ?? 0,
model.current_interval_usage_count ?? 0,
model.current_interval_remaining_percent,
model.remains_time,
model.start_time,
model.end_time,
);
if (interval) usage.windows.push(interval);
let resetText: string | null = null;
let resetMs: number | undefined;
let windowDurationMs: number | undefined;
const remainsTime: number = model.remains_time;
let resetAt: string | undefined;
if (remainsTime && remainsTime > 0) {
resetMs = remainsTime;
resetText = `resets in ${formatDuration(remainsTime)}`;
resetAt = new Date(Date.now() + remainsTime).toISOString();
}
const startTime: number = model.start_time;
const endTime: number = model.end_time;
if (startTime && endTime) {
windowDurationMs = endTime - startTime;
}
// Only show models that have a quota > 0 (skip unused model types)
if (total > 0) {
usage.windows.push({
label: modelName,
percentUsed: Math.min(100, Math.max(0, percentUsed)),
percentLeft: Math.min(100, Math.max(0, 100 - percentUsed)),
resetText,
resetMs,
resetAt,
windowDurationMs,
});
}
const weekly = buildWindow(
`${modelName} (weekly)`,
model.current_weekly_total_count ?? 0,
model.current_weekly_usage_count ?? 0,
model.current_weekly_remaining_percent,
model.weekly_remains_time,
model.weekly_start_time,
model.weekly_end_time,
);
if (weekly) usage.windows.push(weekly);
}
}
} catch (e: unknown) {